@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/glyph.js
CHANGED
|
@@ -30,11 +30,62 @@ function rememberCleanup(el, cleanups, cleanup) {
|
|
|
30
30
|
cleanups.push(wrapped);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const CSS_LENGTH_UNITS =
|
|
34
|
+
'(?:px|r?em|ch|ex|cap|ic|lh|rlh|vw|vh|vi|vb|vmin|vmax|' +
|
|
35
|
+
'svw|svh|svi|svb|svmin|svmax|lvw|lvh|lvi|lvb|lvmin|lvmax|' +
|
|
36
|
+
'dvw|dvh|dvi|dvb|dvmin|dvmax|cm|mm|q|in|pc|pt|%)';
|
|
37
|
+
const CSS_NUMBER = '[-+]?(?:\\d*\\.\\d+|\\d+)';
|
|
38
|
+
const CSS_LENGTH_RE = new RegExp(`^(?:0|${CSS_NUMBER}${CSS_LENGTH_UNITS})$`, 'i');
|
|
39
|
+
const CSS_CALC_VALUE_RE = new RegExp(`^${CSS_NUMBER}(?:${CSS_LENGTH_UNITS})?`, 'i');
|
|
40
|
+
|
|
41
|
+
function isCalcLength(expr) {
|
|
42
|
+
let rest = expr.trim();
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let sawLength = false;
|
|
45
|
+
let expectValue = true;
|
|
46
|
+
|
|
47
|
+
while (rest) {
|
|
48
|
+
if (expectValue) {
|
|
49
|
+
if (rest[0] === '(') {
|
|
50
|
+
depth += 1;
|
|
51
|
+
rest = rest.slice(1).trim();
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const token = rest.match(CSS_CALC_VALUE_RE)?.[0];
|
|
55
|
+
if (!token) return false;
|
|
56
|
+
if (CSS_LENGTH_RE.test(token)) sawLength = true;
|
|
57
|
+
rest = rest.slice(token.length).trim();
|
|
58
|
+
expectValue = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rest[0] === ')') {
|
|
63
|
+
depth -= 1;
|
|
64
|
+
if (depth < 0) return false;
|
|
65
|
+
rest = rest.slice(1).trim();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const op = rest.match(/^[+\-*/]/)?.[0];
|
|
70
|
+
if (!op) return false;
|
|
71
|
+
rest = rest.slice(op.length).trim();
|
|
72
|
+
expectValue = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!sawLength) return false;
|
|
76
|
+
if (expectValue) return false;
|
|
77
|
+
return depth === 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// `size` lands in inline CSS, so accept only concrete CSS lengths or calc()
|
|
81
|
+
// expressions (mirrors glyphs.js cssLen). Used for the mask path's --icon-size.
|
|
36
82
|
function cssLen(v) {
|
|
37
|
-
|
|
83
|
+
const value = String(v ?? '').trim();
|
|
84
|
+
if (CSS_LENGTH_RE.test(value)) return value;
|
|
85
|
+
if (value.toLowerCase().startsWith('calc(') && value.endsWith(')')) {
|
|
86
|
+
return isCalcLength(value.slice(5, -1)) ? value : '';
|
|
87
|
+
}
|
|
88
|
+
return '';
|
|
38
89
|
}
|
|
39
90
|
|
|
40
91
|
function applyGlyphA11y(el, label) {
|
|
@@ -214,7 +265,7 @@ export function initDotGlyph({ root } = {}) {
|
|
|
214
265
|
const label = el.getAttribute('data-bronto-glyph-label');
|
|
215
266
|
|
|
216
267
|
// One-node mask path — the icon-at-scale counterpart to the 256-cell grid.
|
|
217
|
-
if (el.getAttribute('data-bronto-glyph-render') === 'mask') {
|
|
268
|
+
if (el.getAttribute('data-bronto-glyph-render')?.trim() === 'mask') {
|
|
218
269
|
expandMaskGlyph(el, name, label, cleanups);
|
|
219
270
|
continue;
|
|
220
271
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAaC;AAED,oDAUC;AAED,yDAOC;AAMD,8DAIC;AAID;;SAMC;AAkDD,gDAUC;AAMD,+DAKC;AAjLM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
|
package/behaviors/internal.js
CHANGED
|
@@ -70,7 +70,10 @@ export function bindOnce(target, key, add) {
|
|
|
70
70
|
const reg = target[BOUND] || (target[BOUND] = Object.create(null));
|
|
71
71
|
if (reg[key]) reg[key]();
|
|
72
72
|
const remove = add();
|
|
73
|
+
let done = false;
|
|
73
74
|
const cleanup = () => {
|
|
75
|
+
if (done) return;
|
|
76
|
+
done = true;
|
|
74
77
|
remove();
|
|
75
78
|
if (reg[key] === cleanup) delete reg[key];
|
|
76
79
|
};
|
|
@@ -80,11 +83,13 @@ export function bindOnce(target, key, add) {
|
|
|
80
83
|
|
|
81
84
|
export function byIdInHost(host, id) {
|
|
82
85
|
if (!id) return null;
|
|
83
|
-
|
|
86
|
+
const doc = host.nodeType === 9 ? host : host.ownerDocument;
|
|
87
|
+
if (host === doc) return doc.getElementById(id);
|
|
84
88
|
if (host.id === id) return host;
|
|
85
89
|
return (
|
|
86
90
|
Array.from(host.querySelectorAll?.('[id]') || []).find((el) => el.id === id) ||
|
|
87
|
-
|
|
91
|
+
doc?.getElementById(id) ||
|
|
92
|
+
null
|
|
88
93
|
);
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -122,11 +127,53 @@ export function scrollIntoViewSafe(el, opts = { block: 'nearest' }) {
|
|
|
122
127
|
// itself on open). Focus the first focusable descendant, else make the
|
|
123
128
|
// container programmatically focusable and focus it, so a content-only
|
|
124
129
|
// panel/modal still receives focus.
|
|
125
|
-
const FOCUSABLE =
|
|
126
|
-
|
|
130
|
+
const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]';
|
|
131
|
+
|
|
132
|
+
function isInsideDisabledFieldset(el) {
|
|
133
|
+
return Boolean(el.closest?.('fieldset[disabled]'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isRendered(el) {
|
|
137
|
+
try {
|
|
138
|
+
if (
|
|
139
|
+
typeof el.checkVisibility === 'function' &&
|
|
140
|
+
!el.checkVisibility({ checkVisibilityCSS: true, visibilityProperty: true })
|
|
141
|
+
) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
/* fall through to conservative checks */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const view = el.ownerDocument?.defaultView;
|
|
149
|
+
const style = view?.getComputedStyle?.(el);
|
|
150
|
+
if (style?.display === 'none') return false;
|
|
151
|
+
if (style?.visibility === 'hidden' || style?.visibility === 'collapse') return false;
|
|
152
|
+
|
|
153
|
+
const docHasLayout = Boolean(el.ownerDocument?.documentElement?.getClientRects?.().length);
|
|
154
|
+
if (
|
|
155
|
+
docHasLayout &&
|
|
156
|
+
'offsetParent' in el &&
|
|
157
|
+
el.offsetParent === null &&
|
|
158
|
+
el.getClientRects?.().length === 0
|
|
159
|
+
) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isFocusableCandidate(el) {
|
|
166
|
+
if (el.closest?.('[hidden], [inert]')) return false;
|
|
167
|
+
if ('disabled' in el && el.disabled) return false;
|
|
168
|
+
if (isInsideDisabledFieldset(el)) return false;
|
|
169
|
+
if (el.tabIndex < 0) return false;
|
|
170
|
+
return isRendered(el);
|
|
171
|
+
}
|
|
127
172
|
|
|
128
173
|
export function focusInto(container) {
|
|
129
|
-
const first = container.
|
|
174
|
+
const first = Array.from(container.querySelectorAll?.(FOCUSABLE) || []).find(
|
|
175
|
+
isFocusableCandidate,
|
|
176
|
+
);
|
|
130
177
|
if (first) {
|
|
131
178
|
first.focus?.();
|
|
132
179
|
return;
|
package/behaviors/menu.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,CAyC3C"}
|
package/behaviors/menu.js
CHANGED
|
@@ -18,7 +18,8 @@ export function initMenu({ root } = {}) {
|
|
|
18
18
|
if (!hasDom()) return noop;
|
|
19
19
|
const host = resolveHost(root);
|
|
20
20
|
if (!host) return noop;
|
|
21
|
-
const doc = host.nodeType === 9 ? host : host.ownerDocument
|
|
21
|
+
const doc = host.nodeType === 9 ? host : host.ownerDocument;
|
|
22
|
+
if (!doc) return noop;
|
|
22
23
|
const openMenus = () => collectHosts(host, '[data-bronto-menu][open]');
|
|
23
24
|
const shut = (menu) => {
|
|
24
25
|
if (!menu || !menu.open) return;
|
package/behaviors/modal.d.ts
CHANGED
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Mark the overlay `[data-bronto-modal]` (opt-in). On bind it gives the modal a
|
|
14
14
|
* `role="dialog"` + `aria-modal="true"` (unless the author set a role) and
|
|
15
|
-
* dev-warns when it has no accessible name
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* focus only: the **consumer still owns open/close state** (the
|
|
23
|
-
* class). Escape dispatches a cancelable `bronto:modal:close`
|
|
15
|
+
* dev-warns when it has no accessible name. While open, a document-level stack
|
|
16
|
+
* reconciler keeps only the top controlled modal interactive, marks the rest of
|
|
17
|
+
* the page `inert`, admits an open popover owned by the top modal even when the
|
|
18
|
+
* panel is portaled elsewhere, and applies the trap to background nodes added
|
|
19
|
+
* after open. Nested and sibling portal modals therefore share one ownership
|
|
20
|
+
* model instead of independently inverting each other's `inert` state.
|
|
21
|
+
*
|
|
22
|
+
* Bronto owns focus only: the **consumer still owns open/close state** (the
|
|
23
|
+
* `is-open` class). Escape dispatches a cancelable `bronto:modal:close`
|
|
24
24
|
* ({@link ModalCloseDetail}) on the modal so the consumer can drop `is-open` in
|
|
25
|
-
* response; the behavior never changes visibility itself.
|
|
25
|
+
* response; the behavior never changes visibility itself. Closing a parent
|
|
26
|
+
* modal temporarily suspends any still-`is-open` controlled descendants; they
|
|
27
|
+
* resume at the top of the stack if the parent reopens.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
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.
|
|
29
|
+
* SSR-safe, idempotent per modal; returns a cleanup function.
|
|
30
30
|
*
|
|
31
31
|
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
32
32
|
* @returns {import('./internal.js').Cleanup}
|
package/behaviors/modal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAmQA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6E3C;;;;;YA3Ga,QAAQ"}
|
package/behaviors/modal.js
CHANGED
|
@@ -3,25 +3,13 @@ import {
|
|
|
3
3
|
resolveHost,
|
|
4
4
|
noop,
|
|
5
5
|
bindOnce,
|
|
6
|
+
byIdInHost,
|
|
6
7
|
collectHosts,
|
|
7
8
|
focusInto,
|
|
8
9
|
closestSafe,
|
|
9
10
|
} from './internal.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const classPanel = closestSafe(target, '.ui-popover.is-open');
|
|
13
|
-
if (classPanel && modal.contains(classPanel)) return true;
|
|
14
|
-
|
|
15
|
-
const nativePanel = closestSafe(target, '[popover]');
|
|
16
|
-
if (!nativePanel || !modal.contains(nativePanel)) return false;
|
|
17
|
-
try {
|
|
18
|
-
return nativePanel.matches(':popover-open');
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const activeModals = [];
|
|
12
|
+
const states = new WeakMap();
|
|
25
13
|
|
|
26
14
|
const snapshotAttrs = (el, names) => {
|
|
27
15
|
const attrs = {};
|
|
@@ -41,16 +29,233 @@ const restoreAttrs = (el, attrs) => {
|
|
|
41
29
|
}
|
|
42
30
|
};
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
function modalState(doc) {
|
|
33
|
+
let state = states.get(doc);
|
|
34
|
+
if (state) return state;
|
|
35
|
+
state = {
|
|
36
|
+
doc,
|
|
37
|
+
records: new Set(),
|
|
38
|
+
byModal: new Map(),
|
|
39
|
+
ownedInert: new Set(),
|
|
40
|
+
pendingOverlays: new Set(),
|
|
41
|
+
observer: null,
|
|
42
|
+
observing: false,
|
|
43
|
+
sequence: 0,
|
|
44
|
+
scheduled: false,
|
|
45
|
+
top: null,
|
|
46
|
+
};
|
|
47
|
+
states.set(doc, state);
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
function isEffectivelyActive(record, state) {
|
|
52
|
+
if (!record.active || !record.modal.isConnected) return false;
|
|
53
|
+
for (let node = record.modal.parentElement; node; node = node.parentElement) {
|
|
54
|
+
const ancestor = state.byModal.get(node);
|
|
55
|
+
if (ancestor && (!ancestor.active || !ancestor.modal.isConnected)) return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function activeTop(state) {
|
|
61
|
+
let top = null;
|
|
62
|
+
for (const record of state.records) {
|
|
63
|
+
if (!isEffectivelyActive(record, state)) continue;
|
|
64
|
+
if (!top || top.modal.contains(record.modal)) {
|
|
65
|
+
top = record;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!record.modal.contains(top.modal) && record.openedAt > top.openedAt) top = record;
|
|
69
|
+
}
|
|
70
|
+
return top;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function popoverIsOpen(panel, state) {
|
|
74
|
+
if (state.pendingOverlays.has(panel) || panel.classList.contains('is-open')) return true;
|
|
75
|
+
try {
|
|
76
|
+
return panel.matches(':popover-open');
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function modalOwnsPopover(modal, panel) {
|
|
83
|
+
if (modal.contains(panel)) return true;
|
|
84
|
+
if (!panel.id) return false;
|
|
85
|
+
return collectHosts(modal, '[data-bronto-popover]').some(
|
|
86
|
+
(trigger) => trigger.getAttribute('data-bronto-popover') === panel.id,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function liveRoots(state, top) {
|
|
91
|
+
const roots = new Set([top.modal]);
|
|
92
|
+
for (const trigger of collectHosts(top.modal, '[data-bronto-popover]')) {
|
|
93
|
+
const panel = byIdInHost(top.modal, trigger.getAttribute('data-bronto-popover'));
|
|
94
|
+
if (panel && popoverIsOpen(panel, state)) roots.add(panel);
|
|
95
|
+
}
|
|
96
|
+
for (const panel of state.pendingOverlays) {
|
|
97
|
+
if (modalOwnsPopover(top.modal, panel)) roots.add(panel);
|
|
98
|
+
}
|
|
99
|
+
return roots;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function desiredInert(state, top) {
|
|
103
|
+
const body = state.doc.body;
|
|
104
|
+
if (!body || !top) return new Set();
|
|
105
|
+
const roots = liveRoots(state, top);
|
|
106
|
+
const livePath = new Set([body]);
|
|
107
|
+
for (const root of roots) {
|
|
108
|
+
for (let node = root; node && node !== body; node = node.parentElement) {
|
|
109
|
+
livePath.add(node);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const desired = new Set();
|
|
114
|
+
for (const parent of livePath) {
|
|
115
|
+
if (roots.has(parent)) continue;
|
|
116
|
+
for (const child of parent.children || []) {
|
|
117
|
+
if (!livePath.has(child)) desired.add(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return desired;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function reconcileInert(state) {
|
|
124
|
+
const previousTop = state.top;
|
|
125
|
+
const nextTop = activeTop(state);
|
|
126
|
+
const desired = desiredInert(state, nextTop);
|
|
127
|
+
|
|
128
|
+
for (const element of [...state.ownedInert]) {
|
|
129
|
+
if (desired.has(element)) {
|
|
130
|
+
if (!element.inert) element.inert = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
element.inert = false;
|
|
134
|
+
state.ownedInert.delete(element);
|
|
135
|
+
}
|
|
136
|
+
for (const element of desired) {
|
|
137
|
+
if (state.ownedInert.has(element) || element.inert) continue;
|
|
138
|
+
element.inert = true;
|
|
139
|
+
state.ownedInert.add(element);
|
|
140
|
+
}
|
|
141
|
+
state.top = nextTop;
|
|
142
|
+
return { previousTop, nextTop };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scheduleReconcile(state) {
|
|
146
|
+
if (state.scheduled) return;
|
|
147
|
+
state.scheduled = true;
|
|
148
|
+
queueMicrotask(() => {
|
|
149
|
+
state.scheduled = false;
|
|
150
|
+
reconcileInert(state);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ensureDocumentObserver(state) {
|
|
155
|
+
if (state.observing) return;
|
|
156
|
+
state.observing = true;
|
|
157
|
+
const view = state.doc.defaultView;
|
|
158
|
+
const Observer = view?.MutationObserver;
|
|
159
|
+
if (Observer && state.doc.body) {
|
|
160
|
+
state.observer = new Observer((mutations) => {
|
|
161
|
+
const relevant = mutations.some(
|
|
162
|
+
(mutation) =>
|
|
163
|
+
mutation.type === 'childList' || mutation.target.matches?.('.ui-popover, [popover]'),
|
|
164
|
+
);
|
|
165
|
+
if (relevant && state.top) scheduleReconcile(state);
|
|
166
|
+
});
|
|
167
|
+
state.observer.observe(state.doc.body, {
|
|
168
|
+
attributes: true,
|
|
169
|
+
attributeFilter: ['class'],
|
|
170
|
+
childList: true,
|
|
171
|
+
subtree: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
state.onBeforeToggle = (event) => {
|
|
176
|
+
const panel = event.target;
|
|
177
|
+
if (!panel?.matches?.('[popover]')) return;
|
|
178
|
+
if (event.newState === 'open') state.pendingOverlays.add(panel);
|
|
179
|
+
else state.pendingOverlays.delete(panel);
|
|
180
|
+
reconcileInert(state);
|
|
181
|
+
};
|
|
182
|
+
state.onToggle = (event) => {
|
|
183
|
+
state.pendingOverlays.delete(event.target);
|
|
184
|
+
scheduleReconcile(state);
|
|
185
|
+
};
|
|
186
|
+
state.doc.addEventListener('beforetoggle', state.onBeforeToggle, true);
|
|
187
|
+
state.doc.addEventListener('toggle', state.onToggle, true);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function releaseDocumentState(state) {
|
|
191
|
+
if (state.records.size) return;
|
|
192
|
+
state.observer?.disconnect();
|
|
193
|
+
state.observer = null;
|
|
194
|
+
state.observing = false;
|
|
195
|
+
state.doc.removeEventListener('beforetoggle', state.onBeforeToggle, true);
|
|
196
|
+
state.doc.removeEventListener('toggle', state.onToggle, true);
|
|
197
|
+
state.pendingOverlays.clear();
|
|
198
|
+
reconcileInert(state);
|
|
199
|
+
states.delete(state.doc);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function restoreAfterClose(record, nextTop) {
|
|
203
|
+
const back = record.opener;
|
|
204
|
+
record.opener = null;
|
|
205
|
+
if (nextTop) {
|
|
206
|
+
if (back?.isConnected && nextTop.modal.contains(back)) back.focus?.();
|
|
207
|
+
else focusInto(nextTop.modal);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (back?.isConnected && typeof back.focus === 'function') back.focus();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function syncRecord(record) {
|
|
214
|
+
const { modal, state } = record;
|
|
215
|
+
const nextActive = modal.classList.contains('is-open');
|
|
216
|
+
if (nextActive === record.active) {
|
|
217
|
+
if (!record.isNativeDialog) modal.hidden = !nextActive;
|
|
218
|
+
scheduleReconcile(state);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const oldTop = state.top ?? activeTop(state);
|
|
223
|
+
record.active = nextActive;
|
|
224
|
+
if (nextActive) {
|
|
225
|
+
record.opener = state.doc.activeElement;
|
|
226
|
+
record.openedAt = ++state.sequence;
|
|
227
|
+
if (!record.isNativeDialog) modal.hidden = false;
|
|
228
|
+
const { nextTop } = reconcileInert(state);
|
|
229
|
+
if (nextTop && (nextTop === record || modal.contains(nextTop.modal))) {
|
|
230
|
+
focusInto(nextTop.modal);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!record.isNativeDialog) modal.hidden = true;
|
|
236
|
+
const closedOwnedTop = oldTop && (oldTop === record || modal.contains(oldTop.modal));
|
|
237
|
+
const { nextTop } = reconcileInert(state);
|
|
238
|
+
if (closedOwnedTop) restoreAfterClose(record, nextTop);
|
|
239
|
+
else record.opener = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function unregisterRecord(record) {
|
|
243
|
+
const { state, modal } = record;
|
|
244
|
+
const oldTop = state.top ?? activeTop(state);
|
|
245
|
+
const removedOwnedTop = oldTop && (oldTop === record || modal.contains(oldTop.modal));
|
|
246
|
+
record.active = false;
|
|
247
|
+
state.records.delete(record);
|
|
248
|
+
state.byModal.delete(modal);
|
|
249
|
+
const { nextTop } = reconcileInert(state);
|
|
250
|
+
if (removedOwnedTop) restoreAfterClose(record, nextTop);
|
|
251
|
+
else record.opener = null;
|
|
252
|
+
releaseDocumentState(state);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function targetIsOwnedPopover(target, modal, state) {
|
|
256
|
+
const panel = closestSafe(target, '.ui-popover, [popover]');
|
|
257
|
+
return Boolean(panel && popoverIsOpen(panel, state) && modalOwnsPopover(modal, panel));
|
|
258
|
+
}
|
|
54
259
|
|
|
55
260
|
/**
|
|
56
261
|
* @typedef {object} ModalCloseDetail
|
|
@@ -67,21 +272,21 @@ const removeActiveModal = (modal) => {
|
|
|
67
272
|
*
|
|
68
273
|
* Mark the overlay `[data-bronto-modal]` (opt-in). On bind it gives the modal a
|
|
69
274
|
* `role="dialog"` + `aria-modal="true"` (unless the author set a role) and
|
|
70
|
-
* dev-warns when it has no accessible name
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* focus only: the **consumer still owns open/close state** (the
|
|
78
|
-
* class). Escape dispatches a cancelable `bronto:modal:close`
|
|
275
|
+
* dev-warns when it has no accessible name. While open, a document-level stack
|
|
276
|
+
* reconciler keeps only the top controlled modal interactive, marks the rest of
|
|
277
|
+
* the page `inert`, admits an open popover owned by the top modal even when the
|
|
278
|
+
* panel is portaled elsewhere, and applies the trap to background nodes added
|
|
279
|
+
* after open. Nested and sibling portal modals therefore share one ownership
|
|
280
|
+
* model instead of independently inverting each other's `inert` state.
|
|
281
|
+
*
|
|
282
|
+
* Bronto owns focus only: the **consumer still owns open/close state** (the
|
|
283
|
+
* `is-open` class). Escape dispatches a cancelable `bronto:modal:close`
|
|
79
284
|
* ({@link ModalCloseDetail}) on the modal so the consumer can drop `is-open` in
|
|
80
|
-
* response; the behavior never changes visibility itself.
|
|
285
|
+
* response; the behavior never changes visibility itself. Closing a parent
|
|
286
|
+
* modal temporarily suspends any still-`is-open` controlled descendants; they
|
|
287
|
+
* resume at the top of the stack if the parent reopens.
|
|
81
288
|
*
|
|
82
|
-
*
|
|
83
|
-
* case); a deeply-nested modal still gets focus-into, focus-return, and the
|
|
84
|
-
* Escape signal. SSR-safe, idempotent per modal; returns a cleanup function.
|
|
289
|
+
* SSR-safe, idempotent per modal; returns a cleanup function.
|
|
85
290
|
*
|
|
86
291
|
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
87
292
|
* @returns {import('./internal.js').Cleanup}
|
|
@@ -94,64 +299,22 @@ export function initModal({ root } = {}) {
|
|
|
94
299
|
const cleanups = [];
|
|
95
300
|
|
|
96
301
|
for (const modal of modals) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// Inert every sibling at each ancestor level up to <body>: the rest of the
|
|
101
|
-
// page becomes non-focusable/non-interactive while the modal subtree stays
|
|
102
|
-
// live. Skip already-inert nodes so release() can't un-inert something the
|
|
103
|
-
// app inerted for its own reasons.
|
|
104
|
-
const trap = () => {
|
|
105
|
-
if (opener) return; // already trapped
|
|
106
|
-
opener = document.activeElement;
|
|
107
|
-
pushActiveModal(modal);
|
|
108
|
-
let el = modal;
|
|
109
|
-
while (el && el.parentElement && el !== document.body) {
|
|
110
|
-
for (const sib of el.parentElement.children) {
|
|
111
|
-
if (sib !== el && !sib.inert) {
|
|
112
|
-
sib.inert = true;
|
|
113
|
-
inerted.push(sib);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
el = el.parentElement;
|
|
117
|
-
}
|
|
118
|
-
focusInto(modal);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const release = () => {
|
|
122
|
-
if (!opener) return;
|
|
123
|
-
removeActiveModal(modal);
|
|
124
|
-
for (const el of inerted) el.inert = false;
|
|
125
|
-
inerted = [];
|
|
126
|
-
const back = opener;
|
|
127
|
-
opener = null;
|
|
128
|
-
if (back?.isConnected && typeof back.focus === 'function') back.focus();
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const sync = () => (modal.classList.contains('is-open') ? trap() : release());
|
|
132
|
-
|
|
133
|
-
const onKey = (e) => {
|
|
134
|
-
if (e.key === 'Escape' && opener) {
|
|
135
|
-
if (activeModals.at(-1) !== modal) return;
|
|
136
|
-
if (insideOpenPopover(e.target, modal)) return;
|
|
137
|
-
modal.dispatchEvent(
|
|
138
|
-
new CustomEvent('bronto:modal:close', {
|
|
139
|
-
detail: { reason: 'escape' },
|
|
140
|
-
bubbles: true,
|
|
141
|
-
cancelable: true,
|
|
142
|
-
}),
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
302
|
+
const doc = modal.ownerDocument;
|
|
303
|
+
if (!doc) continue;
|
|
304
|
+
const view = doc.defaultView;
|
|
147
305
|
cleanups.push(
|
|
148
306
|
bindOnce(modal, 'modal', () => {
|
|
149
|
-
const attrs = snapshotAttrs(modal, ['role', 'aria-modal', 'tabindex']);
|
|
307
|
+
const attrs = snapshotAttrs(modal, ['role', 'aria-modal', 'tabindex', 'hidden']);
|
|
308
|
+
const state = modalState(doc);
|
|
309
|
+
const record = {
|
|
310
|
+
active: modal.classList.contains('is-open'),
|
|
311
|
+
isNativeDialog: modal.localName === 'dialog',
|
|
312
|
+
modal,
|
|
313
|
+
openedAt: 0,
|
|
314
|
+
opener: null,
|
|
315
|
+
state,
|
|
316
|
+
};
|
|
150
317
|
|
|
151
|
-
// A controlled modal must announce AS a modal dialog, not a generic group —
|
|
152
|
-
// parity with initPopover. Apply a dialog role + aria-modal (unless the
|
|
153
|
-
// author set a role), and dev-warn on a missing accessible name since we
|
|
154
|
-
// can't invent a good one.
|
|
155
318
|
if (!modal.hasAttribute('role')) modal.setAttribute('role', 'dialog');
|
|
156
319
|
if (!modal.hasAttribute('aria-modal')) modal.setAttribute('aria-modal', 'true');
|
|
157
320
|
const named =
|
|
@@ -164,14 +327,38 @@ export function initModal({ root } = {}) {
|
|
|
164
327
|
);
|
|
165
328
|
}
|
|
166
329
|
|
|
167
|
-
|
|
330
|
+
if (record.active) {
|
|
331
|
+
record.opener = doc.activeElement;
|
|
332
|
+
record.openedAt = ++state.sequence;
|
|
333
|
+
}
|
|
334
|
+
if (!record.isNativeDialog) modal.hidden = !record.active;
|
|
335
|
+
state.records.add(record);
|
|
336
|
+
state.byModal.set(modal, record);
|
|
337
|
+
ensureDocumentObserver(state);
|
|
338
|
+
reconcileInert(state);
|
|
339
|
+
if (state.top === record) focusInto(modal);
|
|
340
|
+
|
|
341
|
+
const Observer = view?.MutationObserver;
|
|
342
|
+
const observer = Observer ? new Observer(() => syncRecord(record)) : null;
|
|
168
343
|
observer?.observe(modal, { attributes: true, attributeFilter: ['class'] });
|
|
169
|
-
|
|
170
|
-
|
|
344
|
+
const onKey = (event) => {
|
|
345
|
+
if (event.key !== 'Escape' || state.top !== record) return;
|
|
346
|
+
if (targetIsOwnedPopover(event.target, modal, state)) return;
|
|
347
|
+
const ModalCloseEvent = view?.CustomEvent ?? CustomEvent;
|
|
348
|
+
modal.dispatchEvent(
|
|
349
|
+
new ModalCloseEvent('bronto:modal:close', {
|
|
350
|
+
detail: { reason: 'escape' },
|
|
351
|
+
bubbles: true,
|
|
352
|
+
cancelable: true,
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
doc.addEventListener('keydown', onKey, true);
|
|
357
|
+
|
|
171
358
|
return () => {
|
|
172
359
|
observer?.disconnect();
|
|
173
|
-
|
|
174
|
-
|
|
360
|
+
doc.removeEventListener('keydown', onKey, true);
|
|
361
|
+
unregisterRecord(record);
|
|
175
362
|
restoreAttrs(modal, attrs);
|
|
176
363
|
};
|
|
177
364
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAoP3C"}
|