@ponchia/ui 0.6.0 → 0.6.3
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 +64 -4
- package/README.md +1 -1
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +36 -33
- package/behaviors/carousel.d.ts +28 -0
- package/behaviors/carousel.d.ts.map +1 -0
- package/behaviors/carousel.js +3 -0
- package/behaviors/combobox.d.ts +40 -0
- package/behaviors/combobox.d.ts.map +1 -0
- package/behaviors/combobox.js +71 -20
- package/behaviors/command.d.ts +41 -0
- package/behaviors/command.d.ts.map +1 -0
- package/behaviors/command.js +9 -0
- package/behaviors/connectors.d.ts +17 -0
- package/behaviors/connectors.d.ts.map +1 -0
- package/behaviors/connectors.js +3 -0
- package/behaviors/crosshair.d.ts +42 -0
- package/behaviors/crosshair.d.ts.map +1 -0
- package/behaviors/crosshair.js +19 -1
- package/behaviors/dialog.d.ts +20 -0
- package/behaviors/dialog.d.ts.map +1 -0
- package/behaviors/dialog.js +3 -0
- package/behaviors/disclosure.d.ts +10 -0
- package/behaviors/disclosure.d.ts.map +1 -0
- package/behaviors/disclosure.js +3 -0
- package/behaviors/dismissible.d.ts +10 -0
- package/behaviors/dismissible.d.ts.map +1 -0
- package/behaviors/dismissible.js +3 -0
- package/behaviors/forms.d.ts +27 -0
- package/behaviors/forms.d.ts.map +1 -0
- package/behaviors/forms.js +18 -5
- package/behaviors/glyph.d.ts +14 -0
- package/behaviors/glyph.d.ts.map +1 -0
- package/behaviors/glyph.js +24 -0
- package/behaviors/index.d.ts +31 -237
- package/behaviors/index.d.ts.map +1 -0
- package/behaviors/index.js +17 -0
- package/behaviors/inert.d.ts +20 -0
- package/behaviors/inert.d.ts.map +1 -0
- package/behaviors/inert.js +46 -0
- package/behaviors/internal.d.ts +25 -0
- package/behaviors/internal.d.ts.map +1 -0
- package/behaviors/internal.js +30 -1
- package/behaviors/legend.d.ts +35 -0
- package/behaviors/legend.d.ts.map +1 -0
- package/behaviors/legend.js +9 -0
- package/behaviors/menu.d.ts +16 -0
- package/behaviors/menu.d.ts.map +1 -0
- package/behaviors/menu.js +3 -0
- package/behaviors/modal.d.ts +41 -0
- package/behaviors/modal.d.ts.map +1 -0
- package/behaviors/modal.js +124 -0
- package/behaviors/popover.d.ts +28 -0
- package/behaviors/popover.d.ts.map +1 -0
- package/behaviors/popover.js +17 -17
- package/behaviors/spotlight.d.ts +17 -0
- package/behaviors/spotlight.d.ts.map +1 -0
- package/behaviors/spotlight.js +3 -0
- package/behaviors/table.d.ts +36 -0
- package/behaviors/table.d.ts.map +1 -0
- package/behaviors/table.js +48 -8
- package/behaviors/tabs.d.ts +20 -0
- package/behaviors/tabs.d.ts.map +1 -0
- package/behaviors/tabs.js +3 -0
- package/behaviors/theme.d.ts +54 -0
- package/behaviors/theme.d.ts.map +1 -0
- package/behaviors/theme.js +17 -0
- package/behaviors/toast.d.ts +49 -0
- package/behaviors/toast.d.ts.map +1 -0
- package/behaviors/toast.js +34 -2
- package/classes/classes.json +683 -13
- package/classes/index.d.ts +106 -2
- package/classes/index.js +249 -65
- package/connectors/index.d.ts +12 -0
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +23 -2
- package/css/app.css +26 -0
- package/css/bullet.css +108 -0
- package/css/code.css +98 -0
- package/css/content.css +15 -2
- package/css/crosshair.css +7 -7
- package/css/diff.css +153 -0
- package/css/disclosure.css +18 -4
- package/css/dots.css +37 -7
- package/css/feedback.css +39 -7
- package/css/forms.css +71 -3
- package/css/legend.css +5 -2
- package/css/motion.css +79 -14
- package/css/overlay.css +59 -2
- package/css/primitives.css +67 -8
- package/css/report.css +40 -0
- package/css/sidenote.css +67 -0
- package/css/spark.css +62 -0
- package/css/table.css +9 -2
- package/css/term.css +110 -0
- package/css/textref.css +63 -0
- package/css/toc.css +91 -0
- package/css/tokens.css +14 -1
- package/css/tree.css +134 -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 -0
- package/dist/css/code.css +1 -0
- package/dist/css/content.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/diff.css +1 -0
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/forms.css +1 -1
- package/dist/css/legend.css +1 -1
- package/dist/css/motion.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/sidenote.css +1 -0
- package/dist/css/spark.css +1 -0
- package/dist/css/table.css +1 -1
- package/dist/css/term.css +1 -0
- package/dist/css/textref.css +1 -0
- package/dist/css/toc.css +1 -0
- package/dist/css/tokens.css +1 -1
- package/dist/css/tree.css +1 -0
- package/docs/annotations.md +39 -0
- package/docs/architecture.md +2 -3
- package/docs/bullet.md +78 -0
- package/docs/code.md +76 -0
- package/docs/d2.md +4 -3
- package/docs/diff.md +146 -0
- package/docs/legends.md +8 -4
- package/docs/mermaid.md +21 -4
- package/docs/reference.md +127 -8
- package/docs/reporting.md +35 -14
- package/docs/sidenote.md +64 -0
- package/docs/spark.md +78 -0
- package/docs/stability.md +1 -0
- package/docs/term.md +81 -0
- package/docs/textref.md +78 -0
- package/docs/theming.md +44 -5
- package/docs/toc.md +83 -0
- package/docs/tree.md +74 -0
- package/docs/usage.md +264 -23
- package/docs/vega.md +22 -3
- package/glyphs/glyphs.js +7 -1
- package/llms.txt +159 -13
- package/package.json +47 -7
- package/qwik/index.d.ts +4 -2
- package/qwik/index.d.ts.map +1 -1
- package/qwik/index.js +10 -0
- package/react/index.d.ts +4 -2
- package/react/index.d.ts.map +1 -1
- package/react/index.js +6 -0
- package/solid/index.d.ts +6 -2
- package/solid/index.d.ts.map +1 -1
- package/solid/index.js +6 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
2
|
+
|
|
3
|
+
const DISABLED = '[aria-disabled="true"]';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Make `aria-disabled="true"` controls keyboard-inert, not just pointer-inert.
|
|
7
|
+
*
|
|
8
|
+
* CSS can dim an `aria-disabled` control and set `pointer-events: none`, but it
|
|
9
|
+
* cannot block keyboard activation: a focused `<a aria-disabled>` or
|
|
10
|
+
* `<button aria-disabled>` still fires on Enter/Space, so a keyboard user can
|
|
11
|
+
* activate a control that looks dead. (Native `disabled` is already fully inert;
|
|
12
|
+
* this guard brings the ARIA path up to parity.) The element intentionally stays
|
|
13
|
+
* focusable and announced — the WAI-ARIA disabled pattern keeps it in the tab
|
|
14
|
+
* order — but it no longer *acts*.
|
|
15
|
+
*
|
|
16
|
+
* Wire once near the root, like {@link applyStoredTheme}. Capturing listeners
|
|
17
|
+
* intercept activation before any component handler (tabs, pagination, menus)
|
|
18
|
+
* sees it. (component audit C4.)
|
|
19
|
+
*
|
|
20
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
21
|
+
* @returns {import('./internal.js').Cleanup}
|
|
22
|
+
*/
|
|
23
|
+
export function initDisabledGuard({ root } = {}) {
|
|
24
|
+
if (!hasDom()) return noop;
|
|
25
|
+
const host = resolveHost(root);
|
|
26
|
+
if (!host) return noop;
|
|
27
|
+
const block = (e) => {
|
|
28
|
+
const el = e.target.closest?.(DISABLED);
|
|
29
|
+
if (el && host.contains(el)) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const onKeydown = (e) => {
|
|
35
|
+
// Only the activation keys; Tab/arrows must still move focus PAST the control.
|
|
36
|
+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') block(e);
|
|
37
|
+
};
|
|
38
|
+
return bindOnce(host, 'disabled-guard', () => {
|
|
39
|
+
host.addEventListener('click', block, true);
|
|
40
|
+
host.addEventListener('keydown', onKeydown, true);
|
|
41
|
+
return () => {
|
|
42
|
+
host.removeEventListener('click', block, true);
|
|
43
|
+
host.removeEventListener('keydown', onKeydown, true);
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function resolveHost(root: any, fallback?: Document): any;
|
|
2
|
+
export function bindOnce(target: any, key: any, add: any): () => void;
|
|
3
|
+
export function byIdInHost(host: any, id: any): any;
|
|
4
|
+
export function closestSafe(el: any, selector: any): any;
|
|
5
|
+
export function collectHosts(host: any, selector: any): any[];
|
|
6
|
+
export function scrollIntoViewSafe(el: any, opts?: {
|
|
7
|
+
block: string;
|
|
8
|
+
}): void;
|
|
9
|
+
export function focusInto(container: any): void;
|
|
10
|
+
export function wrapIndex(cur: any, delta: any, len: any): any;
|
|
11
|
+
export function noop(): void;
|
|
12
|
+
export function hasDom(): boolean;
|
|
13
|
+
export function nextFieldUid(): number;
|
|
14
|
+
/**
|
|
15
|
+
* Cleanup function returned by every initializer; calling it tears down the
|
|
16
|
+
* behavior's listeners/observers.
|
|
17
|
+
*/
|
|
18
|
+
export type Cleanup = () => void;
|
|
19
|
+
export type DelegateOpts = {
|
|
20
|
+
/**
|
|
21
|
+
* Event-delegation root; also scopes which controls are queried. Default: `document`.
|
|
22
|
+
*/
|
|
23
|
+
root?: Document | Element | undefined;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AA8BA,iEAGC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAMC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AA9GM,6BAAqB;AAErB,kCAAoD;AAsBpD,uCAAqC;;;;;sBAjC/B,MAAM,IAAI"}
|
package/behaviors/internal.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// Shared, dependency-free DOM helpers for the behavior modules.
|
|
2
2
|
// Not part of the public @ponchia/ui/behaviors surface (the barrel
|
|
3
|
-
// re-exports only the documented behaviors
|
|
3
|
+
// re-exports only the documented behaviors' values — these shared option
|
|
4
|
+
// types are re-exported by name from index.js).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {() => void} Cleanup
|
|
8
|
+
* Cleanup function returned by every initializer; calling it tears down the
|
|
9
|
+
* behavior's listeners/observers.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {object} DelegateOpts
|
|
12
|
+
* @property {Document | Element} [root]
|
|
13
|
+
* Event-delegation root; also scopes which controls are queried. Default: `document`.
|
|
14
|
+
*/
|
|
4
15
|
|
|
5
16
|
export const noop = () => {};
|
|
6
17
|
|
|
@@ -85,6 +96,24 @@ export function scrollIntoViewSafe(el, opts = { block: 'nearest' }) {
|
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
// The focusable-element selector + "move focus into a container" helper shared
|
|
100
|
+
// by the modal and popover focus paths (a dialog/modal must move focus into
|
|
101
|
+
// itself on open). Focus the first focusable descendant, else make the
|
|
102
|
+
// container programmatically focusable and focus it, so a content-only
|
|
103
|
+
// panel/modal still receives focus. (code-quality audit Q4.)
|
|
104
|
+
const FOCUSABLE =
|
|
105
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
106
|
+
|
|
107
|
+
export function focusInto(container) {
|
|
108
|
+
const first = container.querySelector(FOCUSABLE);
|
|
109
|
+
if (first) {
|
|
110
|
+
first.focus?.();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!container.hasAttribute('tabindex')) container.setAttribute('tabindex', '-1');
|
|
114
|
+
container.focus?.();
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
// Wrap an index by `delta` within [0, len), the roving keyboard math shared by
|
|
89
118
|
// the combobox and command listboxes (a -1 `cur` lands on the first/last as
|
|
90
119
|
// before). Only this core is shared — the surrounding setActive/filter/group
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} LegendToggleDetail
|
|
3
|
+
* @property {string | number} series The entry's `data-series`, or its 0-based index when unset.
|
|
4
|
+
* @property {boolean} active The new state (`true` ⇒ series shown).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Wire `[data-bronto-legend]` interactive legends. Each entry is a
|
|
8
|
+
* `.ui-legend__item` authored as a `<button aria-pressed>`; clicking it (or
|
|
9
|
+
* Enter/Space, native to `<button>`) flips `aria-pressed`, toggles the
|
|
10
|
+
* `.is-inactive` class, and dispatches `bronto:legend:toggle` on the legend
|
|
11
|
+
* with `{ detail: { series, active } }` (`series` is the entry's
|
|
12
|
+
* `data-series`, or its 0-based index if unset).
|
|
13
|
+
*
|
|
14
|
+
* Bronto owns only the control and its pressed/inactive *state*. It does not
|
|
15
|
+
* know the chart's series, hide anything, or announce the change: the host
|
|
16
|
+
* listens for the event, hides its own series, and owns any `aria-live`
|
|
17
|
+
* announcement. The convention is `aria-pressed="true"` ⇒ the series is shown
|
|
18
|
+
* (the default); the entry's label never changes on toggle (WAI-ARIA). SSR-safe
|
|
19
|
+
* and idempotent per host; returns a cleanup function.
|
|
20
|
+
*
|
|
21
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
22
|
+
* @returns {import('./internal.js').Cleanup}
|
|
23
|
+
*/
|
|
24
|
+
export function initLegend({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
25
|
+
export type LegendToggleDetail = {
|
|
26
|
+
/**
|
|
27
|
+
* The entry's `data-series`, or its 0-based index when unset.
|
|
28
|
+
*/
|
|
29
|
+
series: string | number;
|
|
30
|
+
/**
|
|
31
|
+
* The new state (`true` ⇒ series shown).
|
|
32
|
+
*/
|
|
33
|
+
active: boolean;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=legend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legend.d.ts","sourceRoot":"","sources":["legend.js"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmD3C;;;;;YAvEa,MAAM,GAAG,MAAM;;;;YACf,OAAO"}
|
package/behaviors/legend.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} LegendToggleDetail
|
|
5
|
+
* @property {string | number} series The entry's `data-series`, or its 0-based index when unset.
|
|
6
|
+
* @property {boolean} active The new state (`true` ⇒ series shown).
|
|
7
|
+
*/
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* Wire `[data-bronto-legend]` interactive legends. Each entry is a
|
|
5
11
|
* `.ui-legend__item` authored as a `<button aria-pressed>`; clicking it (or
|
|
@@ -14,6 +20,9 @@ import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
|
14
20
|
* announcement. The convention is `aria-pressed="true"` ⇒ the series is shown
|
|
15
21
|
* (the default); the entry's label never changes on toggle (WAI-ARIA). SSR-safe
|
|
16
22
|
* and idempotent per host; returns a cleanup function.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
25
|
+
* @returns {import('./internal.js').Cleanup}
|
|
17
26
|
*/
|
|
18
27
|
export function initLegend({ root } = {}) {
|
|
19
28
|
if (!hasDom()) return noop;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dropdown-menu close affordances for a native `<details data-bronto-menu>`
|
|
3
|
+
* holding a `.ui-menu`. `<details>` alone won't close on Escape, on an
|
|
4
|
+
* outside click, or when a `.ui-menu__item` is activated — this adds
|
|
5
|
+
* exactly those, returning focus to the `<summary>` on Esc/activate.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT a full WAI-ARIA menu (no arrow-key roving): the items
|
|
8
|
+
* are real buttons, Tab-reachable; this is a disclosure of actions, and
|
|
9
|
+
* over-claiming `role="menu"` semantics would be worse. SSR-safe,
|
|
10
|
+
* idempotent; returns a cleanup function.
|
|
11
|
+
*
|
|
12
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
13
|
+
* @returns {import('./internal.js').Cleanup}
|
|
14
|
+
*/
|
|
15
|
+
export function initMenu({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
16
|
+
//# sourceMappingURL=menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["menu.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmC3C"}
|
package/behaviors/menu.js
CHANGED
|
@@ -10,6 +10,9 @@ import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
|
10
10
|
* are real buttons, Tab-reachable; this is a disclosure of actions, and
|
|
11
11
|
* over-claiming `role="menu"` semantics would be worse. SSR-safe,
|
|
12
12
|
* idempotent; returns a cleanup function.
|
|
13
|
+
*
|
|
14
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
15
|
+
* @returns {import('./internal.js').Cleanup}
|
|
13
16
|
*/
|
|
14
17
|
export function initMenu({ root } = {}) {
|
|
15
18
|
if (!hasDom()) return noop;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ModalCloseDetail
|
|
3
|
+
* @property {'escape'} reason What asked the modal to close (currently only Escape).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Focus management for the **controlled, non-`<dialog>` modal** — the
|
|
7
|
+
* `.ui-modal.is-open` path a portal/React overlay uses when it genuinely can't
|
|
8
|
+
* be a native `<dialog>`. The native `<dialog>` path gets a focus trap, Escape,
|
|
9
|
+
* and the top layer for free (use `initDialog`); this supplies the equivalent
|
|
10
|
+
* for the `.is-open` path, which otherwise leaves focus management to the
|
|
11
|
+
* consumer.
|
|
12
|
+
*
|
|
13
|
+
* Mark the overlay `[data-bronto-modal]` (opt-in). On bind it gives the modal a
|
|
14
|
+
* `role="dialog"` + `aria-modal="true"` (unless the author set a role) and
|
|
15
|
+
* dev-warns when it has no accessible name, so it announces as a named modal
|
|
16
|
+
* dialog — parity with `initPopover`. The behavior watches its
|
|
17
|
+
* `class` for `is-open`: on open it remembers the focused element, moves focus
|
|
18
|
+
* into the modal (first focusable, else the panel itself), and **traps focus by
|
|
19
|
+
* marking every sibling at each ancestor level `inert`** so the rest of the page
|
|
20
|
+
* is non-focusable and non-interactive — the modern, robust trap. On close it
|
|
21
|
+
* un-inerts exactly what it inerted and returns focus to the opener. Bronto owns
|
|
22
|
+
* focus only: the **consumer still owns open/close state** (the `is-open`
|
|
23
|
+
* class). Escape dispatches a cancelable `bronto:modal:close`
|
|
24
|
+
* ({@link ModalCloseDetail}) on the modal so the consumer can drop `is-open` in
|
|
25
|
+
* response; the behavior never changes visibility itself.
|
|
26
|
+
*
|
|
27
|
+
* Best suited to a body-/portal-level overlay (the documented `.is-open` use
|
|
28
|
+
* case); a deeply-nested modal still gets focus-into, focus-return, and the
|
|
29
|
+
* Escape signal. SSR-safe, idempotent per modal; returns a cleanup function.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
32
|
+
* @returns {import('./internal.js').Cleanup}
|
|
33
|
+
*/
|
|
34
|
+
export function initModal({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
35
|
+
export type ModalCloseDetail = {
|
|
36
|
+
/**
|
|
37
|
+
* What asked the modal to close (currently only Escape).
|
|
38
|
+
*/
|
|
39
|
+
reason: "escape";
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAEA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAyF3C;;;;;YAvHa,QAAQ"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, collectHosts, focusInto } from './internal.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} ModalCloseDetail
|
|
5
|
+
* @property {'escape'} reason What asked the modal to close (currently only Escape).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Focus management for the **controlled, non-`<dialog>` modal** — the
|
|
10
|
+
* `.ui-modal.is-open` path a portal/React overlay uses when it genuinely can't
|
|
11
|
+
* be a native `<dialog>`. The native `<dialog>` path gets a focus trap, Escape,
|
|
12
|
+
* and the top layer for free (use `initDialog`); this supplies the equivalent
|
|
13
|
+
* for the `.is-open` path, which otherwise leaves focus management to the
|
|
14
|
+
* consumer.
|
|
15
|
+
*
|
|
16
|
+
* Mark the overlay `[data-bronto-modal]` (opt-in). On bind it gives the modal a
|
|
17
|
+
* `role="dialog"` + `aria-modal="true"` (unless the author set a role) and
|
|
18
|
+
* dev-warns when it has no accessible name, so it announces as a named modal
|
|
19
|
+
* dialog — parity with `initPopover`. The behavior watches its
|
|
20
|
+
* `class` for `is-open`: on open it remembers the focused element, moves focus
|
|
21
|
+
* into the modal (first focusable, else the panel itself), and **traps focus by
|
|
22
|
+
* marking every sibling at each ancestor level `inert`** so the rest of the page
|
|
23
|
+
* is non-focusable and non-interactive — the modern, robust trap. On close it
|
|
24
|
+
* un-inerts exactly what it inerted and returns focus to the opener. Bronto owns
|
|
25
|
+
* focus only: the **consumer still owns open/close state** (the `is-open`
|
|
26
|
+
* class). Escape dispatches a cancelable `bronto:modal:close`
|
|
27
|
+
* ({@link ModalCloseDetail}) on the modal so the consumer can drop `is-open` in
|
|
28
|
+
* response; the behavior never changes visibility itself.
|
|
29
|
+
*
|
|
30
|
+
* Best suited to a body-/portal-level overlay (the documented `.is-open` use
|
|
31
|
+
* case); a deeply-nested modal still gets focus-into, focus-return, and the
|
|
32
|
+
* Escape signal. SSR-safe, idempotent per modal; returns a cleanup function.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
35
|
+
* @returns {import('./internal.js').Cleanup}
|
|
36
|
+
*/
|
|
37
|
+
export function initModal({ root } = {}) {
|
|
38
|
+
if (!hasDom()) return noop;
|
|
39
|
+
const host = resolveHost(root);
|
|
40
|
+
if (!host) return noop;
|
|
41
|
+
const modals = collectHosts(host, '[data-bronto-modal]');
|
|
42
|
+
const cleanups = [];
|
|
43
|
+
|
|
44
|
+
for (const modal of modals) {
|
|
45
|
+
let opener = null;
|
|
46
|
+
let inerted = [];
|
|
47
|
+
|
|
48
|
+
// A controlled modal must announce AS a modal dialog, not a generic group —
|
|
49
|
+
// parity with initPopover. Apply a dialog role + aria-modal (unless the
|
|
50
|
+
// author set a role), and dev-warn on a missing accessible name since we
|
|
51
|
+
// can't invent a good one. (component audit C13.)
|
|
52
|
+
if (!modal.hasAttribute('role')) modal.setAttribute('role', 'dialog');
|
|
53
|
+
if (!modal.hasAttribute('aria-modal')) modal.setAttribute('aria-modal', 'true');
|
|
54
|
+
const named =
|
|
55
|
+
modal.hasAttribute('aria-label') ||
|
|
56
|
+
modal.hasAttribute('aria-labelledby') ||
|
|
57
|
+
modal.hasAttribute('title');
|
|
58
|
+
if (!named && typeof console !== 'undefined') {
|
|
59
|
+
console.warn(
|
|
60
|
+
`[bronto] initModal(): a [data-bronto-modal] has no accessible name — add aria-label or aria-labelledby so it is announced as a named dialog.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Inert every sibling at each ancestor level up to <body>: the rest of the
|
|
65
|
+
// page becomes non-focusable/non-interactive while the modal subtree stays
|
|
66
|
+
// live. Skip already-inert nodes so release() can't un-inert something the
|
|
67
|
+
// app inerted for its own reasons.
|
|
68
|
+
const trap = () => {
|
|
69
|
+
if (opener) return; // already trapped
|
|
70
|
+
opener = document.activeElement;
|
|
71
|
+
let el = modal;
|
|
72
|
+
while (el && el.parentElement && el !== document.body) {
|
|
73
|
+
for (const sib of el.parentElement.children) {
|
|
74
|
+
if (sib !== el && !sib.inert) {
|
|
75
|
+
sib.inert = true;
|
|
76
|
+
inerted.push(sib);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
el = el.parentElement;
|
|
80
|
+
}
|
|
81
|
+
focusInto(modal);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const release = () => {
|
|
85
|
+
if (!opener) return;
|
|
86
|
+
for (const el of inerted) el.inert = false;
|
|
87
|
+
inerted = [];
|
|
88
|
+
const back = opener;
|
|
89
|
+
opener = null;
|
|
90
|
+
if (back?.isConnected && typeof back.focus === 'function') back.focus();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const sync = () => (modal.classList.contains('is-open') ? trap() : release());
|
|
94
|
+
|
|
95
|
+
const onKey = (e) => {
|
|
96
|
+
if (e.key === 'Escape' && opener) {
|
|
97
|
+
modal.dispatchEvent(
|
|
98
|
+
new CustomEvent('bronto:modal:close', {
|
|
99
|
+
detail: { reason: 'escape' },
|
|
100
|
+
bubbles: true,
|
|
101
|
+
cancelable: true,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
|
|
108
|
+
|
|
109
|
+
cleanups.push(
|
|
110
|
+
bindOnce(modal, 'modal', () => {
|
|
111
|
+
observer?.observe(modal, { attributes: true, attributeFilter: ['class'] });
|
|
112
|
+
document.addEventListener('keydown', onKey, true);
|
|
113
|
+
if (modal.classList.contains('is-open')) trap(); // already open at init
|
|
114
|
+
return () => {
|
|
115
|
+
observer?.disconnect();
|
|
116
|
+
document.removeEventListener('keydown', onKey, true);
|
|
117
|
+
release();
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return () => cleanups.forEach((fn) => fn());
|
|
124
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collision-aware popover, dependency-free. A `[data-bronto-popover]`
|
|
3
|
+
* trigger toggles the `.ui-popover` panel whose id it names. The panel
|
|
4
|
+
* is placed under the trigger and **flips above** when it would
|
|
5
|
+
* overflow the viewport, with its inline edge clamped on-screen — the
|
|
6
|
+
* thing the CSS-only tooltip can't do near edges / inside scroll
|
|
7
|
+
* containers. If the panel has the native `popover` attribute and the
|
|
8
|
+
* Popover API is available it is shown in the top layer (never
|
|
9
|
+
* clipped); otherwise an `.is-open` class is toggled. Manages
|
|
10
|
+
* `aria-expanded` / `aria-controls`, closes on Escape and outside
|
|
11
|
+
* click, and re-positions on scroll/resize while open. SSR-safe,
|
|
12
|
+
* idempotent; returns a cleanup function.
|
|
13
|
+
*
|
|
14
|
+
* The trigger advertises `aria-haspopup="dialog"`, so on open the panel is
|
|
15
|
+
* given `role="dialog"` (unless the author set a role) and focus is moved into
|
|
16
|
+
* it — the first focusable descendant, or the panel itself. It is a *non-modal*
|
|
17
|
+
* dialog: the rest of the page stays interactive and there is no focus trap.
|
|
18
|
+
* Author an accessible name on the panel (`aria-label` / `aria-labelledby`); a
|
|
19
|
+
* dev-time `console.warn` fires when it is missing.
|
|
20
|
+
*
|
|
21
|
+
* Escape returns focus to the trigger; closing via outside-click leaves focus
|
|
22
|
+
* where the click landed (treated as deliberate intent to move on).
|
|
23
|
+
*
|
|
24
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
25
|
+
* @returns {import('./internal.js').Cleanup}
|
|
26
|
+
*/
|
|
27
|
+
export function initPopover({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
28
|
+
//# sourceMappingURL=popover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwJ3C"}
|
package/behaviors/popover.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasDom, resolveHost, noop, bindOnce, byIdInHost } from './internal.js';
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, byIdInHost, focusInto } from './internal.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Collision-aware popover, dependency-free. A `[data-bronto-popover]`
|
|
@@ -22,6 +22,9 @@ import { hasDom, resolveHost, noop, bindOnce, byIdInHost } from './internal.js';
|
|
|
22
22
|
*
|
|
23
23
|
* Escape returns focus to the trigger; closing via outside-click leaves focus
|
|
24
24
|
* where the click landed (treated as deliberate intent to move on).
|
|
25
|
+
*
|
|
26
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
27
|
+
* @returns {import('./internal.js').Cleanup}
|
|
25
28
|
*/
|
|
26
29
|
export function initPopover({ root } = {}) {
|
|
27
30
|
if (!hasDom()) return noop;
|
|
@@ -32,22 +35,9 @@ export function initPopover({ root } = {}) {
|
|
|
32
35
|
let openPanel = null;
|
|
33
36
|
let openTrigger = null;
|
|
34
37
|
|
|
35
|
-
const FOCUSABLE =
|
|
36
|
-
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
37
|
-
|
|
38
38
|
// The trigger advertises `aria-haspopup="dialog"`, so the open panel must BE a
|
|
39
|
-
// dialog: a role, an accessible name, and focus moved into it (C6)
|
|
40
|
-
//
|
|
41
|
-
// focusable) so a content-only panel still receives focus.
|
|
42
|
-
const focusInto = (panel) => {
|
|
43
|
-
const first = panel.querySelector(FOCUSABLE);
|
|
44
|
-
if (first) {
|
|
45
|
-
first.focus?.();
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (!panel.hasAttribute('tabindex')) panel.setAttribute('tabindex', '-1');
|
|
49
|
-
panel.focus?.();
|
|
50
|
-
};
|
|
39
|
+
// dialog: a role, an accessible name, and focus moved into it (C6) — see the
|
|
40
|
+
// shared `focusInto` in internal.js.
|
|
51
41
|
|
|
52
42
|
const place = (trigger, panel) => {
|
|
53
43
|
const r = trigger.getBoundingClientRect();
|
|
@@ -122,7 +112,17 @@ export function initPopover({ root } = {}) {
|
|
|
122
112
|
};
|
|
123
113
|
const onKey = (e) => {
|
|
124
114
|
// close() returns focus to the trigger because focus is inside the panel.
|
|
125
|
-
if (e.key
|
|
115
|
+
if (e.key !== 'Escape' || !openPanel) return;
|
|
116
|
+
// A popover open *inside* a <dialog>/modal owns this Escape. Without this,
|
|
117
|
+
// the same keypress closed BOTH: we hidePopover() the panel synchronously,
|
|
118
|
+
// the browser's close-request then finds the dialog as the new topmost
|
|
119
|
+
// element and dismisses it too. preventDefault() stops that native
|
|
120
|
+
// close-request and stopPropagation() keeps it off other delegated keydown
|
|
121
|
+
// listeners (e.g. initModal's), so only the popover closes — the documented
|
|
122
|
+
// "popover + dialog open together" contract.
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
close();
|
|
126
126
|
};
|
|
127
127
|
const onReflow = () => {
|
|
128
128
|
if (openPanel && openTrigger) place(openTrigger, openPanel);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position a spotlight cutout over a target element. Each
|
|
3
|
+
* `[data-bronto-spotlight]` is a `.ui-spotlight` overlay; `data-target` is the
|
|
4
|
+
* id of the element to highlight. The behavior measures the target and sets
|
|
5
|
+
* `--spot-x/y/w/h` (viewport coordinates) on the overlay, re-placing on
|
|
6
|
+
* resize/scroll and whenever `data-target` changes.
|
|
7
|
+
*
|
|
8
|
+
* Bronto owns only positioning + the visual language. It is NOT a tour engine:
|
|
9
|
+
* the host decides which target is current, when to advance, and whether to
|
|
10
|
+
* show/hide the overlay — just update `data-target` (or toggle `hidden`) and
|
|
11
|
+
* the cutout follows. SSR-safe, idempotent per host; returns a cleanup.
|
|
12
|
+
*
|
|
13
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
14
|
+
* @returns {import('./internal.js').Cleanup}
|
|
15
|
+
*/
|
|
16
|
+
export function initSpotlight({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
17
|
+
//# sourceMappingURL=spotlight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["spotlight.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAuC3C"}
|
package/behaviors/spotlight.js
CHANGED
|
@@ -11,6 +11,9 @@ import { hasDom, resolveHost, noop, bindOnce, byIdInHost, collectHosts } from '.
|
|
|
11
11
|
* the host decides which target is current, when to advance, and whether to
|
|
12
12
|
* show/hide the overlay — just update `data-target` (or toggle `hidden`) and
|
|
13
13
|
* the cutout follows. SSR-safe, idempotent per host; returns a cleanup.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
16
|
+
* @returns {import('./internal.js').Cleanup}
|
|
14
17
|
*/
|
|
15
18
|
export function initSpotlight({ root } = {}) {
|
|
16
19
|
if (!hasDom()) return noop;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side sortable + selectable data table. Wires
|
|
3
|
+
* `[data-bronto-sortable]`:
|
|
4
|
+
*
|
|
5
|
+
* - clicking a header's `.ui-table__sort` button sorts the tbody by
|
|
6
|
+
* that column. Sortable headers are seeded `aria-sort="none"`; a
|
|
7
|
+
* click toggles that header ascending ⇄ descending (first click =
|
|
8
|
+
* ascending) and resets the other sortable headers to `none`.
|
|
9
|
+
* Numeric columns (`data-sort="num"` or `.is-num` cells) sort
|
|
10
|
+
* numerically; everything else, locale string compare. Any
|
|
11
|
+
* `.ui-table__empty` sentinel row is kept last after a sort.
|
|
12
|
+
* - a `[data-bronto-select-all]` checkbox toggles every
|
|
13
|
+
* `[data-bronto-select]` row checkbox and the rows'
|
|
14
|
+
* `aria-selected`; toggling a row keeps the header checkbox's
|
|
15
|
+
* checked/indeterminate state in sync. Emits `bronto:selectionchange`
|
|
16
|
+
* ({ detail: { count } }) on the table.
|
|
17
|
+
*
|
|
18
|
+
* SSR-safe, idempotent per table; returns a cleanup function.
|
|
19
|
+
*
|
|
20
|
+
* The numeric sort parses each cell's display text after normalizing the
|
|
21
|
+
* common report shapes: a Unicode minus (U+2212) and en/em dashes count as a
|
|
22
|
+
* sign (so a "−5" loss sorts BELOW a "5" gain, not above it), accounting
|
|
23
|
+
* parentheses `(1,234)` read as negative, and `,` thousands separators are
|
|
24
|
+
* dropped. Note the consequence: a bare `,` in the *display text* is read as a
|
|
25
|
+
* thousands separator, so a European decimal "3,5" sorts as 35, not 3.5 — for a
|
|
26
|
+
* European decimal comma (or mixed units, or any ambiguous text) put the
|
|
27
|
+
* canonical number in a `data-sort-value` attribute on the cell. That escape
|
|
28
|
+
* hatch wins over the parsed text and accepts either a dot ("3.5") or a single
|
|
29
|
+
* decimal comma ("3,5"). It is a client-side convenience sorter, not a data
|
|
30
|
+
* grid. (component audit C3/C5.)
|
|
31
|
+
*
|
|
32
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
33
|
+
* @returns {import('./internal.js').Cleanup}
|
|
34
|
+
*/
|
|
35
|
+
export function initTableSort({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
36
|
+
//# sourceMappingURL=table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6I3C"}
|
package/behaviors/table.js
CHANGED
|
@@ -19,9 +19,20 @@ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js
|
|
|
19
19
|
*
|
|
20
20
|
* SSR-safe, idempotent per table; returns a cleanup function.
|
|
21
21
|
*
|
|
22
|
-
* The numeric sort parses each cell
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* The numeric sort parses each cell's display text after normalizing the
|
|
23
|
+
* common report shapes: a Unicode minus (U+2212) and en/em dashes count as a
|
|
24
|
+
* sign (so a "−5" loss sorts BELOW a "5" gain, not above it), accounting
|
|
25
|
+
* parentheses `(1,234)` read as negative, and `,` thousands separators are
|
|
26
|
+
* dropped. Note the consequence: a bare `,` in the *display text* is read as a
|
|
27
|
+
* thousands separator, so a European decimal "3,5" sorts as 35, not 3.5 — for a
|
|
28
|
+
* European decimal comma (or mixed units, or any ambiguous text) put the
|
|
29
|
+
* canonical number in a `data-sort-value` attribute on the cell. That escape
|
|
30
|
+
* hatch wins over the parsed text and accepts either a dot ("3.5") or a single
|
|
31
|
+
* decimal comma ("3,5"). It is a client-side convenience sorter, not a data
|
|
32
|
+
* grid. (component audit C3/C5.)
|
|
33
|
+
*
|
|
34
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
35
|
+
* @returns {import('./internal.js').Cleanup}
|
|
25
36
|
*/
|
|
26
37
|
export function initTableSort({ root } = {}) {
|
|
27
38
|
if (!hasDom()) return noop;
|
|
@@ -44,6 +55,38 @@ export function initTableSort({ root } = {}) {
|
|
|
44
55
|
|
|
45
56
|
const colIndex = (th) => [...th.parentElement.children].indexOf(th);
|
|
46
57
|
const cellText = (row, i) => row.children[i]?.textContent.trim() ?? '';
|
|
58
|
+
// Numeric value of a cell for sorting. A `data-sort-value` attribute is the
|
|
59
|
+
// authoritative escape hatch; otherwise normalize the display text so the
|
|
60
|
+
// sign survives (U+2212 / en-em dashes → minus, accounting parens →
|
|
61
|
+
// negative) and `,` grouping is dropped. Returns 0 for unparseable cells so
|
|
62
|
+
// they cluster rather than scatter. (component audit C3.)
|
|
63
|
+
const cellNum = (row, i) => {
|
|
64
|
+
const cell = row.children[i];
|
|
65
|
+
const explicit = cell?.getAttribute?.('data-sort-value');
|
|
66
|
+
if (explicit != null && explicit.trim() !== '') {
|
|
67
|
+
const raw = explicit.trim();
|
|
68
|
+
let v = Number(raw);
|
|
69
|
+
// The escape hatch must actually handle the case the doc names it for: a
|
|
70
|
+
// European decimal comma. `Number("3,5")` is NaN, which silently fell
|
|
71
|
+
// back to parsing the display text (where `,` is dropped as a thousands
|
|
72
|
+
// separator → "35"). A lone comma with no dot is a decimal point here.
|
|
73
|
+
if (!Number.isFinite(v) && /^[+-]?\d+,\d+$/.test(raw)) v = Number(raw.replace(',', '.'));
|
|
74
|
+
if (Number.isFinite(v)) return v;
|
|
75
|
+
}
|
|
76
|
+
let s = (cell?.textContent ?? '').trim();
|
|
77
|
+
if (!s) return 0;
|
|
78
|
+
let sign = 1;
|
|
79
|
+
const paren = /^\((.*)\)$/.exec(s); // accounting negative
|
|
80
|
+
if (paren) {
|
|
81
|
+
sign = -1;
|
|
82
|
+
s = paren[1];
|
|
83
|
+
}
|
|
84
|
+
s = s.replace(/[−–—]/g, '-'); // minus / en / em dash → '-'
|
|
85
|
+
if (/-/.test(s)) sign *= -1;
|
|
86
|
+
s = s.replace(/,/g, ''); // drop thousands separators
|
|
87
|
+
const v = parseFloat(s.replace(/[^\d.]/g, '')); // magnitude
|
|
88
|
+
return Number.isFinite(v) ? sign * v : 0;
|
|
89
|
+
};
|
|
47
90
|
|
|
48
91
|
const sortBy = (th, numeric) => {
|
|
49
92
|
const headers = th.closest('tr').querySelectorAll('th');
|
|
@@ -61,12 +104,9 @@ export function initTableSort({ root } = {}) {
|
|
|
61
104
|
const emptyRows = [...tbody.rows].filter((r) => r.classList.contains('ui-table__empty'));
|
|
62
105
|
const rows = [...tbody.rows].filter((r) => !r.classList.contains('ui-table__empty'));
|
|
63
106
|
rows.sort((a, b) => {
|
|
64
|
-
const x = cellText(a, i);
|
|
65
|
-
const y = cellText(b, i);
|
|
66
107
|
const cmp = numeric
|
|
67
|
-
? (
|
|
68
|
-
|
|
69
|
-
: x.localeCompare(y);
|
|
108
|
+
? cellNum(a, i) - cellNum(b, i)
|
|
109
|
+
: cellText(a, i).localeCompare(cellText(b, i));
|
|
70
110
|
return cmp * sign;
|
|
71
111
|
});
|
|
72
112
|
// Re-parent in document order: sorted data rows, then any empty/sentinel
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire `[data-bronto-tabs]` groups for full keyboard a11y. The framework
|
|
3
|
+
* ships the look + the ARIA/`.is-active` contract; this adds the WAI-ARIA
|
|
4
|
+
* Tabs pattern: roving `tabindex`, `aria-selected`, Arrow/Home/End
|
|
5
|
+
* navigation with automatic activation, and panel `hidden` sync. Tabs are
|
|
6
|
+
* `.ui-tab[data-tab]`; panels are `.ui-tabs__panel[data-panel]` with
|
|
7
|
+
* matching values. SSR-safe and idempotent (re-init replaces, never
|
|
8
|
+
* stacks, the per-group listeners); returns a cleanup function.
|
|
9
|
+
*
|
|
10
|
+
* Accessibility caveat: this is what makes tabs operable. Do **not**
|
|
11
|
+
* author `hidden` on `.ui-tabs__panel` in server-rendered markup unless
|
|
12
|
+
* `initTabs` is guaranteed to run client-side — without it the panels
|
|
13
|
+
* stay hidden with no keyboard/pointer way to reveal them. Prefer
|
|
14
|
+
* authoring all panels visible and letting `initTabs` add `hidden`.
|
|
15
|
+
*
|
|
16
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
17
|
+
* @returns {import('./internal.js').Cleanup}
|
|
18
|
+
*/
|
|
19
|
+
export function initTabs({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
20
|
+
//# sourceMappingURL=tabs.d.ts.map
|