Added accessibility support

This commit is contained in:
2026-09-26 22:29:53 +02:00
parent fa2b607979
commit 827cd9bcbc
29 changed files with 1021 additions and 3 deletions
@@ -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
}