{children ?? (
<>
{primaryButton && (
@@ -86,7 +102,7 @@ const PromotionalBanner = ({
{closeLabel}
diff --git a/src/app/components/PromotionalBanner/index.types.ts b/src/app/components/PromotionalBanner/index.types.ts
index 8a905b15515..bea5bdef46c 100644
--- a/src/app/components/PromotionalBanner/index.types.ts
+++ b/src/app/components/PromotionalBanner/index.types.ts
@@ -1,8 +1,20 @@
+import type { Interpolation, Theme } from '@emotion/react';
+
export interface PromotionalBannerButtonData {
text: string;
longText?: string;
}
+export type PromotionalBannerStyleOverrides = Partial<{
+ banner: Interpolation;
+ content: Interpolation;
+ textContainer: Interpolation;
+ title: Interpolation;
+ description: Interpolation;
+ actionsContainer: Interpolation;
+ closeButton: Interpolation;
+}>;
+
export interface PromotionalBannerConfig {
title: string;
description: string;
@@ -20,4 +32,6 @@ export interface PromotionalBannerProps extends PromotionalBannerConfig {
onSecondaryClick?: (event?: React.MouseEvent) => void;
onClose?: (event?: React.MouseEvent) => void;
children?: React.ReactNode;
+ topImage?: React.ReactNode;
+ styleOverrides?: PromotionalBannerStyleOverrides;
}
diff --git a/src/app/components/SaveArticleButton/SaveArticleButtonGuest/index.tsx b/src/app/components/SaveArticleButton/SaveArticleButtonGuest/index.tsx
index 72daa06a986..31ed1277715 100644
--- a/src/app/components/SaveArticleButton/SaveArticleButtonGuest/index.tsx
+++ b/src/app/components/SaveArticleButton/SaveArticleButtonGuest/index.tsx
@@ -1,13 +1,16 @@
import { ServiceContext } from '#contexts/ServiceContext';
import SaveButton from '#app/components/SaveButton';
-import { use } from 'react';
+import { use, useState } from 'react';
import useHydrationDetection from '#app/hooks/useHydrationDetection';
+import { AccountContext } from '#app/contexts/AccountContext';
+import AccountSignInModal from '#app/components/Account/AccountSignInModal';
-// TODO: This will contain the guest user experience for the SaveArticleButton,
-// which will likely involve prompting the user to sign in or create an account to save articles.
const SaveArticleButtonGuest = () => {
const { translations } = use(ServiceContext);
+ const { signInUrl, registerUrl } = use(AccountContext);
const isHydrated = useHydrationDetection();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
const getButtonText = () => {
if (!isHydrated) {
return translations.saveArticleButton?.loading;
@@ -16,15 +19,21 @@ const SaveArticleButtonGuest = () => {
};
return (
- {
- // eslint-disable-next-line no-alert
- alert('Please sign in to save articles.');
- }}
- buttonText={getButtonText()}
- testId="save-article-btn-guest"
- isLoading={!isHydrated}
- />
+ <>
+ setIsModalOpen(true)}
+ buttonText={getButtonText()}
+ testId="save-article-btn-guest"
+ isLoading={!isHydrated}
+ />
+ {isModalOpen && (
+ setIsModalOpen(false)}
+ signInUrl={signInUrl}
+ registerUrl={registerUrl}
+ />
+ )}
+ >
);
};
diff --git a/src/app/hooks/useTrappedFocus/index.ts b/src/app/hooks/useTrappedFocus/index.ts
new file mode 100644
index 00000000000..70fc88a06fe
--- /dev/null
+++ b/src/app/hooks/useTrappedFocus/index.ts
@@ -0,0 +1,61 @@
+import { useEffect, useRef } from 'react';
+
+const FOCUSABLE_SELECTOR =
+ 'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
+
+const useTrappedFocus = <
+ C extends HTMLElement = HTMLElement,
+ F extends HTMLElement = HTMLElement,
+ L extends HTMLElement = HTMLElement,
+>() => {
+ const containerRef = useRef(null);
+ const firstElementRef = useRef(null);
+ const lastElementRef = useRef(null);
+
+ useEffect(() => {
+ const onDismissFocusElement = document.activeElement as HTMLElement | null;
+ let currentModalFocusRef: Element | null = null;
+
+ const focusListenerWithErrorWrapper = (event: FocusEvent) => {
+ try {
+ if (!containerRef.current) return;
+
+ const isInModal = containerRef.current.contains(event.target as Node);
+
+ if (isInModal && event.target !== containerRef.current) {
+ currentModalFocusRef = event.target as Element;
+ } else {
+ const wasFirstElementFocused =
+ currentModalFocusRef === firstElementRef.current;
+
+ const lastElement: HTMLElement | null =
+ lastElementRef.current ??
+ Array.from(
+ containerRef.current.querySelectorAll(
+ FOCUSABLE_SELECTOR,
+ ),
+ ).pop() ??
+ null;
+
+ if (wasFirstElementFocused) {
+ lastElement?.focus();
+ } else {
+ firstElementRef.current?.focus();
+ }
+ }
+ } catch (error) {} // eslint-disable-line no-empty
+ };
+
+ window.addEventListener('focus', focusListenerWithErrorWrapper, true);
+ firstElementRef.current?.focus();
+
+ return () => {
+ window.removeEventListener('focus', focusListenerWithErrorWrapper, true);
+ onDismissFocusElement?.focus();
+ };
+ }, []);
+
+ return { containerRef, firstElementRef, lastElementRef };
+};
+
+export default useTrappedFocus;