95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
'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
|
|
}
|