diff --git a/web/components/examples/live-region-example.tsx b/web/components/examples/live-region-example.tsx new file mode 100644 index 00000000..68262f97 --- /dev/null +++ b/web/components/examples/live-region-example.tsx @@ -0,0 +1,89 @@ +import {useState} from 'react' +import {useLiveRegion} from 'web/components/live-region' + +function LikeButton({profileId: _profileId}: {profileId: string}) { + const [liked, setLiked] = useState(false) + const {announce} = useLiveRegion() + + const handleLike = () => { + const newLiked = !liked + setLiked(newLiked) + + if (newLiked) { + announce('Profile liked', 'polite') + } else { + announce('Like removed', 'polite') + } + } + + return ( + + ) +} + +function MessageSentNotification() { + const {announce} = useLiveRegion() + + const handleSend = () => { + announce('Message sent successfully', 'polite') + } + + return +} + +function ErrorAlert({message}: {message: string}) { + const {announce} = useLiveRegion() + + const handleRetry = () => { + announce('Retrying connection...', 'polite') + } + + return ( +
+

Error: {message}

+ +
+ ) +} + +function FormSubmission() { + const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle') + const {announce} = useLiveRegion() + + const handleSubmit = async () => { + setStatus('submitting') + announce('Submitting form, please wait', 'polite') + + try { + await new Promise((resolve) => setTimeout(resolve, 1000)) + setStatus('success') + announce('Form submitted successfully', 'polite') + } catch { + setStatus('error') + announce('Form submission failed. Please try again.', 'assertive') + } + } + + return ( +
+ {status === 'success' &&

Success!

} + +
+ ) +} + +function LoadingIndicator({isLoading}: {isLoading: boolean}) { + const {announce} = useLiveRegion() + + if (isLoading) { + announce('Loading more results', 'polite') + } + + return isLoading ? Loading... : null +} + +export {ErrorAlert, FormSubmission, LikeButton, LoadingIndicator, MessageSentNotification} diff --git a/web/components/live-region.tsx b/web/components/live-region.tsx new file mode 100644 index 00000000..537e26e3 --- /dev/null +++ b/web/components/live-region.tsx @@ -0,0 +1,38 @@ +import {createContext, useCallback, useContext, useRef} from 'react' + +interface LiveRegionContextValue { + announce: (message: string, priority?: 'polite' | 'assertive') => void +} + +const LiveRegionContext = createContext(null) + +export function useLiveRegion() { + const context = useContext(LiveRegionContext) + if (!context) { + return {announce: () => {}} + } + return context +} + +export function LiveRegionProvider({children}: {children: React.ReactNode}) { + const politeRef = useRef(null) + const assertiveRef = useRef(null) + + const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => { + const element = priority === 'assertive' ? assertiveRef.current : politeRef.current + if (element) { + element.textContent = '' + setTimeout(() => { + if (element) element.textContent = message + }, 50) + } + }, []) + + return ( + + {children} +
+
+ + ) +} diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 7ee606ef..177ff5ac 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -16,6 +16,8 @@ import posthog from 'posthog-js' import {PostHogProvider} from 'posthog-js/react' import {useEffect, useState} from 'react' import {AuthProvider, AuthUser} from 'web/components/auth-context' +import {ErrorBoundary} from 'web/components/error-boundary' +import {LiveRegionProvider} from 'web/components/live-region' import {useFontPreferenceManager} from 'web/hooks/use-font-preference' import {useHasLoaded} from 'web/hooks/use-has-loaded' import {HiddenProfilesProvider} from 'web/hooks/use-hidden-profiles' @@ -181,27 +183,31 @@ function MyApp(props: AppProps) { /> -
- - - - - - - - - - {/* Workaround for https://github.com/tailwindlabs/headlessui/discussions/666, to allow font CSS variable */} -
-
+ +
+ + + + + + + + + + + + {/* Workaround for https://github.com/tailwindlabs/headlessui/discussions/666, to allow font CSS variable */} +
+
+
-
+
)