@ponchia/ui 0.6.9 → 0.6.11
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 +92 -0
- package/README.md +38 -25
- package/annotations/index.d.ts +15 -15
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +52 -34
- package/behaviors/carousel.d.ts +7 -3
- package/behaviors/carousel.d.ts.map +1 -1
- package/behaviors/carousel.js +157 -27
- package/behaviors/combobox.d.ts +1 -1
- package/behaviors/combobox.d.ts.map +1 -1
- package/behaviors/combobox.js +46 -23
- package/behaviors/command.d.ts +1 -1
- package/behaviors/command.d.ts.map +1 -1
- package/behaviors/command.js +63 -23
- package/behaviors/connectors.d.ts.map +1 -1
- package/behaviors/connectors.js +126 -19
- package/behaviors/crosshair.d.ts.map +1 -1
- package/behaviors/crosshair.js +71 -8
- package/behaviors/dialog.d.ts.map +1 -1
- package/behaviors/dialog.js +20 -3
- package/behaviors/disclosure.d.ts.map +1 -1
- package/behaviors/disclosure.js +35 -6
- package/behaviors/dismissible.js +1 -1
- package/behaviors/forms.d.ts +23 -2
- package/behaviors/forms.d.ts.map +1 -1
- package/behaviors/forms.js +97 -9
- package/behaviors/glyph.d.ts.map +1 -1
- package/behaviors/glyph.js +56 -5
- package/behaviors/internal.d.ts.map +1 -1
- package/behaviors/internal.js +52 -5
- package/behaviors/menu.d.ts.map +1 -1
- package/behaviors/menu.js +2 -1
- package/behaviors/modal.d.ts.map +1 -1
- package/behaviors/modal.js +25 -9
- package/behaviors/popover.d.ts.map +1 -1
- package/behaviors/popover.js +8 -6
- package/behaviors/sources.d.ts.map +1 -1
- package/behaviors/sources.js +24 -3
- package/behaviors/splitter.d.ts.map +1 -1
- package/behaviors/splitter.js +27 -6
- package/behaviors/table.d.ts.map +1 -1
- package/behaviors/table.js +44 -7
- package/behaviors/tabs.d.ts.map +1 -1
- package/behaviors/tabs.js +51 -14
- package/behaviors/theme.d.ts.map +1 -1
- package/behaviors/theme.js +64 -4
- package/behaviors/toast.d.ts +6 -1
- package/behaviors/toast.d.ts.map +1 -1
- package/behaviors/toast.js +48 -12
- package/classes/classes.json +57 -2
- package/classes/index.d.ts +13 -2
- package/classes/index.js +88 -40
- package/connectors/index.d.ts +4 -4
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +14 -12
- package/css/annotations.css +1 -0
- package/css/app.css +7 -0
- package/css/base.css +3 -0
- package/css/bullet.css +41 -7
- package/css/code.css +14 -0
- package/css/command.css +10 -0
- package/css/dataviz.css +27 -0
- package/css/diff.css +2 -0
- package/css/disclosure.css +8 -0
- package/css/dots.css +1 -1
- package/css/feedback.css +9 -0
- package/css/interval.css +20 -2
- package/css/legend.css +10 -9
- package/css/marks.css +1 -0
- package/css/motion.css +2 -0
- package/css/overlay.css +14 -2
- package/css/primitives.css +1 -1
- package/css/report.css +3 -0
- package/css/sources.css +4 -4
- package/css/spotlight.css +6 -0
- package/css/table.css +19 -0
- package/css/term.css +4 -1
- package/css/tokens.css +8 -13
- package/css/workbench.css +128 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/app.css +1 -1
- package/dist/css/bullet.css +1 -1
- package/dist/css/code.css +1 -1
- package/dist/css/command.css +1 -1
- package/dist/css/dataviz.css +1 -1
- package/dist/css/diff.css +1 -1
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/interval.css +1 -1
- package/dist/css/legend.css +1 -1
- package/dist/css/marks.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report-kit.css +1 -1
- package/dist/css/sources.css +1 -1
- package/dist/css/spotlight.css +1 -1
- package/dist/css/table.css +1 -1
- package/dist/css/term.css +1 -1
- package/dist/css/tokens.css +1 -1
- package/dist/css/workbench.css +1 -1
- package/docs/annotations.md +27 -0
- package/docs/architecture.md +5 -3
- package/docs/bullet.md +6 -1
- package/docs/clamp.md +5 -0
- package/docs/command.md +3 -2
- package/docs/contrast.md +3 -3
- package/docs/crosshair.md +6 -0
- package/docs/dots.md +10 -3
- package/docs/figure.md +7 -0
- package/docs/glyphs.md +14 -2
- package/docs/highlights.md +9 -0
- package/docs/interval.md +6 -0
- package/docs/mermaid.md +5 -3
- package/docs/package-contract.md +24 -1
- package/docs/reference.md +21 -1
- package/docs/reporting.md +8 -8
- package/docs/selection.md +9 -0
- package/docs/sources.md +5 -0
- package/docs/state.md +6 -0
- package/docs/textref.md +18 -13
- package/docs/theming.md +18 -8
- package/docs/toc.md +6 -0
- package/docs/tree.md +9 -2
- package/docs/usage.md +2 -2
- package/docs/vega.md +5 -3
- package/docs/workbench.md +56 -9
- package/glyphs/glyphs.js +62 -8
- package/index.d.ts +1 -0
- package/llms.txt +18 -14
- package/package.json +98 -6
- package/qwik/index.d.ts +4 -3
- package/qwik/index.d.ts.map +1 -1
- package/qwik/index.js +7 -5
- package/react/index.d.ts +4 -3
- package/react/index.d.ts.map +1 -1
- package/react/index.js +3 -2
- package/solid/index.d.ts +7 -5
- package/solid/index.d.ts.map +1 -1
- package/solid/index.js +11 -7
- package/tokens/vega.d.ts +1 -1
- package/tokens/vega.js +3 -2
- package/vue/index.d.ts.map +1 -1
- package/vue/index.js +37 -3
package/behaviors/carousel.js
CHANGED
|
@@ -43,6 +43,43 @@ const renderedStatusIndex = (status) => {
|
|
|
43
43
|
return Number.isInteger(value) ? value - 1 : -1;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const rectRight = (rect) => rect.right ?? rect.left + rect.width;
|
|
47
|
+
const rectBottom = (rect) => rect.bottom ?? rect.top + rect.height;
|
|
48
|
+
|
|
49
|
+
function intersectionRatioInViewport(viewportRect, slideRect) {
|
|
50
|
+
if (!slideRect.width || !slideRect.height) return 0;
|
|
51
|
+
const left = Math.max(viewportRect.left, slideRect.left);
|
|
52
|
+
const right = Math.min(rectRight(viewportRect), rectRight(slideRect));
|
|
53
|
+
const top = Math.max(viewportRect.top, slideRect.top);
|
|
54
|
+
const bottom = Math.min(rectBottom(viewportRect), rectBottom(slideRect));
|
|
55
|
+
const width = Math.max(0, right - left);
|
|
56
|
+
const height = Math.max(0, bottom - top);
|
|
57
|
+
return (width * height) / (slideRect.width * slideRect.height);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function measuredCarouselIndex(viewport, slides) {
|
|
61
|
+
const viewportRect = viewport.getBoundingClientRect();
|
|
62
|
+
if (!viewportRect.width || !viewportRect.height) return -1;
|
|
63
|
+
let bestIndex = -1;
|
|
64
|
+
let bestRatio = 0;
|
|
65
|
+
for (const [i, slide] of slides.entries()) {
|
|
66
|
+
const ratio = intersectionRatioInViewport(viewportRect, slide.getBoundingClientRect());
|
|
67
|
+
if (ratio > bestRatio) {
|
|
68
|
+
bestIndex = i;
|
|
69
|
+
bestRatio = ratio;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return bestRatio >= 0.6 ? bestIndex : -1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function bestCarouselEntry(entries) {
|
|
76
|
+
let best = null;
|
|
77
|
+
for (const ent of entries) {
|
|
78
|
+
if (ent.isIntersecting && (!best || ent.intersectionRatio > best.intersectionRatio)) best = ent;
|
|
79
|
+
}
|
|
80
|
+
return best;
|
|
81
|
+
}
|
|
82
|
+
|
|
46
83
|
const snapshotCarouselState = ({ viewport, slides, status, prevBtn, nextBtn, thumbs }) => ({
|
|
47
84
|
viewport: snapshotNode(viewport),
|
|
48
85
|
slides: slides.map((slide) => snapshotNode(slide)),
|
|
@@ -61,11 +98,38 @@ function setDefaultButtonType(button) {
|
|
|
61
98
|
if (button?.tagName === 'BUTTON' && !button.hasAttribute('type')) button.type = 'button';
|
|
62
99
|
}
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
const carouselRoleDescription = (box, viewport, roleDescription) =>
|
|
102
|
+
viewport.getAttribute('data-bronto-carousel-roledescription') ||
|
|
103
|
+
box.getAttribute('data-bronto-carousel-roledescription') ||
|
|
104
|
+
roleDescription ||
|
|
105
|
+
'carousel';
|
|
106
|
+
|
|
107
|
+
const carouselSlideRoleDescription = (box, viewport, slide) =>
|
|
108
|
+
slide.getAttribute('data-bronto-carousel-slide-roledescription') ||
|
|
109
|
+
viewport.getAttribute('data-bronto-carousel-slide-roledescription') ||
|
|
110
|
+
box.getAttribute('data-bronto-carousel-slide-roledescription') ||
|
|
111
|
+
'slide';
|
|
112
|
+
|
|
113
|
+
function applyCarouselA11y({
|
|
114
|
+
box,
|
|
115
|
+
viewport,
|
|
116
|
+
slides,
|
|
117
|
+
status,
|
|
118
|
+
prevBtn,
|
|
119
|
+
nextBtn,
|
|
120
|
+
thumbs,
|
|
121
|
+
n,
|
|
122
|
+
roleDescription,
|
|
123
|
+
}) {
|
|
65
124
|
// ARIA scaffolding — pragmatic carousel semantics (not the full APG
|
|
66
125
|
// tablist), the same restraint initMenu takes.
|
|
67
126
|
viewport.setAttribute('role', 'group');
|
|
68
|
-
viewport.
|
|
127
|
+
if (!viewport.hasAttribute('aria-roledescription')) {
|
|
128
|
+
viewport.setAttribute(
|
|
129
|
+
'aria-roledescription',
|
|
130
|
+
carouselRoleDescription(box, viewport, roleDescription),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
69
133
|
if (!viewport.hasAttribute('aria-label')) {
|
|
70
134
|
viewport.setAttribute(
|
|
71
135
|
'aria-label',
|
|
@@ -75,7 +139,12 @@ function applyCarouselA11y({ box, viewport, slides, status, prevBtn, nextBtn, th
|
|
|
75
139
|
if (!viewport.hasAttribute('tabindex')) viewport.tabIndex = 0;
|
|
76
140
|
slides.forEach((slide, i) => {
|
|
77
141
|
slide.setAttribute('role', 'group');
|
|
78
|
-
slide.
|
|
142
|
+
if (!slide.hasAttribute('aria-roledescription')) {
|
|
143
|
+
slide.setAttribute(
|
|
144
|
+
'aria-roledescription',
|
|
145
|
+
carouselSlideRoleDescription(box, viewport, slide),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
79
148
|
if (!slide.hasAttribute('aria-label')) slide.setAttribute('aria-label', `${i + 1} of ${n}`);
|
|
80
149
|
});
|
|
81
150
|
if (status) status.setAttribute('aria-live', 'polite');
|
|
@@ -100,11 +169,26 @@ function bindCarouselLifecycle({
|
|
|
100
169
|
io,
|
|
101
170
|
holdProgrammatic,
|
|
102
171
|
clearProgrammaticTimer,
|
|
172
|
+
onUserScrollStart,
|
|
173
|
+
roleDescription,
|
|
103
174
|
}) {
|
|
104
175
|
const state = snapshotCarouselState({ viewport, slides, status, prevBtn, nextBtn, thumbs });
|
|
105
|
-
applyCarouselA11y({
|
|
176
|
+
applyCarouselA11y({
|
|
177
|
+
box,
|
|
178
|
+
viewport,
|
|
179
|
+
slides,
|
|
180
|
+
status,
|
|
181
|
+
prevBtn,
|
|
182
|
+
nextBtn,
|
|
183
|
+
thumbs,
|
|
184
|
+
n,
|
|
185
|
+
roleDescription,
|
|
186
|
+
});
|
|
106
187
|
render();
|
|
107
188
|
viewport.addEventListener('keydown', onKey);
|
|
189
|
+
viewport.addEventListener('pointerdown', onUserScrollStart);
|
|
190
|
+
viewport.addEventListener('touchstart', onUserScrollStart, { passive: true });
|
|
191
|
+
viewport.addEventListener('wheel', onUserScrollStart, { passive: true });
|
|
108
192
|
box.addEventListener('click', onClick);
|
|
109
193
|
// Observe inside the add callback so observe/disconnect pair with the
|
|
110
194
|
// binding lifecycle: a re-init tears down the prior binding (which
|
|
@@ -116,6 +200,9 @@ function bindCarouselLifecycle({
|
|
|
116
200
|
}
|
|
117
201
|
return () => {
|
|
118
202
|
viewport.removeEventListener('keydown', onKey);
|
|
203
|
+
viewport.removeEventListener('pointerdown', onUserScrollStart);
|
|
204
|
+
viewport.removeEventListener('touchstart', onUserScrollStart);
|
|
205
|
+
viewport.removeEventListener('wheel', onUserScrollStart);
|
|
119
206
|
box.removeEventListener('click', onClick);
|
|
120
207
|
io?.disconnect();
|
|
121
208
|
clearProgrammaticTimer();
|
|
@@ -135,7 +222,9 @@ function bindCarouselLifecycle({
|
|
|
135
222
|
* `[data-bronto-carousel-prev]` / `[data-bronto-carousel-next]` controls,
|
|
136
223
|
* a `.ui-carousel__thumbs` list of `.ui-carousel__thumb` buttons, and a
|
|
137
224
|
* `.ui-carousel__status` counter slot. Add `data-bronto-carousel-loop` to
|
|
138
|
-
* wrap at the ends, `data-bronto-carousel-label` to name the region
|
|
225
|
+
* wrap at the ends, `data-bronto-carousel-label` to name the region, and
|
|
226
|
+
* `data-bronto-carousel-roledescription` to localize the default
|
|
227
|
+
* `aria-roledescription` when the viewport does not already carry one.
|
|
139
228
|
*
|
|
140
229
|
* A full-screen **lightbox** is the same markup inside a native
|
|
141
230
|
* `<dialog class="ui-lightbox">` opened by {@link initDialog}: the
|
|
@@ -146,10 +235,10 @@ function bindCarouselLifecycle({
|
|
|
146
235
|
* (button, key, thumbnail, or swipe). SSR-safe, idempotent per carousel;
|
|
147
236
|
* returns a cleanup function.
|
|
148
237
|
*
|
|
149
|
-
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
238
|
+
* @param {import('./internal.js').DelegateOpts & { roleDescription?: string }} [opts]
|
|
150
239
|
* @returns {import('./internal.js').Cleanup}
|
|
151
240
|
*/
|
|
152
|
-
export function initCarousel({ root } = {}) {
|
|
241
|
+
export function initCarousel({ root, roleDescription } = {}) {
|
|
153
242
|
if (!hasDom()) return noop;
|
|
154
243
|
const host = resolveHost(root);
|
|
155
244
|
if (!host) return noop;
|
|
@@ -181,16 +270,48 @@ export function initCarousel({ root } = {}) {
|
|
|
181
270
|
// While a button/keyboard nav is smooth-scrolling, the IntersectionObserver
|
|
182
271
|
// would observe the intermediate slides crossing its threshold and re-fire
|
|
183
272
|
// `bronto:change` for each — a feedback burst on a single Home→End jump.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
273
|
+
// Instant/reduced-motion scrolls settle synchronously, so only smooth
|
|
274
|
+
// programmatic scrolls hold IO updates; scrollend releases early when present.
|
|
186
275
|
let programmatic = false;
|
|
187
276
|
let progTimer = null;
|
|
277
|
+
let clearScrollEnd = null;
|
|
278
|
+
let ignoredProgrammaticEntry = null;
|
|
279
|
+
let userScrolledDuringProgrammatic = false;
|
|
280
|
+
const releaseProgrammatic = ({ replay = true } = {}) => {
|
|
281
|
+
const replayIgnored = replay && userScrolledDuringProgrammatic && ignoredProgrammaticEntry;
|
|
282
|
+
programmatic = false;
|
|
283
|
+
if (progTimer) clearTimeout(progTimer);
|
|
284
|
+
progTimer = null;
|
|
285
|
+
clearScrollEnd?.();
|
|
286
|
+
clearScrollEnd = null;
|
|
287
|
+
userScrolledDuringProgrammatic = false;
|
|
288
|
+
const ignored = ignoredProgrammaticEntry;
|
|
289
|
+
ignoredProgrammaticEntry = null;
|
|
290
|
+
if (replayIgnored) {
|
|
291
|
+
const measured = measuredCarouselIndex(viewport, slides);
|
|
292
|
+
if (measured >= 0) syncToIndex(measured);
|
|
293
|
+
else syncFromEntry(ignored);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const shouldHoldProgrammatic = () => {
|
|
297
|
+
const view = viewport.ownerDocument?.defaultView;
|
|
298
|
+
if (view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return false;
|
|
299
|
+
return view?.getComputedStyle?.(viewport).scrollBehavior === 'smooth';
|
|
300
|
+
};
|
|
188
301
|
const holdProgrammatic = () => {
|
|
302
|
+
if (!shouldHoldProgrammatic()) {
|
|
303
|
+
releaseProgrammatic({ replay: false });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
189
306
|
programmatic = true;
|
|
307
|
+
ignoredProgrammaticEntry = null;
|
|
308
|
+
userScrolledDuringProgrammatic = false;
|
|
190
309
|
if (progTimer) clearTimeout(progTimer);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
310
|
+
clearScrollEnd?.();
|
|
311
|
+
const onScrollEnd = () => releaseProgrammatic();
|
|
312
|
+
viewport.addEventListener('scrollend', onScrollEnd, { once: true });
|
|
313
|
+
clearScrollEnd = () => viewport.removeEventListener('scrollend', onScrollEnd);
|
|
314
|
+
progTimer = setTimeout(releaseProgrammatic, 500);
|
|
194
315
|
progTimer?.unref?.(); // don't keep a Node test process alive
|
|
195
316
|
};
|
|
196
317
|
|
|
@@ -209,6 +330,22 @@ export function initCarousel({ root } = {}) {
|
|
|
209
330
|
|
|
210
331
|
const reveal = (el) => scrollIntoViewSafe(el, { block: 'nearest', inline: 'center' });
|
|
211
332
|
|
|
333
|
+
const syncToIndex = (i) => {
|
|
334
|
+
if (i < 0 || i === index) return;
|
|
335
|
+
index = i;
|
|
336
|
+
render();
|
|
337
|
+
reveal(thumbs[index]);
|
|
338
|
+
emit();
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const syncFromEntry = (entry) => syncToIndex(slides.indexOf(entry.target));
|
|
342
|
+
|
|
343
|
+
const onUserScrollStart = () => {
|
|
344
|
+
if (programmatic) userScrolledDuringProgrammatic = true;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const clearProgrammaticHold = () => releaseProgrammatic({ replay: false });
|
|
348
|
+
|
|
212
349
|
const goTo = (i, { emitChange = true } = {}) => {
|
|
213
350
|
const next = loop ? (i + n) % n : Math.max(0, Math.min(n - 1, i));
|
|
214
351
|
const changed = next !== index;
|
|
@@ -254,20 +391,13 @@ export function initCarousel({ root } = {}) {
|
|
|
254
391
|
if (typeof IntersectionObserver === 'function') {
|
|
255
392
|
io = new IntersectionObserver(
|
|
256
393
|
(entries) => {
|
|
257
|
-
|
|
258
|
-
let best = null;
|
|
259
|
-
for (const ent of entries) {
|
|
260
|
-
if (ent.isIntersecting && (!best || ent.intersectionRatio > best.intersectionRatio))
|
|
261
|
-
best = ent;
|
|
262
|
-
}
|
|
394
|
+
const best = bestCarouselEntry(entries);
|
|
263
395
|
if (!best) return;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
render();
|
|
268
|
-
reveal(thumbs[index]);
|
|
269
|
-
emit();
|
|
396
|
+
if (programmatic) {
|
|
397
|
+
ignoredProgrammaticEntry = best;
|
|
398
|
+
return; // ignore the echo of a button/key-driven scroll until release
|
|
270
399
|
}
|
|
400
|
+
syncFromEntry(best);
|
|
271
401
|
},
|
|
272
402
|
{ root: viewport, threshold: 0.6 },
|
|
273
403
|
);
|
|
@@ -288,9 +418,9 @@ export function initCarousel({ root } = {}) {
|
|
|
288
418
|
onClick,
|
|
289
419
|
io,
|
|
290
420
|
holdProgrammatic,
|
|
291
|
-
clearProgrammaticTimer:
|
|
292
|
-
|
|
293
|
-
|
|
421
|
+
clearProgrammaticTimer: clearProgrammaticHold,
|
|
422
|
+
onUserScrollStart,
|
|
423
|
+
roleDescription,
|
|
294
424
|
}),
|
|
295
425
|
);
|
|
296
426
|
cleanups.push(bound);
|
package/behaviors/combobox.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* optional `data-value`). An optional `.ui-combobox__empty` (hidden at rest)
|
|
16
16
|
* shows when nothing matches. The behavior owns ids, `aria-expanded`,
|
|
17
17
|
* `aria-controls`, `aria-activedescendant`, roving active option,
|
|
18
|
-
* type-to-filter,
|
|
18
|
+
* type-to-filter, keyboard list navigation (Down/Up/Enter/Escape/Tab),
|
|
19
19
|
* pointer select, and outside-click close. On select the **visible input shows
|
|
20
20
|
* the option's text label**, while the emitted `bronto:change` CustomEvent
|
|
21
21
|
* carries the option's `data-value` code: `{ detail: { value, label } }` (value
|
|
@@ -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":"AAkKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAqO3C"}
|
package/behaviors/combobox.js
CHANGED
|
@@ -12,6 +12,23 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const COMBOBOX_OPTION_SELECTOR = '[role="option"], .ui-combobox__option';
|
|
14
14
|
|
|
15
|
+
const localeOf = (el) => {
|
|
16
|
+
const locale =
|
|
17
|
+
closestSafe(el, '[lang]')?.getAttribute('lang')?.trim() ||
|
|
18
|
+
el?.ownerDocument?.documentElement?.getAttribute('lang')?.trim();
|
|
19
|
+
return locale || undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const lowerForSearch = (value, locale) => {
|
|
23
|
+
const text = String(value ?? '');
|
|
24
|
+
if (!locale) return text.toLowerCase();
|
|
25
|
+
try {
|
|
26
|
+
return text.toLocaleLowerCase(locale);
|
|
27
|
+
} catch {
|
|
28
|
+
return text.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
15
32
|
const snapshotAttrs = (el, names) => {
|
|
16
33
|
const out = {};
|
|
17
34
|
for (const name of names) {
|
|
@@ -30,14 +47,26 @@ const restoreAttrs = (el, attrs) => {
|
|
|
30
47
|
}
|
|
31
48
|
};
|
|
32
49
|
|
|
50
|
+
const labelFromIdRefs = (input) => {
|
|
51
|
+
const refs = input.getAttribute('aria-labelledby')?.trim();
|
|
52
|
+
if (!refs) return '';
|
|
53
|
+
const doc = input.ownerDocument;
|
|
54
|
+
return refs
|
|
55
|
+
.split(/\s+/)
|
|
56
|
+
.map((id) => doc.getElementById(id)?.textContent?.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join(' ')
|
|
59
|
+
.trim();
|
|
60
|
+
};
|
|
61
|
+
|
|
33
62
|
const inputLabel = (input) =>
|
|
34
|
-
input
|
|
63
|
+
labelFromIdRefs(input) ||
|
|
64
|
+
input.getAttribute('aria-label')?.trim() ||
|
|
65
|
+
(input.labels ? [...input.labels].map((label) => label.textContent?.trim()).join(' ') : '') ||
|
|
66
|
+
input.getAttribute('title')?.trim() ||
|
|
67
|
+
'';
|
|
35
68
|
|
|
36
|
-
const inputHasAccessibleName = (input) =>
|
|
37
|
-
input.hasAttribute('aria-label') ||
|
|
38
|
-
input.hasAttribute('aria-labelledby') ||
|
|
39
|
-
!!input.labels?.length ||
|
|
40
|
-
input.hasAttribute('title');
|
|
69
|
+
const inputHasAccessibleName = (input) => !!inputLabel(input);
|
|
41
70
|
|
|
42
71
|
function mirrorListboxLabel(input, list) {
|
|
43
72
|
if (list.hasAttribute('aria-label') || list.hasAttribute('aria-labelledby')) return;
|
|
@@ -83,6 +112,8 @@ function bindComboboxLifecycle({
|
|
|
83
112
|
onDocClick,
|
|
84
113
|
resetActive,
|
|
85
114
|
}) {
|
|
115
|
+
const doc = box.ownerDocument;
|
|
116
|
+
if (!doc) return noop;
|
|
86
117
|
const state = rememberState();
|
|
87
118
|
const listId = assignListId();
|
|
88
119
|
syncOptions();
|
|
@@ -114,7 +145,7 @@ function bindComboboxLifecycle({
|
|
|
114
145
|
input.addEventListener('input', onInput);
|
|
115
146
|
input.addEventListener('keydown', onKey);
|
|
116
147
|
list.addEventListener('click', onOptionClick);
|
|
117
|
-
|
|
148
|
+
doc.addEventListener('click', onDocClick);
|
|
118
149
|
// Opt-in: keep options in sync with a list mutated after init (async /
|
|
119
150
|
// remote results). Off by default so the common static case stays free.
|
|
120
151
|
const observer = liveOptionObserver(box, list, relist);
|
|
@@ -123,7 +154,7 @@ function bindComboboxLifecycle({
|
|
|
123
154
|
input.removeEventListener('input', onInput);
|
|
124
155
|
input.removeEventListener('keydown', onKey);
|
|
125
156
|
list.removeEventListener('click', onOptionClick);
|
|
126
|
-
|
|
157
|
+
doc.removeEventListener('click', onDocClick);
|
|
127
158
|
restoreState(state);
|
|
128
159
|
resetActive();
|
|
129
160
|
};
|
|
@@ -146,7 +177,7 @@ function bindComboboxLifecycle({
|
|
|
146
177
|
* optional `data-value`). An optional `.ui-combobox__empty` (hidden at rest)
|
|
147
178
|
* shows when nothing matches. The behavior owns ids, `aria-expanded`,
|
|
148
179
|
* `aria-controls`, `aria-activedescendant`, roving active option,
|
|
149
|
-
* type-to-filter,
|
|
180
|
+
* type-to-filter, keyboard list navigation (Down/Up/Enter/Escape/Tab),
|
|
150
181
|
* pointer select, and outside-click close. On select the **visible input shows
|
|
151
182
|
* the option's text label**, while the emitted `bronto:change` CustomEvent
|
|
152
183
|
* carries the option's `data-value` code: `{ detail: { value, label } }` (value
|
|
@@ -181,6 +212,8 @@ export function initCombobox({ root } = {}) {
|
|
|
181
212
|
const empty = box.querySelector('.ui-combobox__empty');
|
|
182
213
|
const optionStates = new WeakMap();
|
|
183
214
|
let listId = '';
|
|
215
|
+
const optionIdBase = `bronto-cb-opt-${nextFieldUid()}`;
|
|
216
|
+
const locale = localeOf(box);
|
|
184
217
|
|
|
185
218
|
const rememberOptionState = (option) => {
|
|
186
219
|
if (optionStates.has(option)) return;
|
|
@@ -202,7 +235,7 @@ export function initCombobox({ root } = {}) {
|
|
|
202
235
|
]),
|
|
203
236
|
list: {
|
|
204
237
|
hidden: list.hidden,
|
|
205
|
-
attrs: snapshotAttrs(list, ['id', 'role', 'aria-label']),
|
|
238
|
+
attrs: snapshotAttrs(list, ['id', 'role', 'aria-label', 'aria-labelledby']),
|
|
206
239
|
},
|
|
207
240
|
empty: empty
|
|
208
241
|
? {
|
|
@@ -238,7 +271,7 @@ export function initCombobox({ root } = {}) {
|
|
|
238
271
|
options = [...list.querySelectorAll(COMBOBOX_OPTION_SELECTOR)];
|
|
239
272
|
options.forEach((o, i) => {
|
|
240
273
|
rememberOptionState(o);
|
|
241
|
-
if (!o.id) o.id = `${
|
|
274
|
+
if (!o.id) o.id = `${optionIdBase}-${i}`;
|
|
242
275
|
o.setAttribute('role', 'option');
|
|
243
276
|
});
|
|
244
277
|
};
|
|
@@ -270,10 +303,10 @@ export function initCombobox({ root } = {}) {
|
|
|
270
303
|
};
|
|
271
304
|
|
|
272
305
|
const filter = () => {
|
|
273
|
-
const q = input.value.trim()
|
|
306
|
+
const q = lowerForSearch(input.value.trim(), locale);
|
|
274
307
|
let any = false;
|
|
275
308
|
for (const o of options) {
|
|
276
|
-
const match = !q || o.textContent
|
|
309
|
+
const match = !q || lowerForSearch(o.textContent, locale).includes(q);
|
|
277
310
|
o.hidden = !match;
|
|
278
311
|
if (match) any = true;
|
|
279
312
|
}
|
|
@@ -313,14 +346,6 @@ export function initCombobox({ root } = {}) {
|
|
|
313
346
|
active = options.indexOf(vis[next]);
|
|
314
347
|
setActive(options[active]);
|
|
315
348
|
};
|
|
316
|
-
const activateEdge = (which) => {
|
|
317
|
-
if (list.hidden) return false;
|
|
318
|
-
const v = visible();
|
|
319
|
-
if (!v.length) return true;
|
|
320
|
-
active = options.indexOf(which === 'first' ? v[0] : v[v.length - 1]);
|
|
321
|
-
setActive(options[active]);
|
|
322
|
-
return true;
|
|
323
|
-
};
|
|
324
349
|
const selectActive = () => {
|
|
325
350
|
if (list.hidden || active < 0 || options[active].hidden) return false;
|
|
326
351
|
select(options[active]);
|
|
@@ -356,8 +381,6 @@ export function initCombobox({ root } = {}) {
|
|
|
356
381
|
move(-1);
|
|
357
382
|
return true;
|
|
358
383
|
},
|
|
359
|
-
Home: () => activateEdge('first'),
|
|
360
|
-
End: () => activateEdge('last'),
|
|
361
384
|
Enter: () => selectActive(),
|
|
362
385
|
Escape: () => closeIfOpen(),
|
|
363
386
|
Tab: () => {
|
package/behaviors/command.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `data-value`), interleaved with `.ui-command__group` labels and an optional
|
|
17
17
|
* `.ui-command__empty`. The behavior owns ids, `role=combobox/listbox/option`,
|
|
18
18
|
* `aria-activedescendant`, a roving active item, substring filtering (hiding
|
|
19
|
-
* empty groups),
|
|
19
|
+
* empty groups), keyboard list navigation (Down/Up/Enter/Escape), and pointer
|
|
20
20
|
* select. It emits `bronto:command:select` ({ detail: { value, label } }) on
|
|
21
21
|
* choose and `bronto:command:close` on Escape. SSR-safe, idempotent per
|
|
22
22
|
* instance; returns a cleanup function.
|
|
@@ -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":"AA6BA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAiP3C;;;;;WA3Qa,MAAM;;;;WACN,MAAM"}
|
package/behaviors/command.js
CHANGED
|
@@ -10,6 +10,23 @@ import {
|
|
|
10
10
|
closestSafe,
|
|
11
11
|
} from './internal.js';
|
|
12
12
|
|
|
13
|
+
const localeOf = (el) => {
|
|
14
|
+
const locale =
|
|
15
|
+
closestSafe(el, '[lang]')?.getAttribute('lang')?.trim() ||
|
|
16
|
+
el?.ownerDocument?.documentElement?.getAttribute('lang')?.trim();
|
|
17
|
+
return locale || undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const lowerForSearch = (value, locale) => {
|
|
21
|
+
const text = String(value ?? '');
|
|
22
|
+
if (!locale) return text.toLowerCase();
|
|
23
|
+
try {
|
|
24
|
+
return text.toLocaleLowerCase(locale);
|
|
25
|
+
} catch {
|
|
26
|
+
return text.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
13
30
|
/**
|
|
14
31
|
* @typedef {object} CommandSelectDetail
|
|
15
32
|
* @property {string} value The chosen command's value.
|
|
@@ -29,7 +46,7 @@ import {
|
|
|
29
46
|
* `data-value`), interleaved with `.ui-command__group` labels and an optional
|
|
30
47
|
* `.ui-command__empty`. The behavior owns ids, `role=combobox/listbox/option`,
|
|
31
48
|
* `aria-activedescendant`, a roving active item, substring filtering (hiding
|
|
32
|
-
* empty groups),
|
|
49
|
+
* empty groups), keyboard list navigation (Down/Up/Enter/Escape), and pointer
|
|
33
50
|
* select. It emits `bronto:command:select` ({ detail: { value, label } }) on
|
|
34
51
|
* choose and `bronto:command:close` on Escape. SSR-safe, idempotent per
|
|
35
52
|
* instance; returns a cleanup function.
|
|
@@ -65,6 +82,31 @@ export function initCommand({ root } = {}) {
|
|
|
65
82
|
}
|
|
66
83
|
};
|
|
67
84
|
|
|
85
|
+
const firstTextNode = (el) => {
|
|
86
|
+
for (const node of el.childNodes) {
|
|
87
|
+
if (node.nodeType === 3 && node.nodeValue.trim()) return node;
|
|
88
|
+
if (node.nodeType === 1) {
|
|
89
|
+
const child = firstTextNode(node);
|
|
90
|
+
if (child) return child;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const refreshLiveText = (el) => {
|
|
97
|
+
const node = firstTextNode(el);
|
|
98
|
+
if (!node) return;
|
|
99
|
+
const text = node.nodeValue;
|
|
100
|
+
node.nodeValue = '';
|
|
101
|
+
node.nodeValue = text;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const prepareEmptyState = (el) => {
|
|
105
|
+
if (!el) return;
|
|
106
|
+
el.setAttribute('role', 'status');
|
|
107
|
+
el.setAttribute('aria-live', 'polite');
|
|
108
|
+
};
|
|
109
|
+
|
|
68
110
|
for (const box of palettes) {
|
|
69
111
|
const input = box.querySelector('.ui-command__input, input');
|
|
70
112
|
const list = box.querySelector('.ui-command__list, [role="listbox"]');
|
|
@@ -72,6 +114,7 @@ export function initCommand({ root } = {}) {
|
|
|
72
114
|
const empty = box.querySelector('.ui-command__empty');
|
|
73
115
|
const items = [...list.querySelectorAll('.ui-command__item, [role="option"]')];
|
|
74
116
|
const groups = [...list.querySelectorAll('.ui-command__group')];
|
|
117
|
+
const locale = localeOf(box);
|
|
75
118
|
|
|
76
119
|
const rememberState = () => ({
|
|
77
120
|
input: snapshotAttrs(input, [
|
|
@@ -83,7 +126,12 @@ export function initCommand({ root } = {}) {
|
|
|
83
126
|
'autocomplete',
|
|
84
127
|
]),
|
|
85
128
|
list: snapshotAttrs(list, ['id', 'role']),
|
|
86
|
-
empty: empty
|
|
129
|
+
empty: empty
|
|
130
|
+
? {
|
|
131
|
+
hidden: empty.hidden,
|
|
132
|
+
attrs: snapshotAttrs(empty, ['role', 'aria-live']),
|
|
133
|
+
}
|
|
134
|
+
: null,
|
|
87
135
|
groups: groups.map((g) => ({
|
|
88
136
|
el: g,
|
|
89
137
|
hidden: g.hidden,
|
|
@@ -100,7 +148,10 @@ export function initCommand({ root } = {}) {
|
|
|
100
148
|
const restoreState = (state) => {
|
|
101
149
|
restoreAttrs(input, state.input);
|
|
102
150
|
restoreAttrs(list, state.list);
|
|
103
|
-
if (empty && state.empty)
|
|
151
|
+
if (empty && state.empty) {
|
|
152
|
+
empty.hidden = state.empty.hidden;
|
|
153
|
+
restoreAttrs(empty, state.empty.attrs);
|
|
154
|
+
}
|
|
104
155
|
for (const group of state.groups) {
|
|
105
156
|
group.el.hidden = group.hidden;
|
|
106
157
|
restoreAttrs(group.el, group.attrs);
|
|
@@ -146,15 +197,18 @@ export function initCommand({ root } = {}) {
|
|
|
146
197
|
};
|
|
147
198
|
|
|
148
199
|
const filter = () => {
|
|
149
|
-
const q = input.value.trim()
|
|
200
|
+
const q = lowerForSearch(input.value.trim(), locale);
|
|
150
201
|
let any = false;
|
|
151
202
|
for (const it of items) {
|
|
152
|
-
const match = !q || it.textContent
|
|
203
|
+
const match = !q || lowerForSearch(it.textContent, locale).includes(q);
|
|
153
204
|
it.hidden = !match;
|
|
154
205
|
if (match) any = true;
|
|
155
206
|
}
|
|
156
207
|
syncGroups();
|
|
157
|
-
if (empty)
|
|
208
|
+
if (empty) {
|
|
209
|
+
empty.hidden = any;
|
|
210
|
+
if (!any) refreshLiveText(empty);
|
|
211
|
+
}
|
|
158
212
|
const vis = visible();
|
|
159
213
|
setActive(vis[0] || null);
|
|
160
214
|
};
|
|
@@ -191,22 +245,6 @@ export function initCommand({ root } = {}) {
|
|
|
191
245
|
e.preventDefault();
|
|
192
246
|
move(-1);
|
|
193
247
|
break;
|
|
194
|
-
case 'Home': {
|
|
195
|
-
const v = visible();
|
|
196
|
-
if (v.length) {
|
|
197
|
-
setActive(v[0]);
|
|
198
|
-
e.preventDefault();
|
|
199
|
-
}
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
case 'End': {
|
|
203
|
-
const v = visible();
|
|
204
|
-
if (v.length) {
|
|
205
|
-
setActive(v[v.length - 1]);
|
|
206
|
-
e.preventDefault();
|
|
207
|
-
}
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
248
|
case 'Enter':
|
|
211
249
|
if (active >= 0 && !items[active].hidden) {
|
|
212
250
|
choose(items[active]);
|
|
@@ -228,12 +266,14 @@ export function initCommand({ root } = {}) {
|
|
|
228
266
|
const bound = bindOnce(box, 'command', () => {
|
|
229
267
|
const state = rememberState();
|
|
230
268
|
const listId = list.id || (list.id = `bronto-cmd-${nextFieldUid()}`);
|
|
269
|
+
const optionIdBase = `bronto-cmd-opt-${nextFieldUid()}`;
|
|
231
270
|
items.forEach((it, i) => {
|
|
232
|
-
if (!it.id) it.id = `${
|
|
271
|
+
if (!it.id) it.id = `${optionIdBase}-${i}`;
|
|
233
272
|
it.setAttribute('role', 'option');
|
|
234
273
|
});
|
|
235
274
|
groups.forEach((g) => g.setAttribute('role', 'presentation'));
|
|
236
275
|
list.setAttribute('role', 'listbox');
|
|
276
|
+
prepareEmptyState(empty);
|
|
237
277
|
input.setAttribute('role', 'combobox');
|
|
238
278
|
input.setAttribute('aria-controls', listId);
|
|
239
279
|
input.setAttribute('aria-autocomplete', 'list');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["connectors.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["connectors.js"],"names":[],"mappings":"AAuHA;;;;;;;;;;;;;;GAcG;AACH,0CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA2K3C"}
|