@ponchia/ui 0.6.10 → 0.6.12
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 +108 -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 +13 -13
- package/behaviors/modal.d.ts.map +1 -1
- package/behaviors/modal.js +283 -96
- package/behaviors/popover.d.ts.map +1 -1
- package/behaviors/popover.js +58 -8
- 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 +24 -0
- package/classes/index.d.ts +3 -2
- package/classes/index.js +77 -39
- package/classes/vscode.css-custom-data.json +1 -1
- 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 +9 -14
- 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/docs/architecture.md +6 -4
- 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 +1 -1
- package/docs/reporting.md +8 -8
- package/docs/selection.md +9 -0
- package/docs/sources.md +5 -0
- package/docs/stability.md +31 -6
- 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 +12 -2
- package/docs/vega.md +5 -3
- 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/figma.variables.json +2 -2
- package/tokens/index.js +1 -1
- package/tokens/index.json +2 -2
- package/tokens/resolved.json +1 -1
- package/tokens/tokens.dtcg.json +1 -1
- 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/popover.js
CHANGED
|
@@ -68,13 +68,59 @@ export function initPopover({ root } = {}) {
|
|
|
68
68
|
if (!hasDom()) return noop;
|
|
69
69
|
const host = resolveHost(root);
|
|
70
70
|
if (!host) return noop;
|
|
71
|
-
const
|
|
71
|
+
const doc = host.nodeType === 9 ? host : host.ownerDocument;
|
|
72
|
+
if (!doc) return noop;
|
|
73
|
+
const view = doc.defaultView;
|
|
72
74
|
const GAP = 8;
|
|
73
75
|
let openPanel = null;
|
|
74
76
|
let openTrigger = null;
|
|
77
|
+
let pendingFocusObserver = null;
|
|
75
78
|
const triggerStates = new Map();
|
|
76
79
|
const panelStates = new Map();
|
|
77
80
|
|
|
81
|
+
const cancelPendingFocus = () => {
|
|
82
|
+
pendingFocusObserver?.disconnect();
|
|
83
|
+
pendingFocusObserver = null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const focusWhenInteractive = (panel) => {
|
|
87
|
+
cancelPendingFocus();
|
|
88
|
+
if (!panel.inert) {
|
|
89
|
+
focusInto(panel);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A panel portaled out of a controlled modal starts in the trapped
|
|
94
|
+
// background. Wait for initModal to admit that specific panel instead of
|
|
95
|
+
// guessing how many microtasks its MutationObserver reconciliation needs.
|
|
96
|
+
const Observer = view?.MutationObserver;
|
|
97
|
+
if (!Observer) {
|
|
98
|
+
queueMicrotask(() => {
|
|
99
|
+
if (openPanel === panel && !panel.inert) focusInto(panel);
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const focusIfReady = () => {
|
|
104
|
+
if (openPanel !== panel) {
|
|
105
|
+
cancelPendingFocus();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (panel.inert) return;
|
|
109
|
+
cancelPendingFocus();
|
|
110
|
+
focusInto(panel);
|
|
111
|
+
};
|
|
112
|
+
pendingFocusObserver = new Observer(focusIfReady);
|
|
113
|
+
pendingFocusObserver.observe(panel, { attributes: true, attributeFilter: ['inert'] });
|
|
114
|
+
// Some DOM implementations model `inert` as a property without reflecting
|
|
115
|
+
// its attribute, so no observer record is delivered. Cover that test-DOM
|
|
116
|
+
// path across the modal reconciler's queued microtask; real browsers still
|
|
117
|
+
// use the attribute observer as the source of truth.
|
|
118
|
+
queueMicrotask(() => {
|
|
119
|
+
focusIfReady();
|
|
120
|
+
if (openPanel === panel && panel.inert) queueMicrotask(focusIfReady);
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
78
124
|
const rememberTrigger = (trigger) => {
|
|
79
125
|
if (!triggerStates.has(trigger)) {
|
|
80
126
|
triggerStates.set(
|
|
@@ -121,13 +167,14 @@ export function initPopover({ root } = {}) {
|
|
|
121
167
|
};
|
|
122
168
|
|
|
123
169
|
const close = () => {
|
|
170
|
+
cancelPendingFocus();
|
|
124
171
|
if (!openPanel) return;
|
|
125
172
|
const panel = openPanel;
|
|
126
173
|
const trigger = openTrigger;
|
|
127
174
|
// Only steal focus back to the trigger when focus is still inside the panel
|
|
128
175
|
// (Escape / programmatic re-toggle). An outside-click leaves focus where the
|
|
129
176
|
// click landed — deliberate intent to move on, per the doc contract.
|
|
130
|
-
const focusWasInside = panel.contains(
|
|
177
|
+
const focusWasInside = panel.contains(doc.activeElement);
|
|
131
178
|
openPanel = openTrigger = null;
|
|
132
179
|
if (panel.hasAttribute('popover') && typeof panel.hidePopover === 'function') {
|
|
133
180
|
try {
|
|
@@ -164,7 +211,7 @@ export function initPopover({ root } = {}) {
|
|
|
164
211
|
openPanel = panel;
|
|
165
212
|
openTrigger = trigger;
|
|
166
213
|
place(trigger, panel);
|
|
167
|
-
|
|
214
|
+
focusWhenInteractive(panel);
|
|
168
215
|
};
|
|
169
216
|
|
|
170
217
|
const onClick = (e) => {
|
|
@@ -225,7 +272,10 @@ export function initPopover({ root } = {}) {
|
|
|
225
272
|
const onToggle = (e) => {
|
|
226
273
|
const isOpen = e.newState === 'open';
|
|
227
274
|
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
228
|
-
if (!isOpen && openPanel === panel)
|
|
275
|
+
if (!isOpen && openPanel === panel) {
|
|
276
|
+
cancelPendingFocus();
|
|
277
|
+
openPanel = openTrigger = null;
|
|
278
|
+
}
|
|
229
279
|
};
|
|
230
280
|
panel.addEventListener('toggle', onToggle);
|
|
231
281
|
seedTeardowns.push(() => panel.removeEventListener('toggle', onToggle));
|
|
@@ -235,8 +285,8 @@ export function initPopover({ root } = {}) {
|
|
|
235
285
|
|
|
236
286
|
return bindOnce(host, 'popover', () => {
|
|
237
287
|
seed();
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
doc.addEventListener('click', onClick);
|
|
289
|
+
doc.addEventListener('keydown', onKey);
|
|
240
290
|
view?.addEventListener('scroll', onReflow, true);
|
|
241
291
|
view?.addEventListener('resize', onReflow);
|
|
242
292
|
return () => {
|
|
@@ -250,8 +300,8 @@ export function initPopover({ root } = {}) {
|
|
|
250
300
|
restoreStyle(panel, state.style);
|
|
251
301
|
}
|
|
252
302
|
panelStates.clear();
|
|
253
|
-
|
|
254
|
-
|
|
303
|
+
doc.removeEventListener('click', onClick);
|
|
304
|
+
doc.removeEventListener('keydown', onKey);
|
|
255
305
|
view?.removeEventListener('scroll', onReflow, true);
|
|
256
306
|
view?.removeEventListener('resize', onReflow);
|
|
257
307
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"AAqDA;;;;;;;;;;;GAWG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+H3C;;;;;QAlLa,MAAM;;;;cACN,OAAO;;;;YACP,OAAO"}
|
package/behaviors/sources.js
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
resolveHost,
|
|
4
4
|
noop,
|
|
5
5
|
bindOnce,
|
|
6
|
-
byIdInHost,
|
|
7
6
|
collectHosts,
|
|
8
7
|
scrollIntoViewSafe,
|
|
9
8
|
closestSafe,
|
|
@@ -42,6 +41,16 @@ function sourcePreview(source) {
|
|
|
42
41
|
return [title, meta, excerpt].filter(Boolean).join(' — ');
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
function idMapFor(island) {
|
|
45
|
+
const ids = new Map();
|
|
46
|
+
const add = (el) => {
|
|
47
|
+
if (el?.id && !ids.has(el.id)) ids.set(el.id, el);
|
|
48
|
+
};
|
|
49
|
+
add(island);
|
|
50
|
+
for (const el of island.querySelectorAll?.('[id]') || []) add(el);
|
|
51
|
+
return ids;
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
/**
|
|
46
55
|
* Source/citation affordances for the `sources.css` trust layer. The behavior
|
|
47
56
|
* is deliberately small: within each `[data-bronto-sources]` island it resolves
|
|
@@ -66,11 +75,23 @@ export function initSources({ root } = {}) {
|
|
|
66
75
|
const timers = new Set();
|
|
67
76
|
const seeded = [];
|
|
68
77
|
const activeSources = new Set();
|
|
78
|
+
const sourcesById = idMapFor(island);
|
|
79
|
+
const previewBySource = new WeakMap();
|
|
69
80
|
|
|
70
81
|
const targetFor = (ref) => {
|
|
71
82
|
const id = sourceId(ref);
|
|
72
83
|
if (!id) return null;
|
|
73
|
-
|
|
84
|
+
const source = sourcesById.get(id) || null;
|
|
85
|
+
return source && island.contains(source) ? source : null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const previewFor = (source) => {
|
|
89
|
+
let preview = previewBySource.get(source);
|
|
90
|
+
if (preview === undefined) {
|
|
91
|
+
preview = sourcePreview(source);
|
|
92
|
+
previewBySource.set(source, preview);
|
|
93
|
+
}
|
|
94
|
+
return preview;
|
|
74
95
|
};
|
|
75
96
|
|
|
76
97
|
const seed = () => {
|
|
@@ -81,7 +102,7 @@ export function initSources({ root } = {}) {
|
|
|
81
102
|
const describedBy = ref.getAttribute('aria-describedby') || '';
|
|
82
103
|
const describedIds = describedBy.split(/\s+/).filter(Boolean);
|
|
83
104
|
const title = ref.getAttribute('title');
|
|
84
|
-
const preview =
|
|
105
|
+
const preview = previewFor(source);
|
|
85
106
|
const prior = {
|
|
86
107
|
ref,
|
|
87
108
|
describedBy,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AA6OA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WAvPa,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
|
package/behaviors/splitter.js
CHANGED
|
@@ -14,7 +14,16 @@ const LARGE_STEP = 10;
|
|
|
14
14
|
* @property {'vertical' | 'horizontal'} orientation Splitter orientation.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
const NUMBER_RE = /^[-+]?(?:(?:\d+\.?\d*)|\.\d+)(?:e[-+]?\d+)?$/i;
|
|
18
|
+
|
|
17
19
|
const num = (v, fallback) => {
|
|
20
|
+
const value = String(v ?? '').trim();
|
|
21
|
+
if (!NUMBER_RE.test(value)) return fallback;
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
return Number.isFinite(n) ? n : fallback;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const cssNum = (v, fallback) => {
|
|
18
27
|
const n = Number.parseFloat(String(v ?? '').trim());
|
|
19
28
|
return Number.isFinite(n) ? n : fallback;
|
|
20
29
|
};
|
|
@@ -26,7 +35,7 @@ const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
|
|
|
26
35
|
const readCssValue = (splitter) => splitter.style.getPropertyValue('--splitter-pos');
|
|
27
36
|
|
|
28
37
|
const readOrientation = (splitter, handle) => {
|
|
29
|
-
const data = splitter.getAttribute('data-bronto-splitter');
|
|
38
|
+
const data = splitter.getAttribute('data-bronto-splitter')?.trim();
|
|
30
39
|
if (data === 'horizontal' || data === 'vertical') return data;
|
|
31
40
|
if (splitter.classList.contains('ui-splitter--horizontal')) return 'horizontal';
|
|
32
41
|
if (splitter.classList.contains('ui-splitter--vertical')) return 'vertical';
|
|
@@ -87,13 +96,25 @@ function wireSplitter(splitter) {
|
|
|
87
96
|
]);
|
|
88
97
|
const splitterPos = snapshotStyleProp(splitter, '--splitter-pos');
|
|
89
98
|
const orientation = readOrientation(splitter, handle);
|
|
90
|
-
const
|
|
91
|
-
const
|
|
99
|
+
const minAttr = handle.getAttribute('aria-valuemin');
|
|
100
|
+
const maxAttr = handle.getAttribute('aria-valuemax');
|
|
101
|
+
const min = num(minAttr, DEFAULT_MIN);
|
|
102
|
+
const max = Math.max(min, num(maxAttr, DEFAULT_MAX));
|
|
92
103
|
let value = clamp(
|
|
93
|
-
num(handle.getAttribute('aria-valuenow'),
|
|
104
|
+
num(handle.getAttribute('aria-valuenow'), cssNum(readCssValue(splitter), DEFAULT_VALUE)),
|
|
94
105
|
min,
|
|
95
106
|
max,
|
|
96
107
|
);
|
|
108
|
+
const syncRangeAttr = (name, authored, normalized) => {
|
|
109
|
+
const parsed = num(authored, Number.NaN);
|
|
110
|
+
if (
|
|
111
|
+
authored === null ||
|
|
112
|
+
!Number.isFinite(parsed) ||
|
|
113
|
+
String(authored).trim() !== fmt(normalized)
|
|
114
|
+
) {
|
|
115
|
+
handle.setAttribute(name, fmt(normalized));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
97
118
|
let activePointer = null;
|
|
98
119
|
|
|
99
120
|
const apply = (next, { emit = true } = {}) => {
|
|
@@ -108,8 +129,8 @@ function wireSplitter(splitter) {
|
|
|
108
129
|
if (!handle.hasAttribute('tabindex')) handle.tabIndex = 0;
|
|
109
130
|
if (!handle.hasAttribute('aria-orientation'))
|
|
110
131
|
handle.setAttribute('aria-orientation', orientation);
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
syncRangeAttr('aria-valuemin', minAttr, min);
|
|
133
|
+
syncRangeAttr('aria-valuemax', maxAttr, max);
|
|
113
134
|
apply(value, { emit: false });
|
|
114
135
|
|
|
115
136
|
const fromPointer = (event) => {
|
package/behaviors/table.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0P3C"}
|
package/behaviors/table.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
|
|
2
2
|
|
|
3
|
+
const localeOf = (el) => {
|
|
4
|
+
const locale =
|
|
5
|
+
closestSafe(el, '[lang]')?.getAttribute('lang')?.trim() ||
|
|
6
|
+
el?.ownerDocument?.documentElement?.getAttribute('lang')?.trim();
|
|
7
|
+
return locale || undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const textComparatorFor = (el) => {
|
|
11
|
+
const options = { sensitivity: 'base', numeric: false };
|
|
12
|
+
if (typeof Intl === 'undefined' || typeof Intl.Collator !== 'function') {
|
|
13
|
+
return (a, b) => a.localeCompare(b);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return new Intl.Collator(localeOf(el), options).compare;
|
|
17
|
+
} catch {
|
|
18
|
+
return new Intl.Collator(undefined, options).compare;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
3
22
|
/**
|
|
4
23
|
* Client-side sortable + selectable data table. Wires
|
|
5
24
|
* `[data-bronto-sortable]`:
|
|
@@ -71,6 +90,8 @@ export function initTableSort({ root } = {}) {
|
|
|
71
90
|
const rows = [];
|
|
72
91
|
const checkboxStates = new WeakMap();
|
|
73
92
|
const checkboxes = [];
|
|
93
|
+
const misplacedSorters = new WeakSet();
|
|
94
|
+
const compareText = textComparatorFor(table);
|
|
74
95
|
|
|
75
96
|
const rememberHeaderState = (th) => {
|
|
76
97
|
if (!th || headerStates.has(th)) return;
|
|
@@ -134,6 +155,15 @@ export function initTableSort({ root } = {}) {
|
|
|
134
155
|
|
|
135
156
|
const colIndex = (th) => [...th.parentElement.children].indexOf(th);
|
|
136
157
|
const cellText = (row, i) => row.children[i]?.textContent.trim() ?? '';
|
|
158
|
+
const warnMisplacedSorter = (sorter) => {
|
|
159
|
+
if (misplacedSorters.has(sorter)) return;
|
|
160
|
+
misplacedSorters.add(sorter);
|
|
161
|
+
if (typeof console !== 'undefined') {
|
|
162
|
+
console.warn(
|
|
163
|
+
'[bronto] initTableSort(): .ui-table__sort must be placed inside a <th>; ignoring this sorter.',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
137
167
|
// Numeric value of a cell for sorting. A `data-sort-value` attribute is the
|
|
138
168
|
// authoritative escape hatch; otherwise normalize the display text so the
|
|
139
169
|
// sign survives (U+2212 / en-em dashes → minus, accounting parens →
|
|
@@ -143,7 +173,7 @@ export function initTableSort({ root } = {}) {
|
|
|
143
173
|
const cell = row.children[i];
|
|
144
174
|
const explicit = cell?.getAttribute?.('data-sort-value');
|
|
145
175
|
if (explicit != null && explicit.trim() !== '') {
|
|
146
|
-
const raw = explicit.trim();
|
|
176
|
+
const raw = explicit.trim().replace(/[−–—]/g, '-');
|
|
147
177
|
let v = Number(raw);
|
|
148
178
|
// The escape hatch must actually handle the case the doc names it for: a
|
|
149
179
|
// European decimal comma. `Number("3,5")` is NaN, which silently fell
|
|
@@ -184,15 +214,18 @@ export function initTableSort({ root } = {}) {
|
|
|
184
214
|
// or after a sort they float above the real rows.
|
|
185
215
|
const emptyRows = [...tbody.rows].filter((r) => r.classList.contains('ui-table__empty'));
|
|
186
216
|
const rows = [...tbody.rows].filter((r) => !r.classList.contains('ui-table__empty'));
|
|
187
|
-
rows.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
const keyedRows = rows.map((row, index) => ({
|
|
218
|
+
row,
|
|
219
|
+
index,
|
|
220
|
+
key: numeric ? cellNum(row, i) : cellText(row, i),
|
|
221
|
+
}));
|
|
222
|
+
keyedRows.sort((a, b) => {
|
|
223
|
+
const cmp = numeric ? a.key - b.key : compareText(a.key, b.key);
|
|
224
|
+
return cmp === 0 ? a.index - b.index : cmp * sign;
|
|
192
225
|
});
|
|
193
226
|
// Re-parent in document order: sorted data rows, then any empty/sentinel
|
|
194
227
|
// row last. These are existing <tr> nodes being moved; no markup is parsed.
|
|
195
|
-
tbody.append(...
|
|
228
|
+
tbody.append(...keyedRows.map(({ row }) => row), ...emptyRows);
|
|
196
229
|
};
|
|
197
230
|
|
|
198
231
|
const allBox = table.querySelector('[data-bronto-select-all]');
|
|
@@ -227,6 +260,10 @@ export function initTableSort({ root } = {}) {
|
|
|
227
260
|
if (sorter && table.contains(sorter)) {
|
|
228
261
|
rememberSorterState(sorter);
|
|
229
262
|
const th = sorter.closest('th');
|
|
263
|
+
if (!th) {
|
|
264
|
+
warnMisplacedSorter(sorter);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
230
267
|
const numeric =
|
|
231
268
|
(sorter.getAttribute('data-sort') || th.getAttribute('data-sort')) === 'num' ||
|
|
232
269
|
th.classList.contains('is-num');
|
package/behaviors/tabs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAoL3C"}
|
package/behaviors/tabs.js
CHANGED
|
@@ -57,6 +57,17 @@ export function initTabs({ root } = {}) {
|
|
|
57
57
|
const panels = [...group.querySelectorAll('.ui-tabs__panel')].filter(owned);
|
|
58
58
|
if (!tabs.length) continue;
|
|
59
59
|
const list = [...group.querySelectorAll('.ui-tabs__list')].find(owned);
|
|
60
|
+
const isNativeDisabled = (tab) => {
|
|
61
|
+
try {
|
|
62
|
+
if (tab.matches?.(':disabled')) return true;
|
|
63
|
+
} catch {
|
|
64
|
+
/* fall through to the native property */
|
|
65
|
+
}
|
|
66
|
+
return Boolean('disabled' in tab && tab.disabled);
|
|
67
|
+
};
|
|
68
|
+
const isReachable = (tab) => !tab.hidden && !isNativeDisabled(tab);
|
|
69
|
+
const isAriaDisabled = (tab) => tab.getAttribute('aria-disabled') === 'true';
|
|
70
|
+
const reachableTabs = () => tabs.filter(isReachable);
|
|
60
71
|
const rememberState = () => ({
|
|
61
72
|
list: list ? snapshotAttrs(list, ['role']) : null,
|
|
62
73
|
tabs: new Map(
|
|
@@ -94,17 +105,31 @@ export function initTabs({ root } = {}) {
|
|
|
94
105
|
}
|
|
95
106
|
};
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
let selectedTab = null;
|
|
109
|
+
const syncTabs = (selected, tabStop) => {
|
|
98
110
|
for (const t of tabs) {
|
|
99
|
-
const on = t ===
|
|
111
|
+
const on = t === selected;
|
|
100
112
|
t.classList.toggle('is-active', on);
|
|
101
113
|
t.setAttribute('role', 'tab');
|
|
102
114
|
t.setAttribute('aria-selected', String(on));
|
|
103
|
-
t.tabIndex =
|
|
115
|
+
t.tabIndex = t === tabStop ? 0 : -1;
|
|
104
116
|
}
|
|
117
|
+
};
|
|
118
|
+
const moveTabStop = (tab) => {
|
|
119
|
+
const candidates = reachableTabs();
|
|
120
|
+
const next = candidates.includes(tab) ? tab : candidates[0] || null;
|
|
121
|
+
if (!next) return false;
|
|
122
|
+
syncTabs(selectedTab, next);
|
|
123
|
+
return true;
|
|
124
|
+
};
|
|
125
|
+
const select = (tab) => {
|
|
126
|
+
const candidates = reachableTabs();
|
|
127
|
+
if (!candidates.includes(tab) || isAriaDisabled(tab)) return false;
|
|
128
|
+
selectedTab = tab;
|
|
129
|
+
syncTabs(tab, tab);
|
|
105
130
|
// Only retarget panels when this tab actually controls one. A panel-less
|
|
106
131
|
// tab must not hide every panel; leave the prior panel visible.
|
|
107
|
-
if (!panels.some((p) => p.dataset.panel === tab.dataset.tab)) return;
|
|
132
|
+
if (!panels.some((p) => p.dataset.panel === tab.dataset.tab)) return true;
|
|
108
133
|
for (const p of panels) {
|
|
109
134
|
p.setAttribute('role', 'tabpanel');
|
|
110
135
|
const shown = p.dataset.panel === tab.dataset.tab;
|
|
@@ -114,6 +139,7 @@ export function initTabs({ root } = {}) {
|
|
|
114
139
|
if (shown) p.tabIndex = 0;
|
|
115
140
|
else p.removeAttribute('tabindex');
|
|
116
141
|
}
|
|
142
|
+
return true;
|
|
117
143
|
};
|
|
118
144
|
const onClick = (e) => {
|
|
119
145
|
// `tabs` is filtered to this group, so membership (not mere DOM
|
|
@@ -121,23 +147,28 @@ export function initTabs({ root } = {}) {
|
|
|
121
147
|
const tab = closestSafe(e.target, '.ui-tab');
|
|
122
148
|
if (tab && tabs.includes(tab)) {
|
|
123
149
|
e.preventDefault();
|
|
124
|
-
select(tab);
|
|
125
|
-
tab.focus();
|
|
150
|
+
const handled = select(tab) || moveTabStop(tab);
|
|
151
|
+
if (handled) tab.focus();
|
|
126
152
|
}
|
|
127
153
|
};
|
|
128
154
|
const onKey = (e) => {
|
|
129
|
-
const
|
|
155
|
+
const candidates = reachableTabs();
|
|
156
|
+
const i = candidates.indexOf(closestSafe(e.target, '.ui-tab'));
|
|
130
157
|
if (i < 0) return;
|
|
158
|
+
const orientation =
|
|
159
|
+
list?.getAttribute('aria-orientation') === 'vertical' ? 'vertical' : 'horizontal';
|
|
160
|
+
const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
161
|
+
const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
131
162
|
let n = i;
|
|
132
|
-
if (e.key ===
|
|
133
|
-
else if (e.key ===
|
|
134
|
-
n = (i - 1 + tabs.length) % tabs.length;
|
|
163
|
+
if (e.key === nextKey) n = (i + 1) % candidates.length;
|
|
164
|
+
else if (e.key === prevKey) n = (i - 1 + candidates.length) % candidates.length;
|
|
135
165
|
else if (e.key === 'Home') n = 0;
|
|
136
|
-
else if (e.key === 'End') n =
|
|
166
|
+
else if (e.key === 'End') n = candidates.length - 1;
|
|
137
167
|
else return;
|
|
138
168
|
e.preventDefault();
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
const next = candidates[n];
|
|
170
|
+
if (!select(next)) moveTabStop(next);
|
|
171
|
+
next.focus();
|
|
141
172
|
};
|
|
142
173
|
cleanups.push(
|
|
143
174
|
bindOnce(group, 'tabs', () => {
|
|
@@ -155,7 +186,13 @@ export function initTabs({ root } = {}) {
|
|
|
155
186
|
t.setAttribute('aria-controls', p.id);
|
|
156
187
|
p.setAttribute('aria-labelledby', t.id);
|
|
157
188
|
}
|
|
158
|
-
|
|
189
|
+
const candidates = reachableTabs();
|
|
190
|
+
const initial =
|
|
191
|
+
candidates.find((t) => t.classList.contains('is-active') && !isAriaDisabled(t)) ||
|
|
192
|
+
candidates.find((t) => !isAriaDisabled(t)) ||
|
|
193
|
+
candidates[0];
|
|
194
|
+
if (initial && !select(initial)) moveTabStop(initial);
|
|
195
|
+
else if (!initial) syncTabs(null, null);
|
|
159
196
|
group.addEventListener('click', onClick);
|
|
160
197
|
group.addEventListener('keydown', onKey);
|
|
161
198
|
return () => {
|
package/behaviors/theme.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"AAuBA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;GAcG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAgH3C;;;;;;;;;;6BAzJY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
|
package/behaviors/theme.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
|
|
2
2
|
|
|
3
3
|
const THEMES = ['light', 'dark'];
|
|
4
|
+
const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)';
|
|
5
|
+
|
|
6
|
+
const colorSchemeQuery = () =>
|
|
7
|
+
typeof matchMedia === 'function' ? matchMedia(DARK_SCHEME_QUERY) : null;
|
|
8
|
+
|
|
9
|
+
function onColorSchemeChange(query, listener) {
|
|
10
|
+
if (
|
|
11
|
+
typeof query?.addEventListener === 'function' &&
|
|
12
|
+
typeof query.removeEventListener === 'function'
|
|
13
|
+
) {
|
|
14
|
+
query.addEventListener('change', listener);
|
|
15
|
+
return () => query.removeEventListener('change', listener);
|
|
16
|
+
}
|
|
17
|
+
if (typeof query?.addListener === 'function' && typeof query.removeListener === 'function') {
|
|
18
|
+
query.addListener(listener);
|
|
19
|
+
return () => query.removeListener(listener);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
/**
|
|
6
25
|
* @typedef {object} ThemeStorageOpts
|
|
@@ -66,8 +85,12 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
|
|
|
66
85
|
}
|
|
67
86
|
};
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
let schemeQuery = null;
|
|
89
|
+
let removeSchemeListener = noop;
|
|
90
|
+
|
|
91
|
+
const hasExplicitTheme = () => THEMES.includes(docEl.getAttribute('data-theme'));
|
|
92
|
+
|
|
93
|
+
const prefersDark = () => (schemeQuery || colorSchemeQuery())?.matches === true;
|
|
71
94
|
|
|
72
95
|
const current = () => {
|
|
73
96
|
const attr = docEl.getAttribute('data-theme');
|
|
@@ -87,6 +110,38 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
|
|
|
87
110
|
});
|
|
88
111
|
};
|
|
89
112
|
|
|
113
|
+
const clearSchemeListener = () => {
|
|
114
|
+
removeSchemeListener();
|
|
115
|
+
removeSchemeListener = noop;
|
|
116
|
+
schemeQuery = null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const syncSchemeListener = () => {
|
|
120
|
+
if (hasExplicitTheme()) {
|
|
121
|
+
clearSchemeListener();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (schemeQuery) return;
|
|
125
|
+
const query = colorSchemeQuery();
|
|
126
|
+
const cleanup = onColorSchemeChange(query, onSchemeChange);
|
|
127
|
+
if (!cleanup) return;
|
|
128
|
+
schemeQuery = query;
|
|
129
|
+
removeSchemeListener = cleanup;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function onSchemeChange() {
|
|
133
|
+
if (hasExplicitTheme()) {
|
|
134
|
+
clearSchemeListener();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
reflect();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const onThemeChange = () => {
|
|
141
|
+
reflect();
|
|
142
|
+
syncSchemeListener();
|
|
143
|
+
};
|
|
144
|
+
|
|
90
145
|
const onClick = (e) => {
|
|
91
146
|
const trigger = closestSafe(e.target, '[data-bronto-theme-toggle]');
|
|
92
147
|
if (!trigger || !host.contains(trigger)) return;
|
|
@@ -94,6 +149,7 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
|
|
|
94
149
|
const forced = trigger.getAttribute('data-bronto-theme-toggle');
|
|
95
150
|
const next = THEMES.includes(forced) ? forced : current() === 'dark' ? 'light' : 'dark';
|
|
96
151
|
docEl.setAttribute('data-theme', next);
|
|
152
|
+
clearSchemeListener();
|
|
97
153
|
try {
|
|
98
154
|
localStorage.setItem(storageKey, next);
|
|
99
155
|
} catch {
|
|
@@ -105,12 +161,16 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
|
|
|
105
161
|
);
|
|
106
162
|
};
|
|
107
163
|
|
|
108
|
-
applyStoredTheme({ storageKey, root: docEl });
|
|
109
|
-
reflect();
|
|
110
164
|
return bindOnce(host, 'themeToggle', () => {
|
|
165
|
+
applyStoredTheme({ storageKey, root: docEl });
|
|
166
|
+
reflect();
|
|
167
|
+
syncSchemeListener();
|
|
168
|
+
docEl.addEventListener('bronto:themechange', onThemeChange);
|
|
111
169
|
host.addEventListener('click', onClick);
|
|
112
170
|
return () => {
|
|
171
|
+
docEl.removeEventListener('bronto:themechange', onThemeChange);
|
|
113
172
|
host.removeEventListener('click', onClick);
|
|
173
|
+
clearSchemeListener();
|
|
114
174
|
for (const [el, state] of toggleStates) {
|
|
115
175
|
if (state.had) el.setAttribute('aria-pressed', state.value);
|
|
116
176
|
else el.removeAttribute('aria-pressed');
|
package/behaviors/toast.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @property {number} [duration] Auto-dismiss delay in ms. `0` keeps it until dismissed. Default: `4000`.
|
|
6
6
|
* @property {boolean} [assertive] Route to the assertive live region so AT interrupts immediately. Defaults to `true` when `tone === 'danger'`.
|
|
7
7
|
* @property {boolean} [closable] Render a dismiss button on the toast.
|
|
8
|
+
* @property {string} [dismissLabel] Accessible label for the generated dismiss button. Default: `Dismiss`.
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
11
|
* Push a transient toast into a shared, screen-anchored stack. The stack
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
* @param {ToastOpts} [opts]
|
|
24
25
|
* @returns {import('./internal.js').Cleanup}
|
|
25
26
|
*/
|
|
26
|
-
export function toast(message: string, { tone, title, duration, assertive, closable }?: ToastOpts): import("./internal.js").Cleanup;
|
|
27
|
+
export function toast(message: string, { tone, title, duration, assertive, closable, dismissLabel }?: ToastOpts): import("./internal.js").Cleanup;
|
|
27
28
|
export type ToastOpts = {
|
|
28
29
|
/**
|
|
29
30
|
* Status tone — maps to `ui-toast--<tone>`.
|
|
@@ -45,5 +46,9 @@ export type ToastOpts = {
|
|
|
45
46
|
* Render a dismiss button on the toast.
|
|
46
47
|
*/
|
|
47
48
|
closable?: boolean | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Accessible label for the generated dismiss button. Default: `Dismiss`.
|
|
51
|
+
*/
|
|
52
|
+
dismissLabel?: string | undefined;
|
|
48
53
|
};
|
|
49
54
|
//# sourceMappingURL=toast.d.ts.map
|
package/behaviors/toast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["toast.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["toast.js"],"names":[],"mappings":"AAwJA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,+BAJW,MAAM,iEACN,SAAS,GACP,OAAO,eAAe,EAAE,OAAO,CAsD3C"}
|