183 lines
5.4 KiB
TypeScript
183 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
import type { A11yState } from './state.js'
|
|
|
|
import { useAccessibility } from './AccessibilityProvider.js'
|
|
|
|
export type A11yTexts = {
|
|
bigCursor?: string
|
|
close?: string
|
|
contrast?: string
|
|
contrastHigh?: string
|
|
contrastInverted?: string
|
|
grayscale?: string
|
|
lineHeight?: string
|
|
open?: string
|
|
readableFont?: string
|
|
reduceMotion?: string
|
|
reset?: string
|
|
textSize?: string
|
|
title?: string
|
|
underlineLinks?: string
|
|
}
|
|
|
|
const DEFAULT_TEXTS: Required<A11yTexts> = {
|
|
bigCursor: 'Duży kursor',
|
|
close: 'Zamknij',
|
|
contrast: 'Kontrast',
|
|
contrastHigh: 'Wysoki',
|
|
contrastInverted: 'Odwrócony',
|
|
grayscale: 'Skala szarości',
|
|
lineHeight: 'Odstęp między liniami',
|
|
open: 'Otwórz panel dostępności',
|
|
readableFont: 'Czytelna czcionka',
|
|
reduceMotion: 'Wyłącz animacje',
|
|
reset: 'Resetuj',
|
|
textSize: 'Rozmiar tekstu',
|
|
title: 'Dostępność',
|
|
underlineLinks: 'Podkreśl linki',
|
|
}
|
|
|
|
export type A11yClassNames = {
|
|
active?: string
|
|
button?: string
|
|
closeButton?: string
|
|
control?: string
|
|
label?: string
|
|
panel?: string
|
|
resetButton?: string
|
|
row?: string
|
|
}
|
|
|
|
/**
|
|
* Accessibility toolbar: a floating button that opens a panel of options (text
|
|
* size, line height, contrast, grayscale, underline links, readable font, reduce
|
|
* motion, big cursor). Choices persist in a cookie and apply as data-attributes
|
|
* on <html> (the project's CSS styles them).
|
|
*
|
|
* Unstyled by default — pass classNames to match the project's design (like
|
|
* CookieBanner). Wrap the app in <AccessibilityProvider> first.
|
|
*
|
|
* <AccessibilityWidget classNames={{ button: 'a11y-btn', panel: 'a11y-panel' }} />
|
|
*/
|
|
export function AccessibilityWidget({
|
|
classNames,
|
|
texts,
|
|
}: {
|
|
classNames?: A11yClassNames
|
|
texts?: A11yTexts
|
|
}) {
|
|
const { reset, set, state } = useAccessibility()
|
|
const [open, setOpen] = useState(false)
|
|
const t = { ...DEFAULT_TEXTS, ...texts }
|
|
const cn = classNames ?? {}
|
|
|
|
const toggle = <K extends keyof A11yState>(key: K) => set(key, !state[key] as A11yState[K])
|
|
|
|
const isActive = (on: boolean) => (on ? (cn.active ?? '') : '')
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
aria-expanded={open}
|
|
aria-label={t.open}
|
|
className={cn.button}
|
|
onClick={() => setOpen((o) => !o)}
|
|
type="button"
|
|
>
|
|
{/* Project can style/replace via CSS; simple glyph fallback. */}
|
|
<span aria-hidden="true">♿</span>
|
|
</button>
|
|
|
|
{open && (
|
|
<div aria-label={t.title} className={cn.panel} role="dialog">
|
|
<div className={cn.row}>
|
|
<span className={cn.label}>{t.textSize}</span>
|
|
<div className={cn.control}>
|
|
{[0, 1, 2, 3].map((n) => (
|
|
<button
|
|
className={isActive(state.textSize === n)}
|
|
key={n}
|
|
onClick={() => set('textSize', n as A11yState['textSize'])}
|
|
type="button"
|
|
>
|
|
A{n > 0 ? '+'.repeat(n) : ''}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={cn.row}>
|
|
<span className={cn.label}>{t.lineHeight}</span>
|
|
<div className={cn.control}>
|
|
{[0, 1, 2].map((n) => (
|
|
<button
|
|
className={isActive(state.lineHeight === n)}
|
|
key={n}
|
|
onClick={() => set('lineHeight', n as A11yState['lineHeight'])}
|
|
type="button"
|
|
>
|
|
{n === 0 ? '—' : '≡'.repeat(n)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={cn.row}>
|
|
<span className={cn.label}>{t.contrast}</span>
|
|
<div className={cn.control}>
|
|
<button
|
|
className={isActive(state.contrast === 'high')}
|
|
onClick={() => set('contrast', state.contrast === 'high' ? 'default' : 'high')}
|
|
type="button"
|
|
>
|
|
{t.contrastHigh}
|
|
</button>
|
|
<button
|
|
className={isActive(state.contrast === 'inverted')}
|
|
onClick={() =>
|
|
set('contrast', state.contrast === 'inverted' ? 'default' : 'inverted')
|
|
}
|
|
type="button"
|
|
>
|
|
{t.contrastInverted}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{(
|
|
[
|
|
['grayscale', t.grayscale],
|
|
['underlineLinks', t.underlineLinks],
|
|
['readableFont', t.readableFont],
|
|
['reduceMotion', t.reduceMotion],
|
|
['bigCursor', t.bigCursor],
|
|
] as Array<[keyof A11yState, string]>
|
|
).map(([key, label]) => (
|
|
<div className={cn.row} key={key}>
|
|
<span className={cn.label}>{label}</span>
|
|
<button
|
|
aria-pressed={Boolean(state[key])}
|
|
className={`${cn.control ?? ''} ${isActive(Boolean(state[key]))}`}
|
|
onClick={() => toggle(key)}
|
|
type="button"
|
|
>
|
|
{state[key] ? 'ON' : 'OFF'}
|
|
</button>
|
|
</div>
|
|
))}
|
|
|
|
<button className={cn.resetButton} onClick={reset} type="button">
|
|
{t.reset}
|
|
</button>
|
|
<button className={cn.closeButton} onClick={() => setOpen(false)} type="button">
|
|
{t.close}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|