Added accessibility support
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, use, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
A11Y_COOKIE,
|
||||
A11Y_COOKIE_MAX_AGE,
|
||||
A11Y_DEFAULT,
|
||||
a11yAttributes,
|
||||
type A11yState,
|
||||
parseA11y,
|
||||
serializeA11y,
|
||||
} from './state.js'
|
||||
|
||||
type A11yContextValue = {
|
||||
reset: () => void
|
||||
set: <K extends keyof A11yState>(key: K, value: A11yState[K]) => void
|
||||
state: A11yState
|
||||
}
|
||||
|
||||
const A11yContext = createContext<A11yContextValue | null>(null)
|
||||
|
||||
function readCookie(name: string): string | undefined {
|
||||
if (typeof document === 'undefined') {return undefined}
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
|
||||
return match ? decodeURIComponent(match[1]) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides accessibility preferences, persists them in a cookie, and applies them
|
||||
* as data-attributes on <html> so the project's CSS can react. Like
|
||||
* ConsentProvider for cookies — wrap the app once; the widget/button consume it.
|
||||
*
|
||||
* The plugin ships NO styles: it only sets attributes (data-a11y-*). The project
|
||||
* writes CSS for those it supports (see docs/accessibility.md). This keeps the
|
||||
* design in the project's hands.
|
||||
*
|
||||
* // layout.tsx
|
||||
* import { AccessibilityProvider } from '@intecion/ipal-kit/client'
|
||||
* <AccessibilityProvider>{children}</AccessibilityProvider>
|
||||
*
|
||||
* To avoid a flash, the project can read the a11y-prefs cookie server-side and
|
||||
* set the attributes on <html> during SSR (see docs). This provider re-applies
|
||||
* on the client and keeps them in sync.
|
||||
*/
|
||||
export function AccessibilityProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState<A11yState>(A11Y_DEFAULT)
|
||||
|
||||
// Hydrate from cookie on mount.
|
||||
useEffect(() => {
|
||||
setState(parseA11y(readCookie(A11Y_COOKIE)))
|
||||
}, [])
|
||||
|
||||
// Apply attributes to <html> whenever state changes.
|
||||
useEffect(() => {
|
||||
const el = document.documentElement
|
||||
const attrs = a11yAttributes(state)
|
||||
for (const [attr, value] of Object.entries(attrs)) {
|
||||
if (value === null) {el.removeAttribute(attr)}
|
||||
else {el.setAttribute(attr, value)}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
const persist = useCallback((next: A11yState) => {
|
||||
document.cookie = `${A11Y_COOKIE}=${encodeURIComponent(
|
||||
serializeA11y(next),
|
||||
)}; path=/; max-age=${A11Y_COOKIE_MAX_AGE}; samesite=lax`
|
||||
}, [])
|
||||
|
||||
const set = useCallback(
|
||||
<K extends keyof A11yState>(key: K, value: A11yState[K]) => {
|
||||
setState((prev) => {
|
||||
const next = { ...prev, [key]: value }
|
||||
persist(next)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[persist],
|
||||
)
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState(A11Y_DEFAULT)
|
||||
persist(A11Y_DEFAULT)
|
||||
}, [persist])
|
||||
|
||||
return <A11yContext value={{ reset, set, state }}>{children}</A11yContext>
|
||||
}
|
||||
|
||||
/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */
|
||||
export function useAccessibility(): A11yContextValue {
|
||||
const ctx = use(A11yContext)
|
||||
if (!ctx) {throw new Error('useAccessibility must be used within <AccessibilityProvider>')}
|
||||
return ctx
|
||||
}
|
||||
Reference in New Issue
Block a user