@ponchia/ui 0.6.8 → 0.6.9

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +59 -4
  2. package/README.md +2 -2
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +5 -6
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +100 -60
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +167 -113
  9. package/behaviors/connectors.d.ts.map +1 -1
  10. package/behaviors/connectors.js +39 -23
  11. package/behaviors/forms.d.ts.map +1 -1
  12. package/behaviors/forms.js +211 -207
  13. package/behaviors/glyph.d.ts.map +1 -1
  14. package/behaviors/glyph.js +157 -132
  15. package/behaviors/inert.d.ts +1 -1
  16. package/behaviors/inert.d.ts.map +1 -1
  17. package/behaviors/inert.js +1 -1
  18. package/behaviors/internal.js +2 -2
  19. package/behaviors/modal.js +1 -1
  20. package/behaviors/popover.js +5 -5
  21. package/behaviors/table.d.ts +1 -1
  22. package/behaviors/table.d.ts.map +1 -1
  23. package/behaviors/table.js +7 -8
  24. package/behaviors/tabs.js +2 -2
  25. package/behaviors/toast.js +5 -5
  26. package/classes/index.js +48 -34
  27. package/connectors/index.d.ts +2 -2
  28. package/connectors/index.d.ts.map +1 -1
  29. package/connectors/index.js +7 -10
  30. package/css/app.css +3 -4
  31. package/css/base.css +1 -1
  32. package/css/content.css +3 -3
  33. package/css/disclosure.css +3 -3
  34. package/css/dots.css +4 -4
  35. package/css/feedback.css +6 -7
  36. package/css/forms.css +9 -12
  37. package/css/legend.css +1 -1
  38. package/css/marks.css +1 -1
  39. package/css/motion.css +6 -6
  40. package/css/overlay.css +5 -7
  41. package/css/primitives.css +14 -16
  42. package/css/sidenote.css +2 -2
  43. package/css/table.css +2 -2
  44. package/docs/annotations.md +9 -0
  45. package/docs/architecture.md +28 -0
  46. package/docs/interop/react-flow.md +89 -0
  47. package/docs/package-contract.md +2 -0
  48. package/docs/reporting.md +8 -8
  49. package/docs/stability.md +67 -7
  50. package/glyphs/glyphs.js +43 -33
  51. package/llms.txt +10 -4
  52. package/package.json +5 -2
  53. package/schemas/report-claims.v1.schema.json +1 -1
  54. package/tokens/index.js +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,61 @@
5
5
  |> `^0` / `*` wildcard does **not** protect you. See README → Versioning, and
6
6
  |> the deprecation policy in CONTRIBUTING.md.
7
7
 
8
+ ## 0.6.9 — 2026-06-20
9
+
10
+ ### Changed
11
+
12
+ - **Product doctrine and 1.0 readiness.** `CONTRIBUTING.md`, `ROADMAP.md`,
13
+ `docs/architecture.md`, and `docs/stability.md` now codify the
14
+ core-vs-opt-in surface boundary, the refusal list, registry-first gate
15
+ maintenance, packed-tarball proof, a gate-backed 1.0 readiness ledger, and
16
+ release evidence policy for public-safe downstream proof. The stability
17
+ matrix now includes explicit rows for every exported opt-in CSS leaf plus
18
+ the public machine-readable/theme subpath families.
19
+ - **Registry-backed gate ownership.** `check:report`,
20
+ `check:component-matrix`, `check:exports`, and `check:consumer-surface` now
21
+ share internal registries for the reporting toolbox, exported CSS leaves, and
22
+ optional framework peers instead of carrying separate hand-maintained lists.
23
+ The matrix gates also share one proof-owner helper for cached file reads,
24
+ owner-file existence, word matching, and required text evidence.
25
+ - **Native complexity budget.** `check:complexity` is now part of the
26
+ aggregate `npm run check` chain. It uses the existing TypeScript parser to
27
+ keep function-level cyclomatic complexity at 12 or lower and function size
28
+ under budget without carrying per-function exception baselines.
29
+ - **Gate-backed annotation package boundary.** The annotation docs now make the
30
+ split with `@ponchia/annotations` explicit: `@ponchia/ui/annotations` remains
31
+ the dependency-free Bronto static-helper compatibility surface, while richer
32
+ placement, renderer, editing, and adapter work belongs in the sibling
33
+ annotation package. `check:public-metadata` now guards that doctrine,
34
+ `check:exports` rejects packed code or declaration references to the sibling
35
+ package, and no runtime or public type dependency is added to `@ponchia/ui`.
36
+ - **Public source hygiene guard.** `check:public-hygiene` now rejects internal
37
+ audit-ticket markers across repository source files, and still scans the
38
+ packed public text files for private terms, local paths, and secret-looking
39
+ assignments. `check:doc-links` also rejects executable URL schemes in public
40
+ authoring docs. The pass also brought
41
+ `behaviors/inert.js` under the generated declaration emit inputs so
42
+ `check:dts-emit` covers its public `.d.ts` surface.
43
+
44
+ ### Verified
45
+
46
+ - **Release evidence.** `npm run check` passed for `0.6.9`, including
47
+ `check:pack`, `check:consumer-surface`, `check:consumer-types`,
48
+ `check:examples`, `check:publint`, and `check:attw`. No default-bundle budget
49
+ movement; this release is gate/docs/declaration hardening rather than a
50
+ runtime surface expansion.
51
+ - **Downstream proof.** The packed current tarball installed into a disposable
52
+ copy of a real React/Vite app consumer that imports `@ponchia/ui/classes`,
53
+ `@ponchia/ui/behaviors`, `@ponchia/ui/tokens/resolved.json`, and
54
+ `@ponchia/ui/vega`; its `typecheck` and production `build` both passed.
55
+
56
+ ### Fixed
57
+
58
+ - **Report sidecar hash validation.** `report-claims.v1` now requires
59
+ `contentHash` values to use exact SHA digest lengths (`sha256`, `sha384`, or
60
+ `sha512`) instead of accepting any hex payload after a supported algorithm
61
+ prefix.
62
+
8
63
  ## 0.6.8 — 2026-06-16
9
64
 
10
65
  Patch release for the deep UI-framework audit: broader browser/package gates,
@@ -462,7 +517,7 @@ and D2. The data-viz **palette** (`--chart-*`, `tokens/charts.json`) and the
462
517
 
463
518
  ### Fixed
464
519
 
465
- - **Published-type drift (code-quality audit).** `ui.meter({ tone: 'info' })` and
520
+ - **Published-type drift.** `ui.meter({ tone: 'info' })` and
466
521
  `ui.bracketNote({ tone: 'success' })` emit real classes at runtime, but the
467
522
  generated `.d.ts` tone unions (hand-mirrored in `gen-dts.mjs`) omitted them, so
468
523
  a TS consumer got a spurious type error for a value that renders. The unions
@@ -634,16 +689,16 @@ and D2. The data-viz **palette** (`--chart-*`, `tokens/charts.json`) and the
634
689
  token source as a read-only export for foreign renderers (in-DOM ink is
635
690
  `--button-text`). `contrast.md` now prints APCA `Lc` to one decimal so an
636
691
  advisory shortfall (e.g. `Lc 44.9`) no longer rounds to a passing-looking `45`.
637
- - Raw bundle budget 81 → 82 kB for the component-audit accessibility/state
692
+ - Raw bundle budget 81 → 82 kB for the accessibility/state
638
693
  blocks (gzip held ~14.1 kB — the additions are repetitive media-query and
639
694
  `:has()`/`:not()` rules that compress well).
640
- - **Code-quality audit (16-agent) — two new gates + targeted dedup, no churn.**
695
+ - **Code-health pass — two new gates + targeted dedup, no churn.**
641
696
  A code-health pass (complexity / duplication / AI-slop / missing-best-practice)
642
697
  that deliberately left working, gate-protected code alone. Added:
643
698
  `check:recipe-types` (factory↔`.d.ts` option parity, above) and `check:chain`
644
699
  (every `check:*` script is wired into the aggregate `check` chain — closes the
645
700
  silent-coverage-drop class; it would have caught a forgotten gate). Reconciled
646
- a latent bug — `clamp()` had silently diverged between `connectors` and
701
+ a latent bug — `clamp()` had drifted between `connectors` and
647
702
  `annotations`; the two now share one scalar/geometry kernel (the guarded form).
648
703
  Dedup that removed real duplication: a shared `collectHosts()` /
649
704
  `scrollIntoViewSafe()` / `wrapIndex()` in `behaviors/internal.js` (~9 behaviors),
package/README.md CHANGED
@@ -67,7 +67,7 @@ npm i @ponchia/ui
67
67
  Or drop it in with no build step, straight from a CDN:
68
68
 
69
69
  ```html
70
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/bronto.css">
70
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/bronto.css">
71
71
  ```
72
72
 
73
73
  ## Quick start
@@ -140,7 +140,7 @@ Arrows, chevrons, check/close/plus/minus, search/menu/gear, info/warning/bell/lo
140
140
  - **Disclosure & nav** — tabs, accordion, segmented, breadcrumb, pagination, `ui-steps`, `ui-timeline`, `ui-pagehead`, `ui-kbd`.
141
141
  - **Shells** — a service/app shell (`ui-app-*`) and a content/marketing site shell (`ui-site*`, `ui-container`).
142
142
  - **Prose** — `.ui-prose` styles raw, unclassed semantic HTML (Markdown / CMS / LLM output) with zero classes.
143
- - **Analytical & communication primitives** _(opt-in)_ — `@ponchia/ui/css/analytical.css`: **figure** stages, SVG **annotations** (subject/connector/note), standalone **legends**/data-keys, text/evidence **marks**, leader-line **connectors** (+ a pure `@ponchia/ui/connectors` geometry kernel), a guided-focus **spotlight**, a **crosshair**/readout, a cross-cutting **selection** vocabulary, and CSS Custom Highlight API **highlights**. Each owns its visual grammar + pure geometry and refuses scales/state/hit-testing — figures that explain themselves, not a chart engine. Plus standalone **`source`/provenance** (trust), **interval**, **clamp**, and **lifecycle `state`** leaves.
143
+ - **Analytical & communication primitives** _(opt-in)_ — `@ponchia/ui/css/analytical.css`: **figure** stages, SVG **annotations** (subject/connector/note), standalone **legends**/data-keys, text/evidence **marks**, leader-line **connectors** (+ a pure `@ponchia/ui/connectors` geometry kernel), a guided-focus **spotlight**, a **crosshair**/readout, a cross-cutting **selection** vocabulary, and CSS Custom Highlight API **highlights**. Each owns its visual grammar + pure geometry and refuses scales/state/hit-testing — figures that explain themselves, not a chart engine. The richer annotation engine lives in `@ponchia/annotations`; this package keeps only the dependency-free Bronto helper surface. Plus standalone **`source`/provenance** (trust), **interval**, **clamp**, and **lifecycle `state`** leaves.
144
144
  - **Reports** _(opt-in)_ — `@ponchia/ui/css/report-kit.css` for a complete static/PDF report vocabulary, or `@ponchia/ui/css/report.css` plus only the leaves a narrow report uses. Covers, decisions, claims, sections, severity-labelled findings, evidence packets, evidence ledgers, action registers, source-card bindings, `ui-figure` composition, intervals, bounded excerpts, chart wrappers and print utilities.
145
145
  - **Motion & dots** — the dot-matrix motif kit: dot grid, status dots, dot loaders, the orbital spinner, matrix reveal — all reduced-motion aware.
146
146
  - **Glyphs** — `@ponchia/ui/glyphs`, a 71-glyph dot-matrix icon set on the `.ui-dotmatrix` primitive (display marks + crisp `solid` inline icons + one-node `.ui-icon` mask rendering).
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"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"}
@@ -209,8 +209,7 @@ import {
209
209
  arrowHead,
210
210
  dotMark,
211
211
  angleBetween,
212
- // Shared scalar/geometry kernel single source of truth (was copy-pasted,
213
- // and the local clamp had silently diverged from the connectors one).
212
+ // Shared scalar/geometry kernel for annotation and connector path output.
214
213
  roundNumber,
215
214
  finite,
216
215
  dimension,
@@ -220,8 +219,8 @@ import {
220
219
  rectPath,
221
220
  } from '../connectors/index.js';
222
221
 
223
- // A circle subject is just a filled dot at (x, y) delegate to the kernel's
224
- // dotMark so the arc geometry/precision can't diverge. (code-quality audit Q5.)
222
+ // A circle subject is just a filled dot at (x, y); delegate to the connector
223
+ // kernel so arc geometry and numeric precision stay aligned.
225
224
  function circlePathAt(x, y, radius) {
226
225
  return dotMark({ x, y }, radius);
227
226
  }
@@ -670,8 +669,8 @@ export function connectorCurve(opts = {}) {
670
669
  return curvePath(start, end, { curvature: 0.35 });
671
670
  }
672
671
 
673
- // subject.type → its path builder. A flat dispatch table (replaces an 11-arm
674
- // if/else) keyed by the SubjectType union; an unknown type throws below. (Q10.)
672
+ // subject.type → its path builder. A flat dispatch table keyed by the
673
+ // SubjectType union; an unknown type throws below.
675
674
  const SUBJECT_BUILDERS = {
676
675
  circle: circleSubjectPath,
677
676
  rect: rectSubjectPath,
@@ -1 +1 @@
1
- {"version":3,"file":"carousel.d.ts","sourceRoot":"","sources":["carousel.js"],"names":[],"mappings":"AA6CA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA8L3C"}
1
+ {"version":3,"file":"carousel.d.ts","sourceRoot":"","sources":["carousel.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAsJ3C"}
@@ -43,6 +43,86 @@ const renderedStatusIndex = (status) => {
43
43
  return Number.isInteger(value) ? value - 1 : -1;
44
44
  };
45
45
 
46
+ const snapshotCarouselState = ({ viewport, slides, status, prevBtn, nextBtn, thumbs }) => ({
47
+ viewport: snapshotNode(viewport),
48
+ slides: slides.map((slide) => snapshotNode(slide)),
49
+ status: snapshotNode(status, { html: true }),
50
+ controls: [prevBtn, nextBtn, ...thumbs].filter(Boolean).map((control) => snapshotNode(control)),
51
+ });
52
+
53
+ function restoreCarouselState(state) {
54
+ restoreNode(state.viewport);
55
+ state.slides.forEach(restoreNode);
56
+ restoreNode(state.status);
57
+ state.controls.forEach(restoreNode);
58
+ }
59
+
60
+ function setDefaultButtonType(button) {
61
+ if (button?.tagName === 'BUTTON' && !button.hasAttribute('type')) button.type = 'button';
62
+ }
63
+
64
+ function applyCarouselA11y({ box, viewport, slides, status, prevBtn, nextBtn, thumbs, n }) {
65
+ // ARIA scaffolding — pragmatic carousel semantics (not the full APG
66
+ // tablist), the same restraint initMenu takes.
67
+ viewport.setAttribute('role', 'group');
68
+ viewport.setAttribute('aria-roledescription', 'carousel');
69
+ if (!viewport.hasAttribute('aria-label')) {
70
+ viewport.setAttribute(
71
+ 'aria-label',
72
+ box.getAttribute('data-bronto-carousel-label') || 'Carousel',
73
+ );
74
+ }
75
+ if (!viewport.hasAttribute('tabindex')) viewport.tabIndex = 0;
76
+ slides.forEach((slide, i) => {
77
+ slide.setAttribute('role', 'group');
78
+ slide.setAttribute('aria-roledescription', 'slide');
79
+ if (!slide.hasAttribute('aria-label')) slide.setAttribute('aria-label', `${i + 1} of ${n}`);
80
+ });
81
+ if (status) status.setAttribute('aria-live', 'polite');
82
+ [prevBtn, nextBtn, ...thumbs].forEach(setDefaultButtonType);
83
+ if (prevBtn && !prevBtn.hasAttribute('aria-label'))
84
+ prevBtn.setAttribute('aria-label', 'Previous');
85
+ if (nextBtn && !nextBtn.hasAttribute('aria-label')) nextBtn.setAttribute('aria-label', 'Next');
86
+ }
87
+
88
+ function bindCarouselLifecycle({
89
+ box,
90
+ viewport,
91
+ slides,
92
+ status,
93
+ prevBtn,
94
+ nextBtn,
95
+ thumbs,
96
+ n,
97
+ render,
98
+ onKey,
99
+ onClick,
100
+ io,
101
+ holdProgrammatic,
102
+ clearProgrammaticTimer,
103
+ }) {
104
+ const state = snapshotCarouselState({ viewport, slides, status, prevBtn, nextBtn, thumbs });
105
+ applyCarouselA11y({ box, viewport, slides, status, prevBtn, nextBtn, thumbs, n });
106
+ render();
107
+ viewport.addEventListener('keydown', onKey);
108
+ box.addEventListener('click', onClick);
109
+ // Observe inside the add callback so observe/disconnect pair with the
110
+ // binding lifecycle: a re-init tears down the prior binding (which
111
+ // disconnects the old observer) before this starts, so two observers
112
+ // never watch the same slides — even for one tick.
113
+ if (io) {
114
+ holdProgrammatic();
115
+ slides.forEach((slide) => io.observe(slide));
116
+ }
117
+ return () => {
118
+ viewport.removeEventListener('keydown', onKey);
119
+ box.removeEventListener('click', onClick);
120
+ io?.disconnect();
121
+ clearProgrammaticTimer();
122
+ restoreCarouselState(state);
123
+ };
124
+ }
125
+
46
126
  /**
47
127
  * Image carousel / gallery, built on CSS scroll-snap so touch + trackpad
48
128
  * swipe (and momentum) are the browser's, not hand-rolled. This wires the
@@ -193,66 +273,26 @@ export function initCarousel({ root } = {}) {
193
273
  );
194
274
  }
195
275
 
196
- const bound = bindOnce(box, 'carousel', () => {
197
- const state = {
198
- viewport: snapshotNode(viewport),
199
- slides: slides.map((slide) => snapshotNode(slide)),
200
- status: snapshotNode(status, { html: true }),
201
- controls: [prevBtn, nextBtn, ...thumbs]
202
- .filter(Boolean)
203
- .map((control) => snapshotNode(control)),
204
- };
205
-
206
- // ARIA scaffolding — pragmatic carousel semantics (not the full APG
207
- // tablist), the same restraint initMenu takes.
208
- viewport.setAttribute('role', 'group');
209
- viewport.setAttribute('aria-roledescription', 'carousel');
210
- if (!viewport.hasAttribute('aria-label'))
211
- viewport.setAttribute(
212
- 'aria-label',
213
- box.getAttribute('data-bronto-carousel-label') || 'Carousel',
214
- );
215
- if (!viewport.hasAttribute('tabindex')) viewport.tabIndex = 0;
216
- slides.forEach((s, i) => {
217
- s.setAttribute('role', 'group');
218
- s.setAttribute('aria-roledescription', 'slide');
219
- if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', `${i + 1} of ${n}`);
220
- });
221
- if (status) status.setAttribute('aria-live', 'polite');
222
- for (const b of [prevBtn, nextBtn]) {
223
- if (!b) continue;
224
- if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
225
- }
226
- for (const b of thumbs) {
227
- if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
228
- }
229
- if (prevBtn && !prevBtn.hasAttribute('aria-label'))
230
- prevBtn.setAttribute('aria-label', 'Previous');
231
- if (nextBtn && !nextBtn.hasAttribute('aria-label'))
232
- nextBtn.setAttribute('aria-label', 'Next');
233
-
234
- render();
235
- viewport.addEventListener('keydown', onKey);
236
- box.addEventListener('click', onClick);
237
- // Observe inside the add callback so observe/disconnect pair with the
238
- // binding lifecycle: a re-init tears down the prior binding (which
239
- // disconnects the old observer) before this starts, so two observers
240
- // never watch the same slides — even for one tick.
241
- if (io) {
242
- holdProgrammatic();
243
- slides.forEach((s) => io.observe(s));
244
- }
245
- return () => {
246
- viewport.removeEventListener('keydown', onKey);
247
- box.removeEventListener('click', onClick);
248
- io?.disconnect();
249
- if (progTimer) clearTimeout(progTimer);
250
- restoreNode(state.viewport);
251
- state.slides.forEach(restoreNode);
252
- restoreNode(state.status);
253
- state.controls.forEach(restoreNode);
254
- };
255
- });
276
+ const bound = bindOnce(box, 'carousel', () =>
277
+ bindCarouselLifecycle({
278
+ box,
279
+ viewport,
280
+ slides,
281
+ status,
282
+ prevBtn,
283
+ nextBtn,
284
+ thumbs,
285
+ n,
286
+ render,
287
+ onKey,
288
+ onClick,
289
+ io,
290
+ holdProgrammatic,
291
+ clearProgrammaticTimer: () => {
292
+ if (progTimer) clearTimeout(progTimer);
293
+ },
294
+ }),
295
+ );
256
296
  cleanups.push(bound);
257
297
  }
258
298
 
@@ -1 +1 @@
1
- {"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["combobox.js"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA8S3C"}
1
+ {"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["combobox.js"],"names":[],"mappings":"AAmIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6O3C"}
@@ -10,6 +10,125 @@ import {
10
10
  closestSafe,
11
11
  } from './internal.js';
12
12
 
13
+ const COMBOBOX_OPTION_SELECTOR = '[role="option"], .ui-combobox__option';
14
+
15
+ const snapshotAttrs = (el, names) => {
16
+ const out = {};
17
+ for (const name of names) {
18
+ out[name] = {
19
+ had: el.hasAttribute(name),
20
+ value: el.getAttribute(name),
21
+ };
22
+ }
23
+ return out;
24
+ };
25
+
26
+ const restoreAttrs = (el, attrs) => {
27
+ for (const [name, attr] of Object.entries(attrs)) {
28
+ if (attr.had) el.setAttribute(name, attr.value);
29
+ else el.removeAttribute(name);
30
+ }
31
+ };
32
+
33
+ const inputLabel = (input) =>
34
+ input.getAttribute('aria-label') || input.labels?.[0]?.textContent?.trim();
35
+
36
+ const inputHasAccessibleName = (input) =>
37
+ input.hasAttribute('aria-label') ||
38
+ input.hasAttribute('aria-labelledby') ||
39
+ !!input.labels?.length ||
40
+ input.hasAttribute('title');
41
+
42
+ function mirrorListboxLabel(input, list) {
43
+ if (list.hasAttribute('aria-label') || list.hasAttribute('aria-labelledby')) return;
44
+ const name = inputLabel(input);
45
+ if (name) list.setAttribute('aria-label', name);
46
+ }
47
+
48
+ function warnNamelessCombobox(input) {
49
+ if (inputHasAccessibleName(input) || typeof console === 'undefined') return;
50
+ console.warn(
51
+ '[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
52
+ );
53
+ }
54
+
55
+ function prepareEmptyState(empty) {
56
+ if (!empty) return;
57
+ empty.hidden = true;
58
+ if (!empty.hasAttribute('role')) empty.setAttribute('role', 'status');
59
+ }
60
+
61
+ function liveOptionObserver(box, list, relist) {
62
+ if (!box.hasAttribute('data-bronto-combobox-live')) return null;
63
+ if (typeof MutationObserver !== 'function') return null;
64
+ const observer = new MutationObserver(relist);
65
+ observer.observe(list, { childList: true, subtree: true });
66
+ return observer;
67
+ }
68
+
69
+ function bindComboboxLifecycle({
70
+ box,
71
+ input,
72
+ list,
73
+ empty,
74
+ rememberState,
75
+ restoreState,
76
+ assignListId,
77
+ syncOptions,
78
+ close,
79
+ relist,
80
+ onInput,
81
+ onKey,
82
+ onOptionClick,
83
+ onDocClick,
84
+ resetActive,
85
+ }) {
86
+ const state = rememberState();
87
+ const listId = assignListId();
88
+ syncOptions();
89
+ list.setAttribute('role', 'listbox');
90
+ // Give the listbox its own accessible name (a bare role=listbox is unnamed
91
+ // to a screen reader) by mirroring the input's real name.
92
+ // The placeholder is deliberately NOT in this chain: the input warning below
93
+ // already rejects a placeholder as an inadequate name, so papering the
94
+ // listbox over with it would contradict that. If there is no real name, the
95
+ // listbox stays unnamed and the warning is the signal.
96
+ mirrorListboxLabel(input, list);
97
+ // A `role="combobox"` with no accessible name is a silent AT failure. A
98
+ // placeholder is not a robust name (it can vanish and is ignored by some
99
+ // AT), so warn unless there is a real label/aria-label/aria-labelledby/title
100
+ // We cannot invent a good name, hence a dev-time warning, not a guess.
101
+ warnNamelessCombobox(input);
102
+ input.setAttribute('role', 'combobox');
103
+ input.setAttribute('aria-controls', listId);
104
+ input.setAttribute('aria-autocomplete', 'list');
105
+ input.setAttribute('aria-expanded', 'false');
106
+ input.setAttribute('autocomplete', 'off');
107
+ // Hide the empty-state at rest: it must only appear once a filter yields no
108
+ // matches, never on an idle combobox. Without this an author who omits
109
+ // `hidden` on `.ui-combobox__empty` ships a box that reads "No matches"
110
+ // before the user has typed anything. Make it a status live region so its
111
+ // appearance is announced.
112
+ prepareEmptyState(empty);
113
+ close();
114
+ input.addEventListener('input', onInput);
115
+ input.addEventListener('keydown', onKey);
116
+ list.addEventListener('click', onOptionClick);
117
+ document.addEventListener('click', onDocClick);
118
+ // Opt-in: keep options in sync with a list mutated after init (async /
119
+ // remote results). Off by default so the common static case stays free.
120
+ const observer = liveOptionObserver(box, list, relist);
121
+ return () => {
122
+ observer?.disconnect();
123
+ input.removeEventListener('input', onInput);
124
+ input.removeEventListener('keydown', onKey);
125
+ list.removeEventListener('click', onOptionClick);
126
+ document.removeEventListener('click', onDocClick);
127
+ restoreState(state);
128
+ resetActive();
129
+ };
130
+ }
131
+
13
132
  /**
14
133
  * Editable combobox with a filtered listbox popup, implementing the
15
134
  * WAI-ARIA APG combobox pattern (the widget the framework most lacked
@@ -55,24 +174,6 @@ export function initCombobox({ root } = {}) {
55
174
  const boxes = collectHosts(host, '[data-bronto-combobox]');
56
175
  const cleanups = [];
57
176
 
58
- const snapshotAttrs = (el, names) => {
59
- const out = {};
60
- for (const name of names) {
61
- out[name] = {
62
- had: el.hasAttribute(name),
63
- value: el.getAttribute(name),
64
- };
65
- }
66
- return out;
67
- };
68
-
69
- const restoreAttrs = (el, attrs) => {
70
- for (const [name, attr] of Object.entries(attrs)) {
71
- if (attr.had) el.setAttribute(name, attr.value);
72
- else el.removeAttribute(name);
73
- }
74
- };
75
-
76
177
  for (const box of boxes) {
77
178
  const input = box.querySelector('[role="combobox"], .ui-combobox__input');
78
179
  const list = box.querySelector('[role="listbox"], .ui-combobox__list');
@@ -134,7 +235,7 @@ export function initCombobox({ root } = {}) {
134
235
  // `filter`, `move`, etc. close over this binding, so reassigning it is enough.
135
236
  let options = [];
136
237
  const syncOptions = () => {
137
- options = [...list.querySelectorAll('[role="option"], .ui-combobox__option')];
238
+ options = [...list.querySelectorAll(COMBOBOX_OPTION_SELECTOR)];
138
239
  options.forEach((o, i) => {
139
240
  rememberOptionState(o);
140
241
  if (!o.id) o.id = `${listId}-opt-${i}`;
@@ -190,7 +291,6 @@ export function initCombobox({ root } = {}) {
190
291
  // Show the human LABEL in the input; emit the `data-value` CODE in the
191
292
  // event. The natural pattern is code in `data-value`, label in the text —
192
293
  // putting the code in the visible input silently shows the user a raw code.
193
- // (component audit C10.)
194
294
  const label = opt.textContent.trim();
195
295
  const value = opt.dataset.value ?? label;
196
296
  input.value = label;
@@ -243,107 +343,61 @@ export function initCombobox({ root } = {}) {
243
343
 
244
344
  const onInput = () => filter();
245
345
  const onKey = (e) => {
246
- switch (e.key) {
247
- case 'ArrowDown':
248
- e.preventDefault();
249
- list.hidden ? filter() : move(1);
250
- break;
251
- case 'ArrowUp':
252
- e.preventDefault();
253
- move(-1);
254
- break;
255
- case 'Home':
256
- if (activateEdge('first')) e.preventDefault();
257
- break;
258
- case 'End':
259
- if (activateEdge('last')) e.preventDefault();
260
- break;
261
- case 'Enter':
262
- if (selectActive()) e.preventDefault();
263
- break;
264
- case 'Escape':
265
- if (closeIfOpen()) e.preventDefault();
266
- break;
267
- case 'Tab':
268
- close();
269
- break;
270
- default:
271
- break;
272
- }
346
+ const handled = keyHandlers[e.key]?.();
347
+ if (handled) e.preventDefault();
348
+ };
349
+ const keyHandlers = {
350
+ ArrowDown: () => {
351
+ if (list.hidden) filter();
352
+ else move(1);
353
+ return true;
354
+ },
355
+ ArrowUp: () => {
356
+ move(-1);
357
+ return true;
358
+ },
359
+ Home: () => activateEdge('first'),
360
+ End: () => activateEdge('last'),
361
+ Enter: () => selectActive(),
362
+ Escape: () => closeIfOpen(),
363
+ Tab: () => {
364
+ close();
365
+ return false;
366
+ },
273
367
  };
274
368
  const onOptionClick = (e) => {
275
- const opt = closestSafe(e.target, '[role="option"], .ui-combobox__option');
369
+ const opt = closestSafe(e.target, COMBOBOX_OPTION_SELECTOR);
276
370
  if (opt) select(opt);
277
371
  };
278
372
  const onDocClick = (e) => {
279
373
  if (!box.contains(e.target)) close();
280
374
  };
281
-
282
- const bound = bindOnce(box, 'combobox', () => {
283
- const state = rememberState();
375
+ const assignListId = () => {
284
376
  listId = list.id || (list.id = `bronto-cb-list-${nextFieldUid()}`);
285
- syncOptions();
286
- list.setAttribute('role', 'listbox');
287
- // Give the listbox its own accessible name (a bare role=listbox is unnamed
288
- // to a screen reader) by mirroring the input's REAL name. (a11y review C30.)
289
- // The placeholder is deliberately NOT in this chain: the input warning below
290
- // already rejects a placeholder as an inadequate name, so papering the
291
- // listbox over with it would contradict that — if there's no real name the
292
- // listbox stays unnamed and the warning is the signal. (component audit C28.)
293
- if (!list.hasAttribute('aria-label') && !list.hasAttribute('aria-labelledby')) {
294
- const name = input.getAttribute('aria-label') || input.labels?.[0]?.textContent?.trim();
295
- if (name) list.setAttribute('aria-label', name);
296
- }
297
- // A `role="combobox"` with no accessible name is a silent AT failure. A
298
- // placeholder is not a robust name (it can vanish and is ignored by some
299
- // AT), so warn unless there is a real label/aria-label/aria-labelledby/title
300
- // (C7). We can't invent a good name, hence a dev-time warning, not a guess.
301
- const inputNamed =
302
- input.hasAttribute('aria-label') ||
303
- input.hasAttribute('aria-labelledby') ||
304
- !!input.labels?.length ||
305
- input.hasAttribute('title');
306
- if (!inputNamed && typeof console !== 'undefined') {
307
- console.warn(
308
- '[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
309
- );
310
- }
311
- input.setAttribute('role', 'combobox');
312
- input.setAttribute('aria-controls', listId);
313
- input.setAttribute('aria-autocomplete', 'list');
314
- input.setAttribute('aria-expanded', 'false');
315
- input.setAttribute('autocomplete', 'off');
316
- // Hide the empty-state at rest: it must only appear once a filter yields no
317
- // matches, never on an idle combobox. Without this an author who omits
318
- // `hidden` on `.ui-combobox__empty` ships a box that reads "No matches"
319
- // before the user has typed anything. (component audit C10.) Make it a
320
- // status live region so its appearance is announced. (component audit C38.)
321
- if (empty) {
322
- empty.hidden = true;
323
- if (!empty.hasAttribute('role')) empty.setAttribute('role', 'status');
324
- }
325
- close();
326
- input.addEventListener('input', onInput);
327
- input.addEventListener('keydown', onKey);
328
- list.addEventListener('click', onOptionClick);
329
- document.addEventListener('click', onDocClick);
330
- // Opt-in: keep options in sync with a list mutated after init (async /
331
- // remote results). Off by default so the common static case stays free.
332
- const observer =
333
- box.hasAttribute('data-bronto-combobox-live') && typeof MutationObserver === 'function'
334
- ? new MutationObserver(relist)
335
- : null;
336
- observer?.observe(list, { childList: true, subtree: true });
337
- return () => {
338
- observer?.disconnect();
339
- input.removeEventListener('input', onInput);
340
- input.removeEventListener('keydown', onKey);
341
- list.removeEventListener('click', onOptionClick);
342
- document.removeEventListener('click', onDocClick);
343
- restoreState(state);
344
- active = -1;
345
- };
346
- });
377
+ return listId;
378
+ };
379
+
380
+ const bound = bindOnce(box, 'combobox', () =>
381
+ bindComboboxLifecycle({
382
+ box,
383
+ input,
384
+ list,
385
+ empty,
386
+ rememberState,
387
+ restoreState,
388
+ assignListId,
389
+ syncOptions,
390
+ close,
391
+ relist,
392
+ onInput,
393
+ onKey,
394
+ onOptionClick,
395
+ onDocClick,
396
+ resetActive: () => {
397
+ active = -1;
398
+ },
399
+ }),
400
+ );
347
401
  cleanups.push(bound);
348
402
  }
349
403