@ponchia/ui 0.6.7 → 0.6.8
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/CHANGELOG.md +70 -0
- package/README.md +3 -3
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +21 -3
- package/behaviors/carousel.d.ts.map +1 -1
- package/behaviors/carousel.js +91 -35
- package/behaviors/combobox.d.ts.map +1 -1
- package/behaviors/combobox.js +117 -43
- package/behaviors/command.d.ts.map +1 -1
- package/behaviors/command.js +74 -14
- package/behaviors/connectors.d.ts.map +1 -1
- package/behaviors/connectors.js +92 -9
- package/behaviors/crosshair.d.ts.map +1 -1
- package/behaviors/crosshair.js +47 -1
- package/behaviors/dialog.d.ts.map +1 -1
- package/behaviors/dialog.js +24 -9
- package/behaviors/disclosure.d.ts.map +1 -1
- package/behaviors/disclosure.js +33 -3
- package/behaviors/dismissible.d.ts.map +1 -1
- package/behaviors/dismissible.js +3 -2
- package/behaviors/forms.d.ts.map +1 -1
- package/behaviors/forms.js +67 -0
- package/behaviors/glyph.d.ts.map +1 -1
- package/behaviors/glyph.js +17 -2
- package/behaviors/inert.js +3 -2
- package/behaviors/internal.d.ts.map +1 -1
- package/behaviors/internal.js +2 -1
- package/behaviors/legend.d.ts +0 -5
- package/behaviors/legend.d.ts.map +1 -1
- package/behaviors/legend.js +45 -13
- package/behaviors/menu.d.ts.map +1 -1
- package/behaviors/menu.js +13 -8
- package/behaviors/modal.d.ts.map +1 -1
- package/behaviors/modal.js +77 -19
- package/behaviors/popover.d.ts +4 -3
- package/behaviors/popover.d.ts.map +1 -1
- package/behaviors/popover.js +89 -9
- package/behaviors/sources.d.ts.map +1 -1
- package/behaviors/sources.js +14 -2
- package/behaviors/splitter.d.ts.map +1 -1
- package/behaviors/splitter.js +149 -110
- package/behaviors/spotlight.d.ts.map +1 -1
- package/behaviors/spotlight.js +28 -2
- package/behaviors/table.d.ts.map +1 -1
- package/behaviors/table.js +103 -11
- package/behaviors/tabs.d.ts.map +1 -1
- package/behaviors/tabs.js +82 -18
- package/behaviors/theme.d.ts.map +1 -1
- package/behaviors/theme.js +25 -5
- package/classes/index.d.ts +15 -2
- package/classes/index.js +0 -1
- package/connectors/index.d.ts +39 -6
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +67 -9
- package/css/annotations.css +12 -0
- package/css/crosshair.css +27 -2
- package/css/feedback.css +2 -30
- package/css/navigation.css +12 -0
- package/css/tokens.css +16 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/annotations.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/navigation.css +1 -1
- package/dist/css/report-kit.css +1 -1
- package/dist/css/tokens.css +1 -1
- package/docs/adr/0001-color-system.md +3 -2
- package/docs/annotations.md +12 -1
- package/docs/architecture.md +46 -13
- package/docs/command.md +4 -1
- package/docs/connectors.md +16 -0
- package/docs/crosshair.md +1 -1
- package/docs/dots.md +4 -1
- package/docs/glyphs.md +11 -0
- package/docs/migrations/0.2-to-0.3.md +1 -1
- package/docs/package-contract.md +5 -5
- package/docs/reporting.md +23 -12
- package/docs/stability.md +18 -2
- package/docs/theming.md +2 -2
- package/docs/usage.md +16 -2
- package/docs/vega.md +4 -4
- package/llms.txt +10 -5
- package/package.json +20 -4
- package/svelte/index.d.ts +71 -45
- package/svelte/index.d.ts.map +1 -1
- package/svelte/index.js +29 -2
- package/vue/index.d.ts +42 -5
- package/vue/index.d.ts.map +1 -1
- package/vue/index.js +32 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,76 @@
|
|
|
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.8 — 2026-06-16
|
|
9
|
+
|
|
10
|
+
Patch release for the deep UI-framework audit: broader browser/package gates,
|
|
11
|
+
clean-consumer verification, and runtime fixes found while hardening the public
|
|
12
|
+
surface. No breaking changes, no `MIGRATIONS.json` entry.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Tarball and clean-consumer gates.** `check:consumer-surface` now imports
|
|
17
|
+
public JS/JSON subpaths from the packed package, resolves concrete CSS/doc/
|
|
18
|
+
font subpaths without optional peers, and verifies behavior initializers stay
|
|
19
|
+
SSR-safe. `check:consumer-types` installs the tarball into a clean TypeScript
|
|
20
|
+
consumer and compiles every typed `@ponchia/ui/...` package subpath.
|
|
21
|
+
- **Matrix ownership gates.** `check:component-matrix`,
|
|
22
|
+
`check:behavior-matrix`, `check:helper-matrix`, and `check:binding-matrix`
|
|
23
|
+
now require shipped CSS leaves, public behavior exports, helper modules, and
|
|
24
|
+
framework bindings to have explicit docs, type, unit, or browser ownership.
|
|
25
|
+
- **Public docs and package hygiene gates.** `check:doc-links` validates local
|
|
26
|
+
links and heading anchors across shipped docs, authoring docs, and the docs
|
|
27
|
+
viewer route list. `check:schemas`, `check:visual-baselines`,
|
|
28
|
+
`check:playwright-container`, and stronger `check:contract` / `check:report`
|
|
29
|
+
coverage close stale public snippets, missing visual baselines, and invalid
|
|
30
|
+
report/schema surfaces.
|
|
31
|
+
- **Broader browser coverage.** The Playwright suite now pins docs viewer deep
|
|
32
|
+
links, cascade-layer behavior, source focusing, renderer geometry, behavior
|
|
33
|
+
cleanup/idempotency, connector transforms, annotation motion/overflow,
|
|
34
|
+
command interactions, crosshair payloads, responsive overflow, and more
|
|
35
|
+
forced-colors/reduced-motion contracts.
|
|
36
|
+
- **Packed example smoke coverage.** The example runner now builds and smokes
|
|
37
|
+
the packed examples from one registry, with richer runtime assertions,
|
|
38
|
+
desktop/mobile visual health, and optional Chromium/Firefox/WebKit coverage.
|
|
39
|
+
Astro joins the packed-tarball smoke matrix.
|
|
40
|
+
- **Renderer theme helper coverage.** Chart, Mermaid, D2, and Vega package
|
|
41
|
+
helpers now have type/runtime coverage that checks default exports,
|
|
42
|
+
theme-selection fallbacks, resolved colors, and `var()` leak prevention.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Standalone dot readouts survive `report-kit.css`.** `crosshair.css` now
|
|
47
|
+
scopes pinned readout-chip styling to `.ui-crosshair .ui-readout`, so the
|
|
48
|
+
core dot-matrix `.ui-readout` keeps its normal inline layout when a report
|
|
49
|
+
imports the full report kit.
|
|
50
|
+
- **Rendered docs deep links work.** `docs/index.html` now preserves
|
|
51
|
+
`doc.md#section` routes, generates deterministic heading IDs, keeps
|
|
52
|
+
same-page anchors inside the current doc route, and drops the invalid
|
|
53
|
+
meta-CSP `frame-ancestors` directive that browsers reported as a console
|
|
54
|
+
error.
|
|
55
|
+
- **Command adapter docs match the shipped matrix.** `docs/command.md`, the
|
|
56
|
+
stability matrix, package-contract provenance, and `llms.txt` now name the
|
|
57
|
+
Svelte action and Vue directive/plugin paths alongside the React/Solid/Qwik
|
|
58
|
+
bindings.
|
|
59
|
+
- **CodeQL review findings.** Docs slug helpers no longer use incomplete
|
|
60
|
+
regex-based tag stripping, and wildcard package-subpath expansion replaces
|
|
61
|
+
every placeholder rather than only the first one.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- `npm run check` now owns the unit suite through `check:unit`; CI, release
|
|
66
|
+
workflow validation, PR templates, release docs, and package-contract docs
|
|
67
|
+
all describe the same aggregate gate instead of duplicating `npm test`.
|
|
68
|
+
- Release hygiene now verifies the aggregate check includes unit coverage and
|
|
69
|
+
prevents duplicate release-workflow unit runs from drifting out of sync.
|
|
70
|
+
- `check:exports` now pins package-level CSS metadata: the top-level `style`
|
|
71
|
+
field, root export targets, `files`, and CSS-preserving `sideEffects`.
|
|
72
|
+
- Type-only coverage now instantiates Svelte action and Vue directive
|
|
73
|
+
declarations from consumer-shaped code, including invalid root-shape
|
|
74
|
+
assertions.
|
|
75
|
+
- The default bundle budget is recalibrated to 91 kB raw / 15.65 kB gzip after
|
|
76
|
+
the audited bundle landed below that ceiling at 87.9 kB raw / 15.0 kB gzip.
|
|
77
|
+
|
|
8
78
|
## 0.6.7 — 2026-06-15
|
|
9
79
|
|
|
10
80
|
### Added
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ponchia/ui)
|
|
4
4
|
[](https://www.npmjs.com/package/@ponchia/ui#provenance)
|
|
5
5
|
[](https://github.com/Ponchia/bronto-ui/blob/main/package.json)
|
|
6
|
-
[](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
|
|
7
7
|
[](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml)
|
|
8
8
|
[](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
|
|
9
9
|
[](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE)
|
|
@@ -67,12 +67,12 @@ 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.
|
|
70
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/bronto.css">
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
## Quick start
|
|
74
74
|
|
|
75
|
-
**1. Load the CSS.** One flattened, minified bundle — the whole standard component set, one request (~
|
|
75
|
+
**1. Load the CSS.** One flattened, minified bundle — the whole standard component set, one request (~88 kB raw / ~15 kB gzip):
|
|
76
76
|
|
|
77
77
|
```css
|
|
78
78
|
@import '@ponchia/ui'; /* via a bundler */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"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"}
|
package/annotations/index.js
CHANGED
|
@@ -237,6 +237,24 @@ function validateOffset(opts) {
|
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
function annotationConnectorType(value) {
|
|
241
|
+
const type = value ?? 'callout';
|
|
242
|
+
if (type === 'callout' || type === 'elbow' || type === 'curve') return type;
|
|
243
|
+
throw new TypeError('type must be "callout", "elbow" or "curve"');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function directLabelAxis(value) {
|
|
247
|
+
const axis = value ?? 'y';
|
|
248
|
+
if (axis === 'x' || axis === 'y') return axis;
|
|
249
|
+
throw new TypeError('axis must be "x" or "y"');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function directLabelShape(value) {
|
|
253
|
+
const shape = value ?? 'straight';
|
|
254
|
+
if (shape === 'straight' || shape === 'elbow' || shape === 'curve') return shape;
|
|
255
|
+
throw new TypeError('shape must be "straight", "elbow" or "curve"');
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
function trimForCircle(dx, dy, subject) {
|
|
241
259
|
const len = Math.hypot(dx, dy);
|
|
242
260
|
const radius = dimension('subject.radius', subject.radius);
|
|
@@ -673,7 +691,7 @@ const SUBJECT_BUILDERS = {
|
|
|
673
691
|
* @returns {AnnotationParts}
|
|
674
692
|
*/
|
|
675
693
|
export function annotationParts(opts = {}) {
|
|
676
|
-
const type = opts.type
|
|
694
|
+
const type = annotationConnectorType(opts.type);
|
|
677
695
|
const transform = annotationTransform({ x: opts.x ?? 0, y: opts.y ?? 0 });
|
|
678
696
|
const dx = finite('dx', opts.dx, 0);
|
|
679
697
|
const dy = finite('dy', opts.dy, 0);
|
|
@@ -764,9 +782,9 @@ export function declutterLabels(items, opts = {}) {
|
|
|
764
782
|
*/
|
|
765
783
|
export function directLabels(items, opts = {}) {
|
|
766
784
|
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
767
|
-
const axis = opts.axis
|
|
785
|
+
const axis = directLabelAxis(opts.axis);
|
|
768
786
|
const cross = finite('cross', opts.cross, 0);
|
|
769
|
-
const shape = opts.shape
|
|
787
|
+
const shape = directLabelShape(opts.shape);
|
|
770
788
|
|
|
771
789
|
const anchors = items.map((it) => ({
|
|
772
790
|
anchor: { x: finite('anchor.x', it?.anchor?.x), y: finite('anchor.y', it?.anchor?.y) },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"carousel.d.ts","sourceRoot":"","sources":["carousel.js"],"names":[],"mappings":"
|
|
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"}
|
package/behaviors/carousel.js
CHANGED
|
@@ -5,8 +5,44 @@ import {
|
|
|
5
5
|
bindOnce,
|
|
6
6
|
scrollIntoViewSafe,
|
|
7
7
|
collectHosts,
|
|
8
|
+
closestSafe,
|
|
8
9
|
} from './internal.js';
|
|
9
10
|
|
|
11
|
+
const snapshotAttrs = (el) =>
|
|
12
|
+
Array.from(el.attributes, ({ name, value }) => ({
|
|
13
|
+
name,
|
|
14
|
+
value,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const restoreAttrs = (el, attrs) => {
|
|
18
|
+
for (const { name } of Array.from(el.attributes)) el.removeAttribute(name);
|
|
19
|
+
for (const { name, value } of attrs) el.setAttribute(name, value);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const snapshotNode = (el, { html = false } = {}) =>
|
|
23
|
+
el
|
|
24
|
+
? {
|
|
25
|
+
el,
|
|
26
|
+
attrs: snapshotAttrs(el),
|
|
27
|
+
...(html ? { innerHTML: el.innerHTML } : {}),
|
|
28
|
+
}
|
|
29
|
+
: null;
|
|
30
|
+
|
|
31
|
+
const restoreNode = (state) => {
|
|
32
|
+
if (!state) return;
|
|
33
|
+
restoreAttrs(state.el, state.attrs);
|
|
34
|
+
if ('innerHTML' in state) state.el.innerHTML = state.innerHTML;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const clampIndex = (value, max) => Math.max(0, Math.min(max, value));
|
|
38
|
+
|
|
39
|
+
const renderedStatusIndex = (status) => {
|
|
40
|
+
const match = /^(\d+)\s*\/\s*\d+$/.exec(status?.textContent?.trim() ?? '');
|
|
41
|
+
if (!match) return -1;
|
|
42
|
+
const value = Number(match[1]);
|
|
43
|
+
return Number.isInteger(value) ? value - 1 : -1;
|
|
44
|
+
};
|
|
45
|
+
|
|
10
46
|
/**
|
|
11
47
|
* Image carousel / gallery, built on CSS scroll-snap so touch + trackpad
|
|
12
48
|
* swipe (and momentum) are the browser's, not hand-rolled. This wires the
|
|
@@ -54,36 +90,12 @@ export function initCarousel({ root } = {}) {
|
|
|
54
90
|
const nextBtn = box.querySelector('[data-bronto-carousel-next]');
|
|
55
91
|
const loop = box.hasAttribute('data-bronto-carousel-loop');
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'aria-label',
|
|
64
|
-
box.getAttribute('data-bronto-carousel-label') || 'Carousel',
|
|
65
|
-
);
|
|
66
|
-
if (!viewport.hasAttribute('tabindex')) viewport.tabIndex = 0;
|
|
67
|
-
slides.forEach((s, i) => {
|
|
68
|
-
s.setAttribute('role', 'group');
|
|
69
|
-
s.setAttribute('aria-roledescription', 'slide');
|
|
70
|
-
if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', `${i + 1} of ${n}`);
|
|
71
|
-
});
|
|
72
|
-
if (status) status.setAttribute('aria-live', 'polite');
|
|
73
|
-
for (const b of [prevBtn, nextBtn]) {
|
|
74
|
-
if (!b) continue;
|
|
75
|
-
if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
|
|
76
|
-
}
|
|
77
|
-
for (const b of thumbs) {
|
|
78
|
-
if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
|
|
79
|
-
}
|
|
80
|
-
if (prevBtn && !prevBtn.hasAttribute('aria-label'))
|
|
81
|
-
prevBtn.setAttribute('aria-label', 'Previous');
|
|
82
|
-
if (nextBtn && !nextBtn.hasAttribute('aria-label')) nextBtn.setAttribute('aria-label', 'Next');
|
|
83
|
-
|
|
84
|
-
let index = Math.max(
|
|
85
|
-
0,
|
|
86
|
-
slides.findIndex((s) => s.hasAttribute('data-bronto-carousel-current')),
|
|
93
|
+
const authoredIndex = slides.findIndex((s) => s.hasAttribute('data-bronto-carousel-current'));
|
|
94
|
+
const renderedThumbIndex = thumbs.findIndex((t) => t.getAttribute('aria-current') === 'true');
|
|
95
|
+
const statusIndex = renderedStatusIndex(status);
|
|
96
|
+
let index = clampIndex(
|
|
97
|
+
renderedThumbIndex >= 0 ? renderedThumbIndex : statusIndex >= 0 ? statusIndex : authoredIndex,
|
|
98
|
+
n - 1,
|
|
87
99
|
);
|
|
88
100
|
|
|
89
101
|
// While a button/keyboard nav is smooth-scrolling, the IntersectionObserver
|
|
@@ -139,15 +151,15 @@ export function initCarousel({ root } = {}) {
|
|
|
139
151
|
goTo(target);
|
|
140
152
|
};
|
|
141
153
|
const onClick = (e) => {
|
|
142
|
-
if (prevBtn && e.target
|
|
154
|
+
if (prevBtn && closestSafe(e.target, '[data-bronto-carousel-prev]')) {
|
|
143
155
|
goTo(index - 1);
|
|
144
156
|
return;
|
|
145
157
|
}
|
|
146
|
-
if (nextBtn && e.target
|
|
158
|
+
if (nextBtn && closestSafe(e.target, '[data-bronto-carousel-next]')) {
|
|
147
159
|
goTo(index + 1);
|
|
148
160
|
return;
|
|
149
161
|
}
|
|
150
|
-
const thumb = e.target
|
|
162
|
+
const thumb = closestSafe(e.target, '.ui-carousel__thumb');
|
|
151
163
|
if (thumb) {
|
|
152
164
|
const i = thumbs.indexOf(thumb);
|
|
153
165
|
if (i >= 0) goTo(i);
|
|
@@ -181,20 +193,64 @@ export function initCarousel({ root } = {}) {
|
|
|
181
193
|
);
|
|
182
194
|
}
|
|
183
195
|
|
|
184
|
-
render();
|
|
185
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();
|
|
186
235
|
viewport.addEventListener('keydown', onKey);
|
|
187
236
|
box.addEventListener('click', onClick);
|
|
188
237
|
// Observe inside the add callback so observe/disconnect pair with the
|
|
189
238
|
// binding lifecycle: a re-init tears down the prior binding (which
|
|
190
239
|
// disconnects the old observer) before this starts, so two observers
|
|
191
240
|
// never watch the same slides — even for one tick.
|
|
192
|
-
|
|
241
|
+
if (io) {
|
|
242
|
+
holdProgrammatic();
|
|
243
|
+
slides.forEach((s) => io.observe(s));
|
|
244
|
+
}
|
|
193
245
|
return () => {
|
|
194
246
|
viewport.removeEventListener('keydown', onKey);
|
|
195
247
|
box.removeEventListener('click', onClick);
|
|
196
248
|
io?.disconnect();
|
|
197
249
|
if (progTimer) clearTimeout(progTimer);
|
|
250
|
+
restoreNode(state.viewport);
|
|
251
|
+
state.slides.forEach(restoreNode);
|
|
252
|
+
restoreNode(state.status);
|
|
253
|
+
state.controls.forEach(restoreNode);
|
|
198
254
|
};
|
|
199
255
|
});
|
|
200
256
|
cleanups.push(bound);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["combobox.js"],"names":[],"mappings":"
|
|
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"}
|
package/behaviors/combobox.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
scrollIntoViewSafe,
|
|
8
8
|
wrapIndex,
|
|
9
9
|
collectHosts,
|
|
10
|
+
closestSafe,
|
|
10
11
|
} from './internal.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -54,12 +55,80 @@ export function initCombobox({ root } = {}) {
|
|
|
54
55
|
const boxes = collectHosts(host, '[data-bronto-combobox]');
|
|
55
56
|
const cleanups = [];
|
|
56
57
|
|
|
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
|
+
|
|
57
76
|
for (const box of boxes) {
|
|
58
77
|
const input = box.querySelector('[role="combobox"], .ui-combobox__input');
|
|
59
78
|
const list = box.querySelector('[role="listbox"], .ui-combobox__list');
|
|
60
79
|
if (!input || !list) continue;
|
|
61
80
|
const empty = box.querySelector('.ui-combobox__empty');
|
|
62
|
-
const
|
|
81
|
+
const optionStates = new WeakMap();
|
|
82
|
+
let listId = '';
|
|
83
|
+
|
|
84
|
+
const rememberOptionState = (option) => {
|
|
85
|
+
if (optionStates.has(option)) return;
|
|
86
|
+
optionStates.set(option, {
|
|
87
|
+
hidden: option.hidden,
|
|
88
|
+
active: option.classList.contains('is-active'),
|
|
89
|
+
attrs: snapshotAttrs(option, ['id', 'role', 'aria-selected']),
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const rememberState = () => ({
|
|
94
|
+
input: snapshotAttrs(input, [
|
|
95
|
+
'role',
|
|
96
|
+
'aria-controls',
|
|
97
|
+
'aria-autocomplete',
|
|
98
|
+
'aria-expanded',
|
|
99
|
+
'aria-activedescendant',
|
|
100
|
+
'autocomplete',
|
|
101
|
+
]),
|
|
102
|
+
list: {
|
|
103
|
+
hidden: list.hidden,
|
|
104
|
+
attrs: snapshotAttrs(list, ['id', 'role', 'aria-label']),
|
|
105
|
+
},
|
|
106
|
+
empty: empty
|
|
107
|
+
? {
|
|
108
|
+
hidden: empty.hidden,
|
|
109
|
+
attrs: snapshotAttrs(empty, ['role']),
|
|
110
|
+
}
|
|
111
|
+
: null,
|
|
112
|
+
options: optionStates,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const restoreState = (state) => {
|
|
116
|
+
restoreAttrs(input, state.input);
|
|
117
|
+
list.hidden = state.list.hidden;
|
|
118
|
+
restoreAttrs(list, state.list.attrs);
|
|
119
|
+
if (empty && state.empty) {
|
|
120
|
+
empty.hidden = state.empty.hidden;
|
|
121
|
+
restoreAttrs(empty, state.empty.attrs);
|
|
122
|
+
}
|
|
123
|
+
for (const option of options) {
|
|
124
|
+
const optionState = state.options.get(option);
|
|
125
|
+
if (!optionState) continue;
|
|
126
|
+
option.hidden = optionState.hidden;
|
|
127
|
+
option.classList.toggle('is-active', optionState.active);
|
|
128
|
+
restoreAttrs(option, optionState.attrs);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
63
132
|
// Re-readable so the opt-in MutationObserver (`data-bronto-combobox-live`)
|
|
64
133
|
// can pick up async/replaced option nodes without a full re-init. `visible`,
|
|
65
134
|
// `filter`, `move`, etc. close over this binding, so reassigning it is enough.
|
|
@@ -67,51 +136,11 @@ export function initCombobox({ root } = {}) {
|
|
|
67
136
|
const syncOptions = () => {
|
|
68
137
|
options = [...list.querySelectorAll('[role="option"], .ui-combobox__option')];
|
|
69
138
|
options.forEach((o, i) => {
|
|
139
|
+
rememberOptionState(o);
|
|
70
140
|
if (!o.id) o.id = `${listId}-opt-${i}`;
|
|
71
141
|
o.setAttribute('role', 'option');
|
|
72
142
|
});
|
|
73
143
|
};
|
|
74
|
-
syncOptions();
|
|
75
|
-
list.setAttribute('role', 'listbox');
|
|
76
|
-
// Give the listbox its own accessible name (a bare role=listbox is unnamed
|
|
77
|
-
// to a screen reader) by mirroring the input's REAL name. (a11y review C30.)
|
|
78
|
-
// The placeholder is deliberately NOT in this chain: the input warning below
|
|
79
|
-
// already rejects a placeholder as an inadequate name, so papering the
|
|
80
|
-
// listbox over with it would contradict that — if there's no real name the
|
|
81
|
-
// listbox stays unnamed and the warning is the signal. (component audit C28.)
|
|
82
|
-
if (!list.hasAttribute('aria-label') && !list.hasAttribute('aria-labelledby')) {
|
|
83
|
-
const name = input.getAttribute('aria-label') || input.labels?.[0]?.textContent?.trim();
|
|
84
|
-
if (name) list.setAttribute('aria-label', name);
|
|
85
|
-
}
|
|
86
|
-
// A `role="combobox"` with no accessible name is a silent AT failure. A
|
|
87
|
-
// placeholder is not a robust name (it can vanish and is ignored by some
|
|
88
|
-
// AT), so warn unless there is a real label/aria-label/aria-labelledby/title
|
|
89
|
-
// (C7). We can't invent a good name, hence a dev-time warning, not a guess.
|
|
90
|
-
const inputNamed =
|
|
91
|
-
input.hasAttribute('aria-label') ||
|
|
92
|
-
input.hasAttribute('aria-labelledby') ||
|
|
93
|
-
!!input.labels?.length ||
|
|
94
|
-
input.hasAttribute('title');
|
|
95
|
-
if (!inputNamed && typeof console !== 'undefined') {
|
|
96
|
-
console.warn(
|
|
97
|
-
'[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
input.setAttribute('role', 'combobox');
|
|
101
|
-
input.setAttribute('aria-controls', listId);
|
|
102
|
-
input.setAttribute('aria-autocomplete', 'list');
|
|
103
|
-
input.setAttribute('aria-expanded', 'false');
|
|
104
|
-
input.setAttribute('autocomplete', 'off');
|
|
105
|
-
list.hidden = true;
|
|
106
|
-
// Hide the empty-state at rest: it must only appear once a filter yields no
|
|
107
|
-
// matches, never on an idle combobox. Without this an author who omits
|
|
108
|
-
// `hidden` on `.ui-combobox__empty` ships a box that reads "No matches"
|
|
109
|
-
// before the user has typed anything. (component audit C10.) Make it a
|
|
110
|
-
// status live region so its appearance is announced. (component audit C38.)
|
|
111
|
-
if (empty) {
|
|
112
|
-
empty.hidden = true;
|
|
113
|
-
if (!empty.hasAttribute('role')) empty.setAttribute('role', 'status');
|
|
114
|
-
}
|
|
115
144
|
|
|
116
145
|
let active = -1;
|
|
117
146
|
const visible = () => options.filter((o) => !o.hidden);
|
|
@@ -243,7 +272,7 @@ export function initCombobox({ root } = {}) {
|
|
|
243
272
|
}
|
|
244
273
|
};
|
|
245
274
|
const onOptionClick = (e) => {
|
|
246
|
-
const opt = e.target
|
|
275
|
+
const opt = closestSafe(e.target, '[role="option"], .ui-combobox__option');
|
|
247
276
|
if (opt) select(opt);
|
|
248
277
|
};
|
|
249
278
|
const onDocClick = (e) => {
|
|
@@ -251,6 +280,49 @@ export function initCombobox({ root } = {}) {
|
|
|
251
280
|
};
|
|
252
281
|
|
|
253
282
|
const bound = bindOnce(box, 'combobox', () => {
|
|
283
|
+
const state = rememberState();
|
|
284
|
+
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();
|
|
254
326
|
input.addEventListener('input', onInput);
|
|
255
327
|
input.addEventListener('keydown', onKey);
|
|
256
328
|
list.addEventListener('click', onOptionClick);
|
|
@@ -268,6 +340,8 @@ export function initCombobox({ root } = {}) {
|
|
|
268
340
|
input.removeEventListener('keydown', onKey);
|
|
269
341
|
list.removeEventListener('click', onOptionClick);
|
|
270
342
|
document.removeEventListener('click', onDocClick);
|
|
343
|
+
restoreState(state);
|
|
344
|
+
active = -1;
|
|
271
345
|
};
|
|
272
346
|
});
|
|
273
347
|
cleanups.push(bound);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"AAYA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0N3C;;;;;WApPa,MAAM;;;;WACN,MAAM"}
|