mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-24 17:41:27 -04:00
Add live region
This commit is contained in:
89
web/components/examples/live-region-example.tsx
Normal file
89
web/components/examples/live-region-example.tsx
Normal file
@@ -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 (
|
||||
<button onClick={handleLike} aria-label={liked ? 'Remove like' : 'Like profile'}>
|
||||
{liked ? '❤️' : '🤍'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function MessageSentNotification() {
|
||||
const {announce} = useLiveRegion()
|
||||
|
||||
const handleSend = () => {
|
||||
announce('Message sent successfully', 'polite')
|
||||
}
|
||||
|
||||
return <button onClick={handleSend}>Send</button>
|
||||
}
|
||||
|
||||
function ErrorAlert({message}: {message: string}) {
|
||||
const {announce} = useLiveRegion()
|
||||
|
||||
const handleRetry = () => {
|
||||
announce('Retrying connection...', 'polite')
|
||||
}
|
||||
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Error: {message}</p>
|
||||
<button onClick={handleRetry}>Retry</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{status === 'success' && <p role="status">Success!</p>}
|
||||
<button type="submit" disabled={status === 'submitting'}>
|
||||
{status === 'submitting' ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingIndicator({isLoading}: {isLoading: boolean}) {
|
||||
const {announce} = useLiveRegion()
|
||||
|
||||
if (isLoading) {
|
||||
announce('Loading more results', 'polite')
|
||||
}
|
||||
|
||||
return isLoading ? <span aria-busy="true">Loading...</span> : null
|
||||
}
|
||||
|
||||
export {ErrorAlert, FormSubmission, LikeButton, LoadingIndicator, MessageSentNotification}
|
||||
38
web/components/live-region.tsx
Normal file
38
web/components/live-region.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {createContext, useCallback, useContext, useRef} from 'react'
|
||||
|
||||
interface LiveRegionContextValue {
|
||||
announce: (message: string, priority?: 'polite' | 'assertive') => void
|
||||
}
|
||||
|
||||
const LiveRegionContext = createContext<LiveRegionContextValue | null>(null)
|
||||
|
||||
export function useLiveRegion() {
|
||||
const context = useContext(LiveRegionContext)
|
||||
if (!context) {
|
||||
return {announce: () => {}}
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function LiveRegionProvider({children}: {children: React.ReactNode}) {
|
||||
const politeRef = useRef<HTMLDivElement>(null)
|
||||
const assertiveRef = useRef<HTMLDivElement>(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 (
|
||||
<LiveRegionContext.Provider value={{announce}}>
|
||||
{children}
|
||||
<div ref={politeRef} aria-live="polite" aria-atomic="true" className="sr-only" />
|
||||
<div ref={assertiveRef} aria-live="assertive" aria-atomic="true" className="sr-only" />
|
||||
</LiveRegionContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -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<PageProps>) {
|
||||
/>
|
||||
</Head>
|
||||
<PostHogProvider client={posthog}>
|
||||
<div
|
||||
className={clsx(
|
||||
'contents font-normal',
|
||||
logoFont.variable,
|
||||
// mainFont.variable
|
||||
)}
|
||||
>
|
||||
<I18nContext.Provider value={{locale, setLocale}}>
|
||||
<AuthProvider serverUser={pageProps.auth}>
|
||||
<HiddenProfilesProvider>
|
||||
<WebPush />
|
||||
<AndroidPush />
|
||||
<Component {...pageProps} />
|
||||
</HiddenProfilesProvider>
|
||||
</AuthProvider>
|
||||
</I18nContext.Provider>
|
||||
{/* Workaround for https://github.com/tailwindlabs/headlessui/discussions/666, to allow font CSS variable */}
|
||||
<div id="headlessui-portal-root">
|
||||
<div />
|
||||
<LiveRegionProvider>
|
||||
<div
|
||||
className={clsx(
|
||||
'contents font-normal',
|
||||
logoFont.variable,
|
||||
// mainFont.variable
|
||||
)}
|
||||
>
|
||||
<I18nContext.Provider value={{locale, setLocale}}>
|
||||
<ErrorBoundary>
|
||||
<AuthProvider serverUser={pageProps.auth}>
|
||||
<HiddenProfilesProvider>
|
||||
<WebPush />
|
||||
<AndroidPush />
|
||||
<Component {...pageProps} />
|
||||
</HiddenProfilesProvider>
|
||||
</AuthProvider>
|
||||
</ErrorBoundary>
|
||||
</I18nContext.Provider>
|
||||
{/* Workaround for https://github.com/tailwindlabs/headlessui/discussions/666, to allow font CSS variable */}
|
||||
<div id="headlessui-portal-root">
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LiveRegionProvider>
|
||||
</PostHogProvider>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user