@bquery/bquery 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -23
- package/dist/a11y/announce.d.ts +43 -0
- package/dist/a11y/announce.d.ts.map +1 -0
- package/dist/a11y/audit.d.ts +42 -0
- package/dist/a11y/audit.d.ts.map +1 -0
- package/dist/a11y/index.d.ts +53 -0
- package/dist/a11y/index.d.ts.map +1 -0
- package/dist/a11y/media-preferences.d.ts +77 -0
- package/dist/a11y/media-preferences.d.ts.map +1 -0
- package/dist/a11y/roving-tab-index.d.ts +38 -0
- package/dist/a11y/roving-tab-index.d.ts.map +1 -0
- package/dist/a11y/skip-link.d.ts +37 -0
- package/dist/a11y/skip-link.d.ts.map +1 -0
- package/dist/a11y/trap-focus.d.ts +49 -0
- package/dist/a11y/trap-focus.d.ts.map +1 -0
- package/dist/a11y/types.d.ts +152 -0
- package/dist/a11y/types.d.ts.map +1 -0
- package/dist/a11y-C5QOVvRn.js +421 -0
- package/dist/a11y-C5QOVvRn.js.map +1 -0
- package/dist/a11y.es.mjs +14 -0
- package/dist/component/component.d.ts +13 -5
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/html.d.ts +40 -3
- package/dist/component/html.d.ts.map +1 -1
- package/dist/component/index.d.ts +3 -2
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/component/scope.d.ts +138 -0
- package/dist/component/scope.d.ts.map +1 -0
- package/dist/component/types.d.ts +184 -17
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-CuuTijA6.js +684 -0
- package/dist/component-CuuTijA6.js.map +1 -0
- package/dist/component.es.mjs +10 -6
- package/dist/{config-DRmZZno3.js → config-BW35FKuA.js} +4 -4
- package/dist/config-BW35FKuA.js.map +1 -0
- package/dist/constraints-3lV9yyBw.js +100 -0
- package/dist/constraints-3lV9yyBw.js.map +1 -0
- package/dist/core/collection.d.ts +48 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +92 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/env.d.ts +18 -0
- package/dist/core/env.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/shared.d.ts +8 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core/utils/index.d.ts +52 -41
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core-Cjl7GUu8.js +717 -0
- package/dist/core-Cjl7GUu8.js.map +1 -0
- package/dist/{core-DPdbItcq.js → core-DnlyjbF2.js} +1 -1
- package/dist/{core-DPdbItcq.js.map → core-DnlyjbF2.js.map} +1 -1
- package/dist/core.es.mjs +45 -44
- package/dist/custom-directives-7wAShnnd.js +9 -0
- package/dist/custom-directives-7wAShnnd.js.map +1 -0
- package/dist/devtools/devtools.d.ts +212 -0
- package/dist/devtools/devtools.d.ts.map +1 -0
- package/dist/devtools/index.d.ts +20 -0
- package/dist/devtools/index.d.ts.map +1 -0
- package/dist/devtools/types.d.ts +69 -0
- package/dist/devtools/types.d.ts.map +1 -0
- package/dist/devtools-D2fQLhDN.js +122 -0
- package/dist/devtools-D2fQLhDN.js.map +1 -0
- package/dist/devtools.es.mjs +19 -0
- package/dist/dnd/draggable.d.ts +51 -0
- package/dist/dnd/draggable.d.ts.map +1 -0
- package/dist/dnd/droppable.d.ts +38 -0
- package/dist/dnd/droppable.d.ts.map +1 -0
- package/dist/dnd/index.d.ts +47 -0
- package/dist/dnd/index.d.ts.map +1 -0
- package/dist/dnd/sortable.d.ts +43 -0
- package/dist/dnd/sortable.d.ts.map +1 -0
- package/dist/dnd/types.d.ts +250 -0
- package/dist/dnd/types.d.ts.map +1 -0
- package/dist/dnd-B8EgyzaI.js +244 -0
- package/dist/dnd-B8EgyzaI.js.map +1 -0
- package/dist/dnd.es.mjs +6 -0
- package/dist/env-NeVmr4Gf.js +19 -0
- package/dist/env-NeVmr4Gf.js.map +1 -0
- package/dist/forms/create-form.d.ts +49 -0
- package/dist/forms/create-form.d.ts.map +1 -0
- package/dist/forms/index.d.ts +39 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/types.d.ts +139 -0
- package/dist/forms/types.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +179 -0
- package/dist/forms/validators.d.ts.map +1 -0
- package/dist/forms-C3yovgH9.js +141 -0
- package/dist/forms-C3yovgH9.js.map +1 -0
- package/dist/forms.es.mjs +14 -0
- package/dist/full.d.ts +37 -9
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +186 -91
- package/dist/full.iife.js +47 -31
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +47 -31
- package/dist/full.umd.js.map +1 -1
- package/dist/i18n/formatting.d.ts +40 -0
- package/dist/i18n/formatting.d.ts.map +1 -0
- package/dist/i18n/i18n.d.ts +48 -0
- package/dist/i18n/i18n.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +57 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/translate.d.ts +83 -0
- package/dist/i18n/translate.d.ts.map +1 -0
- package/dist/i18n/types.d.ts +156 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n-BnnhTFOS.js +89 -0
- package/dist/i18n-BnnhTFOS.js.map +1 -0
- package/dist/i18n.es.mjs +6 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.mjs +233 -138
- package/dist/media/battery.d.ts +35 -0
- package/dist/media/battery.d.ts.map +1 -0
- package/dist/media/breakpoints.d.ts +51 -0
- package/dist/media/breakpoints.d.ts.map +1 -0
- package/dist/media/clipboard.d.ts +30 -0
- package/dist/media/clipboard.d.ts.map +1 -0
- package/dist/media/device-sensors.d.ts +54 -0
- package/dist/media/device-sensors.d.ts.map +1 -0
- package/dist/media/geolocation.d.ts +38 -0
- package/dist/media/geolocation.d.ts.map +1 -0
- package/dist/media/index.d.ts +42 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/media-query.d.ts +36 -0
- package/dist/media/media-query.d.ts.map +1 -0
- package/dist/media/network.d.ts +35 -0
- package/dist/media/network.d.ts.map +1 -0
- package/dist/media/types.d.ts +173 -0
- package/dist/media/types.d.ts.map +1 -0
- package/dist/media/viewport.d.ts +32 -0
- package/dist/media/viewport.d.ts.map +1 -0
- package/dist/media-Di2Ta22s.js +340 -0
- package/dist/media-Di2Ta22s.js.map +1 -0
- package/dist/media.es.mjs +12 -0
- package/dist/motion/index.d.ts +7 -3
- package/dist/motion/index.d.ts.map +1 -1
- package/dist/motion/morph.d.ts +27 -0
- package/dist/motion/morph.d.ts.map +1 -0
- package/dist/motion/parallax.d.ts +30 -0
- package/dist/motion/parallax.d.ts.map +1 -0
- package/dist/motion/reduced-motion.d.ts +36 -3
- package/dist/motion/reduced-motion.d.ts.map +1 -1
- package/dist/motion/types.d.ts +58 -0
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion/typewriter.d.ts +31 -0
- package/dist/motion/typewriter.d.ts.map +1 -0
- package/dist/motion-qPj_TYGv.js +530 -0
- package/dist/motion-qPj_TYGv.js.map +1 -0
- package/dist/motion.es.mjs +27 -23
- package/dist/mount-SM07RUa6.js +403 -0
- package/dist/mount-SM07RUa6.js.map +1 -0
- package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
- package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
- package/dist/{platform-B7JhGBc7.js → platform-CPbCprb6.js} +3 -3
- package/dist/platform-CPbCprb6.js.map +1 -0
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/index.d.ts +22 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/registry.d.ts +108 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/types.d.ts +110 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin-cPoOHFLY.js +64 -0
- package/dist/plugin-cPoOHFLY.js.map +1 -0
- package/dist/plugin.es.mjs +9 -0
- package/dist/reactive/computed.d.ts +7 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive-Cfv0RK6x.js +233 -0
- package/dist/reactive-Cfv0RK6x.js.map +1 -0
- package/dist/reactive.es.mjs +18 -17
- package/dist/registry-CWf368tT.js +26 -0
- package/dist/registry-CWf368tT.js.map +1 -0
- package/dist/router/bq-link.d.ts +112 -0
- package/dist/router/bq-link.d.ts.map +1 -0
- package/dist/router/constraints.d.ts +9 -0
- package/dist/router/constraints.d.ts.map +1 -0
- package/dist/router/index.d.ts +14 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/match.d.ts +0 -1
- package/dist/router/match.d.ts.map +1 -1
- package/dist/router/path-pattern.d.ts +14 -0
- package/dist/router/path-pattern.d.ts.map +1 -0
- package/dist/router/query.d.ts.map +1 -1
- package/dist/router/router.d.ts +3 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/types.d.ts +48 -4
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/use-route.d.ts +50 -0
- package/dist/router/use-route.d.ts.map +1 -0
- package/dist/router/utils.d.ts +3 -0
- package/dist/router/utils.d.ts.map +1 -1
- package/dist/router-BrthaP_z.js +473 -0
- package/dist/router-BrthaP_z.js.map +1 -0
- package/dist/router.es.mjs +13 -10
- package/dist/{sanitize-jyJ2ryE2.js → sanitize-B1V4JswB.js} +95 -83
- package/dist/sanitize-B1V4JswB.js.map +1 -0
- package/dist/security/index.d.ts +2 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/sanitize.d.ts +4 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/trusted-html.d.ts +53 -0
- package/dist/security/trusted-html.d.ts.map +1 -0
- package/dist/security.es.mjs +10 -9
- package/dist/ssr/hydrate.d.ts +65 -0
- package/dist/ssr/hydrate.d.ts.map +1 -0
- package/dist/ssr/index.d.ts +59 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/render.d.ts +62 -0
- package/dist/ssr/render.d.ts.map +1 -0
- package/dist/ssr/serialize.d.ts +118 -0
- package/dist/ssr/serialize.d.ts.map +1 -0
- package/dist/ssr/types.d.ts +70 -0
- package/dist/ssr/types.d.ts.map +1 -0
- package/dist/ssr-B2qd_WBB.js +248 -0
- package/dist/ssr-B2qd_WBB.js.map +1 -0
- package/dist/ssr.es.mjs +9 -0
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/define-store.d.ts +1 -1
- package/dist/store/define-store.d.ts.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/mapping.d.ts +1 -1
- package/dist/store/mapping.d.ts.map +1 -1
- package/dist/store/persisted.d.ts +38 -4
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/store/types.d.ts +140 -3
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/utils.d.ts +2 -2
- package/dist/store/utils.d.ts.map +1 -1
- package/dist/store/watch.d.ts +1 -1
- package/dist/store/watch.d.ts.map +1 -1
- package/dist/store-DWpyH6p5.js +338 -0
- package/dist/store-DWpyH6p5.js.map +1 -0
- package/dist/store.es.mjs +11 -10
- package/dist/storybook/index.d.ts +37 -0
- package/dist/storybook/index.d.ts.map +1 -0
- package/dist/storybook.es.mjs +151 -0
- package/dist/storybook.es.mjs.map +1 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/testing.d.ts +156 -0
- package/dist/testing/testing.d.ts.map +1 -0
- package/dist/testing/types.d.ts +134 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing-CsqjNUyy.js +224 -0
- package/dist/testing-CsqjNUyy.js.map +1 -0
- package/dist/testing.es.mjs +9 -0
- package/dist/type-guards-Do9DWgNp.js +44 -0
- package/dist/type-guards-Do9DWgNp.js.map +1 -0
- package/dist/untrack-DJVQQ2WM.js +33 -0
- package/dist/untrack-DJVQQ2WM.js.map +1 -0
- package/dist/view/custom-directives.d.ts +20 -0
- package/dist/view/custom-directives.d.ts.map +1 -0
- package/dist/view/evaluate.d.ts.map +1 -1
- package/dist/view/process.d.ts.map +1 -1
- package/dist/view.es.mjs +11 -10
- package/package.json +52 -11
- package/src/a11y/announce.ts +131 -0
- package/src/a11y/audit.ts +314 -0
- package/src/a11y/index.ts +68 -0
- package/src/a11y/media-preferences.ts +255 -0
- package/src/a11y/roving-tab-index.ts +164 -0
- package/src/a11y/skip-link.ts +255 -0
- package/src/a11y/trap-focus.ts +184 -0
- package/src/a11y/types.ts +183 -0
- package/src/component/component.ts +345 -65
- package/src/component/html.ts +153 -53
- package/src/component/index.ts +12 -2
- package/src/component/library.ts +66 -28
- package/src/component/scope.ts +212 -0
- package/src/component/types.ts +238 -19
- package/src/core/collection.ts +707 -628
- package/src/core/element.ts +981 -774
- package/src/core/env.ts +60 -0
- package/src/core/index.ts +49 -48
- package/src/core/shared.ts +62 -13
- package/src/core/utils/index.ts +148 -83
- package/src/devtools/devtools.ts +410 -0
- package/src/devtools/index.ts +48 -0
- package/src/devtools/types.ts +104 -0
- package/src/dnd/draggable.ts +296 -0
- package/src/dnd/droppable.ts +228 -0
- package/src/dnd/index.ts +62 -0
- package/src/dnd/sortable.ts +307 -0
- package/src/dnd/types.ts +293 -0
- package/src/forms/create-form.ts +278 -0
- package/src/forms/index.ts +65 -0
- package/src/forms/types.ts +154 -0
- package/src/forms/validators.ts +265 -0
- package/src/full.ts +260 -3
- package/src/i18n/formatting.ts +67 -0
- package/src/i18n/i18n.ts +200 -0
- package/src/i18n/index.ts +67 -0
- package/src/i18n/translate.ts +182 -0
- package/src/i18n/types.ts +171 -0
- package/src/index.ts +108 -36
- package/src/media/battery.ts +116 -0
- package/src/media/breakpoints.ts +131 -0
- package/src/media/clipboard.ts +80 -0
- package/src/media/device-sensors.ts +158 -0
- package/src/media/geolocation.ts +119 -0
- package/src/media/index.ts +76 -0
- package/src/media/media-query.ts +92 -0
- package/src/media/network.ts +115 -0
- package/src/media/types.ts +177 -0
- package/src/media/viewport.ts +84 -0
- package/src/motion/index.ts +57 -48
- package/src/motion/morph.ts +151 -0
- package/src/motion/parallax.ts +120 -0
- package/src/motion/reduced-motion.ts +66 -17
- package/src/motion/transition.ts +97 -97
- package/src/motion/types.ts +63 -0
- package/src/motion/typewriter.ts +164 -0
- package/src/platform/announcer.ts +208 -208
- package/src/platform/config.ts +163 -163
- package/src/platform/cookies.ts +165 -165
- package/src/platform/index.ts +39 -39
- package/src/platform/meta.ts +168 -168
- package/src/plugin/index.ts +37 -0
- package/src/plugin/registry.ts +269 -0
- package/src/plugin/types.ts +137 -0
- package/src/reactive/async-data.ts +486 -486
- package/src/reactive/computed.ts +130 -92
- package/src/reactive/index.ts +37 -37
- package/src/reactive/signal.ts +29 -29
- package/src/router/bq-link.ts +279 -0
- package/src/router/constraints.ts +201 -0
- package/src/router/index.ts +49 -41
- package/src/router/match.ts +312 -106
- package/src/router/path-pattern.ts +52 -0
- package/src/router/query.ts +38 -35
- package/src/router/router.ts +402 -211
- package/src/router/types.ts +139 -93
- package/src/router/use-route.ts +68 -0
- package/src/router/utils.ts +157 -116
- package/src/security/constants.ts +211 -211
- package/src/security/index.ts +12 -10
- package/src/security/sanitize.ts +6 -2
- package/src/security/trusted-html.ts +71 -0
- package/src/ssr/hydrate.ts +82 -0
- package/src/ssr/index.ts +70 -0
- package/src/ssr/render.ts +508 -0
- package/src/ssr/serialize.ts +296 -0
- package/src/ssr/types.ts +81 -0
- package/src/store/create-store.ts +467 -329
- package/src/store/define-store.ts +2 -1
- package/src/store/index.ts +27 -22
- package/src/store/mapping.ts +2 -1
- package/src/store/persisted.ts +249 -61
- package/src/store/types.ts +247 -94
- package/src/store/utils.ts +135 -141
- package/src/store/watch.ts +2 -1
- package/src/storybook/index.ts +480 -0
- package/src/testing/index.ts +42 -0
- package/src/testing/testing.ts +593 -0
- package/src/testing/types.ts +170 -0
- package/src/view/custom-directives.ts +30 -0
- package/src/view/evaluate.ts +292 -290
- package/src/view/process.ts +108 -92
- package/dist/component-CY5MVoYN.js +0 -531
- package/dist/component-CY5MVoYN.js.map +0 -1
- package/dist/config-DRmZZno3.js.map +0 -1
- package/dist/core-CK2Mfpf4.js +0 -648
- package/dist/core-CK2Mfpf4.js.map +0 -1
- package/dist/motion-C5DRdPnO.js +0 -415
- package/dist/motion-C5DRdPnO.js.map +0 -1
- package/dist/platform-B7JhGBc7.js.map +0 -1
- package/dist/reactive-BDya-ia8.js +0 -253
- package/dist/reactive-BDya-ia8.js.map +0 -1
- package/dist/router-CijiICxt.js +0 -188
- package/dist/router-CijiICxt.js.map +0 -1
- package/dist/sanitize-jyJ2ryE2.js.map +0 -1
- package/dist/store-CPK9E62U.js +0 -262
- package/dist/store-CPK9E62U.js.map +0 -1
- package/dist/view-Cdi0g-qo.js +0 -396
- package/dist/view-Cdi0g-qo.js.map +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen reader announcement utility using ARIA live regions.
|
|
3
|
+
*
|
|
4
|
+
* Creates and manages off-screen live regions to announce dynamic
|
|
5
|
+
* content changes to assistive technologies.
|
|
6
|
+
*
|
|
7
|
+
* @module bquery/a11y
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AnnouncePriority } from './types';
|
|
11
|
+
|
|
12
|
+
/** Cache for live region containers, keyed by priority. */
|
|
13
|
+
const liveRegions = new Map<AnnouncePriority, HTMLElement>();
|
|
14
|
+
const pendingAnnouncements = new Map<AnnouncePriority, ReturnType<typeof setTimeout>>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Delay in milliseconds before updating the live region text.
|
|
18
|
+
* This ensures screen readers detect the content change even when
|
|
19
|
+
* the same message is announced consecutively — clearing first and
|
|
20
|
+
* setting after a short timer delay forces a new live-region mutation event.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
const ANNOUNCEMENT_DELAY_MS = 50;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets or creates a visually-hidden ARIA live region for the given priority.
|
|
27
|
+
*
|
|
28
|
+
* @param priority - The aria-live priority level
|
|
29
|
+
* @returns The live region element
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
const getOrCreateLiveRegion = (priority: AnnouncePriority): HTMLElement => {
|
|
33
|
+
const existing = liveRegions.get(priority);
|
|
34
|
+
if (existing && existing.isConnected) {
|
|
35
|
+
return existing;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const el = document.createElement('div');
|
|
39
|
+
el.setAttribute('aria-live', priority);
|
|
40
|
+
el.setAttribute('aria-atomic', 'true');
|
|
41
|
+
el.setAttribute('role', priority === 'assertive' ? 'alert' : 'status');
|
|
42
|
+
|
|
43
|
+
// Visually hidden but accessible to screen readers
|
|
44
|
+
Object.assign(el.style, {
|
|
45
|
+
position: 'absolute',
|
|
46
|
+
width: '1px',
|
|
47
|
+
height: '1px',
|
|
48
|
+
padding: '0',
|
|
49
|
+
margin: '-1px',
|
|
50
|
+
overflow: 'hidden',
|
|
51
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
52
|
+
whiteSpace: 'nowrap',
|
|
53
|
+
border: '0',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
document.body.appendChild(el);
|
|
57
|
+
liveRegions.set(priority, el);
|
|
58
|
+
|
|
59
|
+
return el;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Announces a message to screen readers via an ARIA live region.
|
|
64
|
+
*
|
|
65
|
+
* The message is injected into a visually-hidden live region element.
|
|
66
|
+
* Screen readers will pick up the change and announce it to the user.
|
|
67
|
+
*
|
|
68
|
+
* @param message - The text message to announce
|
|
69
|
+
* @param priority - The urgency level: `'polite'` (default) or `'assertive'`
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { announceToScreenReader } from '@bquery/bquery/a11y';
|
|
74
|
+
*
|
|
75
|
+
* // Polite announcement (waits for idle)
|
|
76
|
+
* announceToScreenReader('3 search results found');
|
|
77
|
+
*
|
|
78
|
+
* // Assertive announcement (interrupts current speech)
|
|
79
|
+
* announceToScreenReader('Error: Please fix the form', 'assertive');
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export const announceToScreenReader = (
|
|
83
|
+
message: string,
|
|
84
|
+
priority: AnnouncePriority = 'polite'
|
|
85
|
+
): void => {
|
|
86
|
+
if (!message) return;
|
|
87
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
88
|
+
|
|
89
|
+
const region = getOrCreateLiveRegion(priority);
|
|
90
|
+
const pendingTimeout = pendingAnnouncements.get(priority);
|
|
91
|
+
if (pendingTimeout !== undefined) {
|
|
92
|
+
clearTimeout(pendingTimeout);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Clear first, then set after a short timer delay to ensure screen readers
|
|
96
|
+
// detect the change even if the same message is announced twice.
|
|
97
|
+
region.textContent = '';
|
|
98
|
+
|
|
99
|
+
// Use setTimeout to ensure the DOM update triggers a live region change event
|
|
100
|
+
const timeout = setTimeout(() => {
|
|
101
|
+
pendingAnnouncements.delete(priority);
|
|
102
|
+
if (region.isConnected) {
|
|
103
|
+
region.textContent = message;
|
|
104
|
+
}
|
|
105
|
+
}, ANNOUNCEMENT_DELAY_MS);
|
|
106
|
+
|
|
107
|
+
pendingAnnouncements.set(priority, timeout);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Removes all live region elements created by `announceToScreenReader`.
|
|
112
|
+
* Useful for cleanup in tests or when unmounting an application.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { clearAnnouncements } from '@bquery/bquery/a11y';
|
|
117
|
+
*
|
|
118
|
+
* clearAnnouncements();
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export const clearAnnouncements = (): void => {
|
|
122
|
+
for (const timeout of pendingAnnouncements.values()) {
|
|
123
|
+
clearTimeout(timeout);
|
|
124
|
+
}
|
|
125
|
+
pendingAnnouncements.clear();
|
|
126
|
+
|
|
127
|
+
for (const [, el] of liveRegions) {
|
|
128
|
+
el.remove();
|
|
129
|
+
}
|
|
130
|
+
liveRegions.clear();
|
|
131
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development-time accessibility audit utility.
|
|
3
|
+
*
|
|
4
|
+
* Scans DOM elements for common accessibility issues such as missing
|
|
5
|
+
* alt text on images, missing labels on form inputs, empty links/buttons,
|
|
6
|
+
* and incorrect ARIA usage.
|
|
7
|
+
*
|
|
8
|
+
* @module bquery/a11y
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AuditFinding, AuditResult, AuditSeverity } from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a finding object.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
const finding = (
|
|
18
|
+
severity: AuditSeverity,
|
|
19
|
+
message: string,
|
|
20
|
+
element: Element,
|
|
21
|
+
rule: string
|
|
22
|
+
): AuditFinding => ({
|
|
23
|
+
severity,
|
|
24
|
+
message,
|
|
25
|
+
element,
|
|
26
|
+
rule,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks images for missing alt attributes.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
const auditImages = (container: Element): AuditFinding[] => {
|
|
34
|
+
const findings: AuditFinding[] = [];
|
|
35
|
+
const images = container.querySelectorAll('img');
|
|
36
|
+
|
|
37
|
+
for (const img of images) {
|
|
38
|
+
if (!img.hasAttribute('alt')) {
|
|
39
|
+
findings.push(
|
|
40
|
+
finding(
|
|
41
|
+
'error',
|
|
42
|
+
'Image is missing an alt attribute. Add alt="" for decorative images or a descriptive alt text.',
|
|
43
|
+
img,
|
|
44
|
+
'img-alt'
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
} else if (img.getAttribute('alt') === '' && !img.hasAttribute('role')) {
|
|
48
|
+
findings.push(
|
|
49
|
+
finding(
|
|
50
|
+
'info',
|
|
51
|
+
'Image has empty alt text. Consider adding role="presentation" if decorative.',
|
|
52
|
+
img,
|
|
53
|
+
'img-decorative'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return findings;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks form inputs for missing labels.
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
const auditFormInputs = (container: Element): AuditFinding[] => {
|
|
67
|
+
const findings: AuditFinding[] = [];
|
|
68
|
+
const inputs = container.querySelectorAll('input, select, textarea');
|
|
69
|
+
|
|
70
|
+
for (const input of inputs) {
|
|
71
|
+
const type = input.getAttribute('type');
|
|
72
|
+
|
|
73
|
+
// Hidden, submit, and button inputs don't need labels
|
|
74
|
+
if (type === 'hidden' || type === 'submit' || type === 'button' || type === 'reset') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const id = input.getAttribute('id');
|
|
79
|
+
const hasLabel = id ? !!container.querySelector(`label[for="${id}"]`) : false;
|
|
80
|
+
const hasAriaLabel = input.hasAttribute('aria-label') || input.hasAttribute('aria-labelledby');
|
|
81
|
+
const hasTitle = input.hasAttribute('title');
|
|
82
|
+
const isWrappedInLabel = input.closest('label') !== null;
|
|
83
|
+
|
|
84
|
+
if (!hasLabel && !hasAriaLabel && !hasTitle && !isWrappedInLabel) {
|
|
85
|
+
findings.push(
|
|
86
|
+
finding(
|
|
87
|
+
'error',
|
|
88
|
+
`Form input is missing a label. Add a <label for="id">, aria-label, or aria-labelledby attribute.`,
|
|
89
|
+
input,
|
|
90
|
+
'input-label'
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return findings;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks for empty interactive elements (buttons, links).
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
const auditInteractiveElements = (container: Element): AuditFinding[] => {
|
|
104
|
+
const findings: AuditFinding[] = [];
|
|
105
|
+
|
|
106
|
+
// Check buttons
|
|
107
|
+
const buttons = container.querySelectorAll('button');
|
|
108
|
+
for (const btn of buttons) {
|
|
109
|
+
const hasText = (btn.textContent ?? '').trim().length > 0;
|
|
110
|
+
const hasAriaLabel = btn.hasAttribute('aria-label') || btn.hasAttribute('aria-labelledby');
|
|
111
|
+
const hasTitle = btn.hasAttribute('title');
|
|
112
|
+
|
|
113
|
+
if (!hasText && !hasAriaLabel && !hasTitle) {
|
|
114
|
+
findings.push(
|
|
115
|
+
finding(
|
|
116
|
+
'error',
|
|
117
|
+
'Button has no accessible name. Add text content, aria-label, or title.',
|
|
118
|
+
btn,
|
|
119
|
+
'button-name'
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check links
|
|
126
|
+
const links = container.querySelectorAll('a[href]');
|
|
127
|
+
for (const link of links) {
|
|
128
|
+
const hasText = (link.textContent ?? '').trim().length > 0;
|
|
129
|
+
const hasAriaLabel = link.hasAttribute('aria-label') || link.hasAttribute('aria-labelledby');
|
|
130
|
+
const hasTitle = link.hasAttribute('title');
|
|
131
|
+
const hasImage = link.querySelector('img[alt]') !== null;
|
|
132
|
+
|
|
133
|
+
if (!hasText && !hasAriaLabel && !hasTitle && !hasImage) {
|
|
134
|
+
findings.push(
|
|
135
|
+
finding(
|
|
136
|
+
'error',
|
|
137
|
+
'Link has no accessible name. Add text content, aria-label, or title.',
|
|
138
|
+
link,
|
|
139
|
+
'link-name'
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return findings;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks heading hierarchy for skipped levels.
|
|
150
|
+
* @internal
|
|
151
|
+
*/
|
|
152
|
+
const auditHeadings = (container: Element): AuditFinding[] => {
|
|
153
|
+
const findings: AuditFinding[] = [];
|
|
154
|
+
const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
155
|
+
|
|
156
|
+
let previousLevel = 0;
|
|
157
|
+
|
|
158
|
+
for (const heading of headings) {
|
|
159
|
+
const level = parseInt(heading.tagName.charAt(1), 10);
|
|
160
|
+
|
|
161
|
+
if (previousLevel > 0 && level > previousLevel + 1) {
|
|
162
|
+
findings.push(
|
|
163
|
+
finding(
|
|
164
|
+
'warning',
|
|
165
|
+
`Heading level skipped: <${heading.tagName.toLowerCase()}> follows <h${previousLevel}>. Don't skip heading levels.`,
|
|
166
|
+
heading,
|
|
167
|
+
'heading-order'
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if ((heading.textContent ?? '').trim().length === 0) {
|
|
173
|
+
findings.push(finding('warning', 'Heading element is empty.', heading, 'heading-empty'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
previousLevel = level;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return findings;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Checks for valid ARIA attribute usage.
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
const auditAria = (container: Element): AuditFinding[] => {
|
|
187
|
+
const findings: AuditFinding[] = [];
|
|
188
|
+
|
|
189
|
+
// Check aria-labelledby references exist
|
|
190
|
+
const labelled = container.querySelectorAll('[aria-labelledby]');
|
|
191
|
+
for (const el of labelled) {
|
|
192
|
+
const ids = (el.getAttribute('aria-labelledby') ?? '').split(/\s+/);
|
|
193
|
+
for (const id of ids) {
|
|
194
|
+
if (id && !document.getElementById(id)) {
|
|
195
|
+
findings.push(
|
|
196
|
+
finding(
|
|
197
|
+
'error',
|
|
198
|
+
`aria-labelledby references "${id}" which does not exist in the document.`,
|
|
199
|
+
el,
|
|
200
|
+
'aria-labelledby-ref'
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check aria-describedby references exist
|
|
208
|
+
const described = container.querySelectorAll('[aria-describedby]');
|
|
209
|
+
for (const el of described) {
|
|
210
|
+
const ids = (el.getAttribute('aria-describedby') ?? '').split(/\s+/);
|
|
211
|
+
for (const id of ids) {
|
|
212
|
+
if (id && !document.getElementById(id)) {
|
|
213
|
+
findings.push(
|
|
214
|
+
finding(
|
|
215
|
+
'error',
|
|
216
|
+
`aria-describedby references "${id}" which does not exist in the document.`,
|
|
217
|
+
el,
|
|
218
|
+
'aria-describedby-ref'
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return findings;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Checks for sufficient document landmarks.
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
const auditLandmarks = (container: Element): AuditFinding[] => {
|
|
233
|
+
const findings: AuditFinding[] = [];
|
|
234
|
+
|
|
235
|
+
// Only audit the document body or top-level container
|
|
236
|
+
if (container === document.body || container === document.documentElement) {
|
|
237
|
+
const hasMain = !!container.querySelector('main') || !!container.querySelector('[role="main"]');
|
|
238
|
+
|
|
239
|
+
if (!hasMain) {
|
|
240
|
+
findings.push(
|
|
241
|
+
finding(
|
|
242
|
+
'warning',
|
|
243
|
+
'Page is missing a <main> landmark. Add <main> or role="main" to the primary content area.',
|
|
244
|
+
container,
|
|
245
|
+
'landmark-main'
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return findings;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Runs a development-time accessibility audit on a container element.
|
|
256
|
+
*
|
|
257
|
+
* Checks for common accessibility issues including:
|
|
258
|
+
* - Missing alt text on images
|
|
259
|
+
* - Missing labels on form inputs
|
|
260
|
+
* - Empty buttons and links
|
|
261
|
+
* - Heading hierarchy issues
|
|
262
|
+
* - Invalid ARIA references
|
|
263
|
+
* - Missing document landmarks
|
|
264
|
+
*
|
|
265
|
+
* This is intended as a development tool — not a replacement for
|
|
266
|
+
* manual testing or professional accessibility audits.
|
|
267
|
+
*
|
|
268
|
+
* @param container - The element to audit (defaults to `document.body`)
|
|
269
|
+
* @returns An audit result with findings, counts, and pass/fail status
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { auditA11y } from '@bquery/bquery/a11y';
|
|
274
|
+
*
|
|
275
|
+
* const result = auditA11y();
|
|
276
|
+
* if (!result.passed) {
|
|
277
|
+
* console.warn(`Found ${result.errors} accessibility errors:`);
|
|
278
|
+
* for (const f of result.findings) {
|
|
279
|
+
* console.warn(`[${f.severity}] ${f.message}`, f.element);
|
|
280
|
+
* }
|
|
281
|
+
* }
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export const auditA11y = (container?: Element): AuditResult => {
|
|
285
|
+
if (typeof document === 'undefined' || !document.body) {
|
|
286
|
+
return {
|
|
287
|
+
findings: [],
|
|
288
|
+
errors: 0,
|
|
289
|
+
warnings: 0,
|
|
290
|
+
passed: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const target = container ?? document.body;
|
|
295
|
+
|
|
296
|
+
const allFindings: AuditFinding[] = [
|
|
297
|
+
...auditImages(target),
|
|
298
|
+
...auditFormInputs(target),
|
|
299
|
+
...auditInteractiveElements(target),
|
|
300
|
+
...auditHeadings(target),
|
|
301
|
+
...auditAria(target),
|
|
302
|
+
...auditLandmarks(target),
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const errors = allFindings.filter((f) => f.severity === 'error').length;
|
|
306
|
+
const warnings = allFindings.filter((f) => f.severity === 'warning').length;
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
findings: allFindings,
|
|
310
|
+
errors,
|
|
311
|
+
warnings,
|
|
312
|
+
passed: errors === 0,
|
|
313
|
+
};
|
|
314
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility (a11y) utilities module for bQuery.js.
|
|
3
|
+
*
|
|
4
|
+
* Provides essential accessibility helpers for building inclusive
|
|
5
|
+
* web applications: focus trapping, screen reader announcements,
|
|
6
|
+
* keyboard navigation patterns, skip navigation, media preference
|
|
7
|
+
* signals, and development-time auditing.
|
|
8
|
+
*
|
|
9
|
+
* @module bquery/a11y
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import {
|
|
14
|
+
* trapFocus,
|
|
15
|
+
* announceToScreenReader,
|
|
16
|
+
* rovingTabIndex,
|
|
17
|
+
* skipLink,
|
|
18
|
+
* prefersReducedMotion,
|
|
19
|
+
* prefersColorScheme,
|
|
20
|
+
* auditA11y,
|
|
21
|
+
* } from '@bquery/bquery/a11y';
|
|
22
|
+
*
|
|
23
|
+
* // Trap focus in a modal
|
|
24
|
+
* const trap = trapFocus(dialogElement);
|
|
25
|
+
*
|
|
26
|
+
* // Announce changes to screen readers
|
|
27
|
+
* announceToScreenReader('Form submitted successfully');
|
|
28
|
+
*
|
|
29
|
+
* // Arrow key navigation in a toolbar
|
|
30
|
+
* const roving = rovingTabIndex(toolbar, 'button', {
|
|
31
|
+
* orientation: 'horizontal',
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Auto-generate skip navigation
|
|
35
|
+
* const skip = skipLink('#main-content');
|
|
36
|
+
*
|
|
37
|
+
* // Reactive media preferences
|
|
38
|
+
* const reduced = prefersReducedMotion();
|
|
39
|
+
* const scheme = prefersColorScheme();
|
|
40
|
+
*
|
|
41
|
+
* // Development-time audit
|
|
42
|
+
* const result = auditA11y();
|
|
43
|
+
* if (!result.passed) console.warn(result.findings);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export { announceToScreenReader, clearAnnouncements } from './announce';
|
|
48
|
+
export { auditA11y } from './audit';
|
|
49
|
+
export { prefersColorScheme, prefersContrast, prefersReducedMotion } from './media-preferences';
|
|
50
|
+
export { rovingTabIndex } from './roving-tab-index';
|
|
51
|
+
export { skipLink } from './skip-link';
|
|
52
|
+
export { getFocusableElements, releaseFocus, trapFocus } from './trap-focus';
|
|
53
|
+
|
|
54
|
+
export type {
|
|
55
|
+
AnnouncePriority,
|
|
56
|
+
AuditFinding,
|
|
57
|
+
AuditResult,
|
|
58
|
+
AuditSeverity,
|
|
59
|
+
ColorScheme,
|
|
60
|
+
ContrastPreference,
|
|
61
|
+
FocusTrapHandle,
|
|
62
|
+
MediaPreferenceSignal,
|
|
63
|
+
RovingTabIndexHandle,
|
|
64
|
+
RovingTabIndexOptions,
|
|
65
|
+
SkipLinkHandle,
|
|
66
|
+
SkipLinkOptions,
|
|
67
|
+
TrapFocusOptions,
|
|
68
|
+
} from './types';
|