@ponchia/ui 0.6.7 → 0.6.9

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +129 -4
  2. package/README.md +4 -4
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +26 -9
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +145 -49
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +220 -92
  9. package/behaviors/command.d.ts.map +1 -1
  10. package/behaviors/command.js +74 -14
  11. package/behaviors/connectors.d.ts.map +1 -1
  12. package/behaviors/connectors.js +131 -32
  13. package/behaviors/crosshair.d.ts.map +1 -1
  14. package/behaviors/crosshair.js +47 -1
  15. package/behaviors/dialog.d.ts.map +1 -1
  16. package/behaviors/dialog.js +24 -9
  17. package/behaviors/disclosure.d.ts.map +1 -1
  18. package/behaviors/disclosure.js +33 -3
  19. package/behaviors/dismissible.d.ts.map +1 -1
  20. package/behaviors/dismissible.js +3 -2
  21. package/behaviors/forms.d.ts.map +1 -1
  22. package/behaviors/forms.js +211 -140
  23. package/behaviors/glyph.d.ts.map +1 -1
  24. package/behaviors/glyph.js +172 -132
  25. package/behaviors/inert.d.ts +1 -1
  26. package/behaviors/inert.d.ts.map +1 -1
  27. package/behaviors/inert.js +4 -3
  28. package/behaviors/internal.d.ts.map +1 -1
  29. package/behaviors/internal.js +4 -3
  30. package/behaviors/legend.d.ts +0 -5
  31. package/behaviors/legend.d.ts.map +1 -1
  32. package/behaviors/legend.js +45 -13
  33. package/behaviors/menu.d.ts.map +1 -1
  34. package/behaviors/menu.js +13 -8
  35. package/behaviors/modal.d.ts.map +1 -1
  36. package/behaviors/modal.js +77 -19
  37. package/behaviors/popover.d.ts +4 -3
  38. package/behaviors/popover.d.ts.map +1 -1
  39. package/behaviors/popover.js +94 -14
  40. package/behaviors/sources.d.ts.map +1 -1
  41. package/behaviors/sources.js +14 -2
  42. package/behaviors/splitter.d.ts.map +1 -1
  43. package/behaviors/splitter.js +149 -110
  44. package/behaviors/spotlight.d.ts.map +1 -1
  45. package/behaviors/spotlight.js +28 -2
  46. package/behaviors/table.d.ts +1 -1
  47. package/behaviors/table.d.ts.map +1 -1
  48. package/behaviors/table.js +108 -17
  49. package/behaviors/tabs.d.ts.map +1 -1
  50. package/behaviors/tabs.js +84 -20
  51. package/behaviors/theme.d.ts.map +1 -1
  52. package/behaviors/theme.js +25 -5
  53. package/behaviors/toast.js +5 -5
  54. package/classes/index.d.ts +15 -2
  55. package/classes/index.js +48 -35
  56. package/connectors/index.d.ts +41 -8
  57. package/connectors/index.d.ts.map +1 -1
  58. package/connectors/index.js +74 -19
  59. package/css/annotations.css +12 -0
  60. package/css/app.css +3 -4
  61. package/css/base.css +1 -1
  62. package/css/content.css +3 -3
  63. package/css/crosshair.css +27 -2
  64. package/css/disclosure.css +3 -3
  65. package/css/dots.css +4 -4
  66. package/css/feedback.css +8 -37
  67. package/css/forms.css +9 -12
  68. package/css/legend.css +1 -1
  69. package/css/marks.css +1 -1
  70. package/css/motion.css +6 -6
  71. package/css/navigation.css +12 -0
  72. package/css/overlay.css +5 -7
  73. package/css/primitives.css +14 -16
  74. package/css/sidenote.css +2 -2
  75. package/css/table.css +2 -2
  76. package/css/tokens.css +16 -0
  77. package/dist/bronto.css +1 -1
  78. package/dist/css/analytical.css +1 -1
  79. package/dist/css/annotations.css +1 -1
  80. package/dist/css/crosshair.css +1 -1
  81. package/dist/css/feedback.css +1 -1
  82. package/dist/css/navigation.css +1 -1
  83. package/dist/css/report-kit.css +1 -1
  84. package/dist/css/tokens.css +1 -1
  85. package/docs/adr/0001-color-system.md +3 -2
  86. package/docs/annotations.md +21 -1
  87. package/docs/architecture.md +74 -13
  88. package/docs/command.md +4 -1
  89. package/docs/connectors.md +16 -0
  90. package/docs/crosshair.md +1 -1
  91. package/docs/dots.md +4 -1
  92. package/docs/glyphs.md +11 -0
  93. package/docs/interop/react-flow.md +89 -0
  94. package/docs/migrations/0.2-to-0.3.md +1 -1
  95. package/docs/package-contract.md +7 -5
  96. package/docs/reporting.md +23 -12
  97. package/docs/stability.md +85 -9
  98. package/docs/theming.md +2 -2
  99. package/docs/usage.md +16 -2
  100. package/docs/vega.md +4 -4
  101. package/glyphs/glyphs.js +43 -33
  102. package/llms.txt +19 -8
  103. package/package.json +23 -4
  104. package/schemas/report-claims.v1.schema.json +1 -1
  105. package/svelte/index.d.ts +71 -45
  106. package/svelte/index.d.ts.map +1 -1
  107. package/svelte/index.js +29 -2
  108. package/tokens/index.js +2 -2
  109. package/vue/index.d.ts +42 -5
  110. package/vue/index.d.ts.map +1 -1
  111. package/vue/index.js +32 -1
@@ -1,11 +1,35 @@
1
1
  import { hasDom, resolveHost, noop, collectHosts } from './internal.js';
2
2
  import { GLYPH_SIZE, glyphCells, glyphMask } from '../glyphs/glyphs.js';
3
3
 
4
+ const GLYPH_CLEANUP = Symbol('bronto-glyph-cleanup');
5
+
4
6
  function restoreAttr(el, name, prev) {
5
7
  if (prev === null) el.removeAttribute(name);
6
8
  else el.setAttribute(name, prev);
7
9
  }
8
10
 
11
+ function restoreStyleProp(el, name, prev) {
12
+ if (prev) el.style.setProperty(name, prev);
13
+ else el.style.removeProperty(name);
14
+ }
15
+
16
+ function cleanupEmptyClassAndStyle(el) {
17
+ if (el.getAttribute('class') === '') el.removeAttribute('class');
18
+ if (el.getAttribute('style') === '') el.removeAttribute('style');
19
+ }
20
+
21
+ function rememberCleanup(el, cleanups, cleanup) {
22
+ let done = false;
23
+ const wrapped = () => {
24
+ if (done) return;
25
+ done = true;
26
+ cleanup();
27
+ if (el[GLYPH_CLEANUP] === wrapped) delete el[GLYPH_CLEANUP];
28
+ };
29
+ el[GLYPH_CLEANUP] = wrapped;
30
+ cleanups.push(wrapped);
31
+ }
32
+
9
33
  // `dot`/`gap`/`size` land in inline CSS, so allow only length/calc syntax —
10
34
  // drop anything with a `;`/`{` that could open a second declaration (mirrors
11
35
  // glyphs.js cssLen). Used for the mask path's --icon-size.
@@ -13,6 +37,151 @@ function cssLen(v) {
13
37
  return v && /^[\w.%+\-*/()\s,]+$/.test(v) ? v : '';
14
38
  }
15
39
 
40
+ function applyGlyphA11y(el, label) {
41
+ if (label) {
42
+ el.setAttribute('role', 'img');
43
+ el.setAttribute('aria-label', label);
44
+ el.removeAttribute('aria-hidden');
45
+ } else {
46
+ el.setAttribute('aria-hidden', 'true');
47
+ }
48
+ }
49
+
50
+ function expandMaskGlyph(el, name, label, cleanups) {
51
+ if (el.classList.contains('ui-icon') && el.style.getPropertyValue('--icon-mask')) return;
52
+ const mask = glyphMask(name);
53
+ if (!mask) return; // unknown glyph — leave the placeholder as-is
54
+
55
+ const hadIcon = el.classList.contains('ui-icon');
56
+ const hadMask = el.style.getPropertyValue('--icon-mask');
57
+ const hadSize = el.style.getPropertyValue('--icon-size');
58
+ const hadAriaHidden = el.getAttribute('aria-hidden');
59
+ const hadRole = el.getAttribute('role');
60
+ const hadAriaLabel = el.getAttribute('aria-label');
61
+ const size = cssLen(el.getAttribute('data-bronto-glyph-size'));
62
+
63
+ el.classList.add('ui-icon');
64
+ el.style.setProperty('--icon-mask', mask);
65
+ if (size) el.style.setProperty('--icon-size', size);
66
+ applyGlyphA11y(el, label);
67
+
68
+ rememberCleanup(el, cleanups, () => {
69
+ if (!hadIcon) el.classList.remove('ui-icon');
70
+ restoreStyleProp(el, '--icon-mask', hadMask);
71
+ restoreStyleProp(el, '--icon-size', hadSize);
72
+ restoreAttr(el, 'aria-hidden', hadAriaHidden);
73
+ restoreAttr(el, 'role', hadRole);
74
+ restoreAttr(el, 'aria-label', hadAriaLabel);
75
+ cleanupEmptyClassAndStyle(el);
76
+ });
77
+ }
78
+
79
+ function glyphAnimClass(animAttr) {
80
+ if (animAttr === 'reveal') return 'ui-dotmatrix--reveal';
81
+ if (animAttr === 'pulse') return 'ui-dotmatrix--pulse';
82
+ return null;
83
+ }
84
+
85
+ function authoredDotSize(el) {
86
+ return (
87
+ el.style.getPropertyValue('--dotmatrix-dot') ||
88
+ (typeof getComputedStyle === 'function'
89
+ ? getComputedStyle(el).getPropertyValue('--dotmatrix-dot').trim()
90
+ : '')
91
+ );
92
+ }
93
+
94
+ function applyDefaultDotScale(el, solid, hadGap) {
95
+ const setDefaultDot = !authoredDotSize(el);
96
+ let setDefaultGap = false;
97
+ if (setDefaultDot) {
98
+ el.style.setProperty('--dotmatrix-dot', '0.08em');
99
+ if (!solid && !hadGap) {
100
+ el.style.setProperty('--dotmatrix-gap', '0.02em'); // tight, so it reads as one glyph
101
+ setDefaultGap = true;
102
+ }
103
+ }
104
+ return { setDefaultDot, setDefaultGap };
105
+ }
106
+
107
+ function dotCellClass(cell) {
108
+ if (!cell.on) return 'ui-dotmatrix__cell';
109
+ if (cell.tone === 'hot') return 'ui-dotmatrix__cell ui-dotmatrix__cell--hot';
110
+ if (cell.tone === 'accent') return 'ui-dotmatrix__cell ui-dotmatrix__cell--accent';
111
+ return 'ui-dotmatrix__cell';
112
+ }
113
+
114
+ function appendGlyphCells(el, cells, { solid, animAttr }) {
115
+ const frag = document.createDocumentFragment();
116
+ cells.forEach((cell, i) => {
117
+ const span = document.createElement('span');
118
+ span.className = dotCellClass(cell);
119
+ if (!cell.on && solid) span.style.background = 'transparent'; // glyph-only
120
+ if (animAttr === 'reveal') span.style.setProperty('--i', String(i)); // scan stagger
121
+ frag.appendChild(span);
122
+ });
123
+ el.appendChild(frag);
124
+ }
125
+
126
+ function expandCellGlyph(el, name, label, cleanups) {
127
+ // Scope to DIRECT-child cells (the ones we append) — so a placeholder that
128
+ // legitimately nests its own .ui-dotmatrix is neither mis-read as already
129
+ // expanded here nor have its inner cells removed by cleanup below.
130
+ if (el.querySelector(':scope > .ui-dotmatrix__cell')) return; // already expanded
131
+ const cells = glyphCells(name);
132
+ if (!cells.length) return; // unknown glyph — leave the placeholder as-is
133
+
134
+ // `data-bronto-glyph-solid` → square, gapless pixel glyph (legible small),
135
+ // the DOM counterpart to renderGlyph's `solid` option. Implies glyph-only.
136
+ const solid = el.hasAttribute('data-bronto-glyph-solid');
137
+ // `data-bronto-glyph-anim="reveal|pulse"` → decorative animation (the DOM
138
+ // counterpart to renderGlyph's `anim`; reduced-motion-safe via CSS).
139
+ const animAttr = el.getAttribute('data-bronto-glyph-anim');
140
+ const animClass = glyphAnimClass(animAttr);
141
+ const hadAnimClass = animClass ? el.classList.contains(animClass) : false;
142
+ const hadMatrix = el.classList.contains('ui-dotmatrix');
143
+ const hadCols = el.style.getPropertyValue('--dotmatrix-cols');
144
+ const hadRadius = el.style.getPropertyValue('--dotmatrix-dot-radius');
145
+ const hadGap = el.style.getPropertyValue('--dotmatrix-gap');
146
+ const hadAriaHidden = el.getAttribute('aria-hidden');
147
+ const hadRole = el.getAttribute('role');
148
+ const hadAriaLabel = el.getAttribute('aria-label');
149
+
150
+ el.classList.add('ui-dotmatrix');
151
+ if (animClass) el.classList.add(animClass);
152
+ el.style.setProperty('--dotmatrix-cols', String(GLYPH_SIZE));
153
+ if (solid) {
154
+ el.style.setProperty('--dotmatrix-dot-radius', '0');
155
+ el.style.setProperty('--dotmatrix-gap', '0');
156
+ }
157
+
158
+ // Without a track size the grid cells default to `1fr`, so the 16×16 matrix
159
+ // balloons to fill its container (full-bleed) — asymmetric with the mask
160
+ // path's safe 1em. If the author set no `--dotmatrix-dot` (inline OR via the
161
+ // cascade), default it to an intrinsic icon scale so a forgotten size
162
+ // degrades to ~icon, not full-bleed.
163
+ const defaults = applyDefaultDotScale(el, solid, hadGap);
164
+ applyGlyphA11y(el, label);
165
+ appendGlyphCells(el, cells, { solid, animAttr });
166
+
167
+ rememberCleanup(el, cleanups, () => {
168
+ el.querySelectorAll(':scope > .ui-dotmatrix__cell').forEach((n) => n.remove());
169
+ if (!hadMatrix) el.classList.remove('ui-dotmatrix');
170
+ if (animClass && !hadAnimClass) el.classList.remove(animClass);
171
+ if (solid) {
172
+ restoreStyleProp(el, '--dotmatrix-dot-radius', hadRadius);
173
+ restoreStyleProp(el, '--dotmatrix-gap', hadGap);
174
+ }
175
+ if (defaults.setDefaultDot) el.style.removeProperty('--dotmatrix-dot');
176
+ if (defaults.setDefaultGap) el.style.removeProperty('--dotmatrix-gap');
177
+ restoreStyleProp(el, '--dotmatrix-cols', hadCols);
178
+ restoreAttr(el, 'aria-hidden', hadAriaHidden);
179
+ restoreAttr(el, 'role', hadRole);
180
+ restoreAttr(el, 'aria-label', hadAriaLabel);
181
+ cleanupEmptyClassAndStyle(el);
182
+ });
183
+ }
184
+
16
185
  /**
17
186
  * Expand `[data-bronto-glyph="name"]` placeholders into a `.ui-dotmatrix`
18
187
  * grid of GLYPH_SIZE² cells — the DOM counterpart to renderGlyph() from
@@ -40,146 +209,17 @@ export function initDotGlyph({ root } = {}) {
40
209
  const cleanups = [];
41
210
 
42
211
  for (const el of els) {
212
+ el[GLYPH_CLEANUP]?.();
43
213
  const name = el.getAttribute('data-bronto-glyph');
44
214
  const label = el.getAttribute('data-bronto-glyph-label');
45
215
 
46
216
  // One-node mask path — the icon-at-scale counterpart to the 256-cell grid.
47
217
  if (el.getAttribute('data-bronto-glyph-render') === 'mask') {
48
- if (el.classList.contains('ui-icon') && el.style.getPropertyValue('--icon-mask')) continue;
49
- const mask = glyphMask(name);
50
- if (!mask) continue; // unknown glyph — leave the placeholder as-is
51
- const hadIcon = el.classList.contains('ui-icon');
52
- const hadMask = el.style.getPropertyValue('--icon-mask');
53
- const hadSize = el.style.getPropertyValue('--icon-size');
54
- const hadAriaHiddenM = el.getAttribute('aria-hidden');
55
- const hadRoleM = el.getAttribute('role');
56
- const hadAriaLabelM = el.getAttribute('aria-label');
57
- const sizeM = cssLen(el.getAttribute('data-bronto-glyph-size'));
58
-
59
- el.classList.add('ui-icon');
60
- el.style.setProperty('--icon-mask', mask);
61
- if (sizeM) el.style.setProperty('--icon-size', sizeM);
62
- if (label) {
63
- el.setAttribute('role', 'img');
64
- el.setAttribute('aria-label', label);
65
- el.removeAttribute('aria-hidden');
66
- } else {
67
- el.setAttribute('aria-hidden', 'true');
68
- }
69
-
70
- cleanups.push(() => {
71
- if (!hadIcon) el.classList.remove('ui-icon');
72
- if (hadMask) el.style.setProperty('--icon-mask', hadMask);
73
- else el.style.removeProperty('--icon-mask');
74
- if (sizeM && !hadSize) el.style.removeProperty('--icon-size');
75
- else if (hadSize) el.style.setProperty('--icon-size', hadSize);
76
- restoreAttr(el, 'aria-hidden', hadAriaHiddenM);
77
- restoreAttr(el, 'role', hadRoleM);
78
- restoreAttr(el, 'aria-label', hadAriaLabelM);
79
- if (el.getAttribute('class') === '') el.removeAttribute('class');
80
- if (el.getAttribute('style') === '') el.removeAttribute('style');
81
- });
218
+ expandMaskGlyph(el, name, label, cleanups);
82
219
  continue;
83
220
  }
84
221
 
85
- // Scope to DIRECT-child cells (the ones we append) — so a placeholder that
86
- // legitimately nests its own .ui-dotmatrix is neither mis-read as already
87
- // expanded here nor have its inner cells removed by cleanup below.
88
- if (el.querySelector(':scope > .ui-dotmatrix__cell')) continue; // already expanded
89
- const cells = glyphCells(name);
90
- if (!cells.length) continue; // unknown glyph — leave the placeholder as-is
91
- // `data-bronto-glyph-solid` → square, gapless pixel glyph (legible small),
92
- // the DOM counterpart to renderGlyph's `solid` option. Implies glyph-only.
93
- const solid = el.hasAttribute('data-bronto-glyph-solid');
94
- // `data-bronto-glyph-anim="reveal|pulse"` → decorative animation (the DOM
95
- // counterpart to renderGlyph's `anim`; reduced-motion-safe via CSS).
96
- const animAttr = el.getAttribute('data-bronto-glyph-anim');
97
- const animClass =
98
- animAttr === 'reveal'
99
- ? 'ui-dotmatrix--reveal'
100
- : animAttr === 'pulse'
101
- ? 'ui-dotmatrix--pulse'
102
- : null;
103
- const hadAnimClass = animClass ? el.classList.contains(animClass) : false;
104
- const hadMatrix = el.classList.contains('ui-dotmatrix');
105
- const hadCols = el.style.getPropertyValue('--dotmatrix-cols');
106
- const hadRadius = el.style.getPropertyValue('--dotmatrix-dot-radius');
107
- const hadGap = el.style.getPropertyValue('--dotmatrix-gap');
108
- const hadAriaHidden = el.getAttribute('aria-hidden');
109
- const hadRole = el.getAttribute('role');
110
- const hadAriaLabel = el.getAttribute('aria-label');
111
-
112
- el.classList.add('ui-dotmatrix');
113
- if (animClass) el.classList.add(animClass);
114
- el.style.setProperty('--dotmatrix-cols', String(GLYPH_SIZE));
115
- if (solid) {
116
- el.style.setProperty('--dotmatrix-dot-radius', '0');
117
- el.style.setProperty('--dotmatrix-gap', '0');
118
- }
119
- // Without a track size the grid cells default to `1fr`, so the 16×16 matrix
120
- // balloons to fill its container (full-bleed) — asymmetric with the mask
121
- // path's safe 1em. If the author set no `--dotmatrix-dot` (inline OR via the
122
- // cascade), default it to an intrinsic icon scale so a forgotten size
123
- // degrades to ~icon, not full-bleed. (component audit C9.)
124
- const authoredDot =
125
- el.style.getPropertyValue('--dotmatrix-dot') ||
126
- (typeof getComputedStyle === 'function'
127
- ? getComputedStyle(el).getPropertyValue('--dotmatrix-dot').trim()
128
- : '');
129
- const setDefaultDot = !authoredDot;
130
- let setDefaultGap = false;
131
- if (setDefaultDot) {
132
- el.style.setProperty('--dotmatrix-dot', '0.08em');
133
- if (!solid && !hadGap) {
134
- el.style.setProperty('--dotmatrix-gap', '0.02em'); // tight, so it reads as one glyph
135
- setDefaultGap = true;
136
- }
137
- }
138
- if (label) {
139
- el.setAttribute('role', 'img');
140
- el.setAttribute('aria-label', label);
141
- el.removeAttribute('aria-hidden'); // a labelled img must not also be hidden
142
- } else {
143
- el.setAttribute('aria-hidden', 'true');
144
- }
145
-
146
- const frag = document.createDocumentFragment();
147
- cells.forEach((c, i) => {
148
- const span = document.createElement('span');
149
- span.className = !c.on
150
- ? 'ui-dotmatrix__cell'
151
- : c.tone === 'hot'
152
- ? 'ui-dotmatrix__cell ui-dotmatrix__cell--hot'
153
- : c.tone === 'accent'
154
- ? 'ui-dotmatrix__cell ui-dotmatrix__cell--accent'
155
- : 'ui-dotmatrix__cell';
156
- if (!c.on && solid) span.style.background = 'transparent'; // glyph-only
157
- if (animAttr === 'reveal') span.style.setProperty('--i', String(i)); // scan stagger
158
- frag.appendChild(span);
159
- });
160
- el.appendChild(frag);
161
-
162
- cleanups.push(() => {
163
- el.querySelectorAll(':scope > .ui-dotmatrix__cell').forEach((n) => n.remove());
164
- if (!hadMatrix) el.classList.remove('ui-dotmatrix');
165
- if (animClass && !hadAnimClass) el.classList.remove(animClass);
166
- if (solid) {
167
- if (hadRadius) el.style.setProperty('--dotmatrix-dot-radius', hadRadius);
168
- else el.style.removeProperty('--dotmatrix-dot-radius');
169
- if (hadGap) el.style.setProperty('--dotmatrix-gap', hadGap);
170
- else el.style.removeProperty('--dotmatrix-gap');
171
- }
172
- if (setDefaultDot) el.style.removeProperty('--dotmatrix-dot');
173
- if (setDefaultGap) el.style.removeProperty('--dotmatrix-gap');
174
- if (hadCols) el.style.setProperty('--dotmatrix-cols', hadCols);
175
- else el.style.removeProperty('--dotmatrix-cols');
176
- restoreAttr(el, 'aria-hidden', hadAriaHidden);
177
- restoreAttr(el, 'role', hadRole);
178
- restoreAttr(el, 'aria-label', hadAriaLabel);
179
- // Don't leave behind empty class=""/style="" we ourselves created.
180
- if (el.getAttribute('class') === '') el.removeAttribute('class');
181
- if (el.getAttribute('style') === '') el.removeAttribute('style');
182
- });
222
+ expandCellGlyph(el, name, label, cleanups);
183
223
  }
184
224
 
185
225
  return () => cleanups.forEach((fn) => fn());
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Wire once near the root, like {@link applyStoredTheme}. Capturing listeners
13
13
  * intercept activation before any component handler (tabs, pagination, menus)
14
- * sees it. (component audit C4.)
14
+ * sees it.
15
15
  *
16
16
  * @param {import('./internal.js').DelegateOpts} [opts]
17
17
  * @returns {import('./internal.js').Cleanup}
@@ -1 +1 @@
1
- {"version":3,"file":"inert.d.ts","sourceRoot":"","sources":["inert.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,6CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAyB3C"}
1
+ {"version":3,"file":"inert.d.ts","sourceRoot":"","sources":["inert.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,6CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0B3C"}
@@ -1,4 +1,4 @@
1
- import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, closestSafe } from './internal.js';
2
2
 
3
3
  const DISABLED = '[aria-disabled="true"]';
4
4
 
@@ -15,7 +15,7 @@ const DISABLED = '[aria-disabled="true"]';
15
15
  *
16
16
  * Wire once near the root, like {@link applyStoredTheme}. Capturing listeners
17
17
  * intercept activation before any component handler (tabs, pagination, menus)
18
- * sees it. (component audit C4.)
18
+ * sees it.
19
19
  *
20
20
  * @param {import('./internal.js').DelegateOpts} [opts]
21
21
  * @returns {import('./internal.js').Cleanup}
@@ -25,9 +25,10 @@ export function initDisabledGuard({ root } = {}) {
25
25
  const host = resolveHost(root);
26
26
  if (!host) return noop;
27
27
  const block = (e) => {
28
- const el = e.target.closest?.(DISABLED);
28
+ const el = closestSafe(e.target, DISABLED);
29
29
  if (el && host.contains(el)) {
30
30
  e.preventDefault();
31
+ e.stopImmediatePropagation?.();
31
32
  e.stopPropagation();
32
33
  }
33
34
  };
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAMC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AAjIM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAOC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AAlIM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
@@ -90,7 +90,8 @@ export function byIdInHost(host, id) {
90
90
 
91
91
  export function closestSafe(el, selector) {
92
92
  try {
93
- return el.closest(selector);
93
+ const start = el?.nodeType === 1 ? el : el?.parentElement;
94
+ return start?.closest?.(selector) ?? null;
94
95
  } catch {
95
96
  return null;
96
97
  }
@@ -120,7 +121,7 @@ export function scrollIntoViewSafe(el, opts = { block: 'nearest' }) {
120
121
  // by the modal and popover focus paths (a dialog/modal must move focus into
121
122
  // itself on open). Focus the first focusable descendant, else make the
122
123
  // container programmatically focusable and focus it, so a content-only
123
- // panel/modal still receives focus. (code-quality audit Q4.)
124
+ // panel/modal still receives focus.
124
125
  const FOCUSABLE =
125
126
  'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
126
127
 
@@ -137,7 +138,7 @@ export function focusInto(container) {
137
138
  // Wrap an index by `delta` within [0, len), the roving keyboard math shared by
138
139
  // the combobox and command listboxes (a -1 `cur` lands on the first/last as
139
140
  // before). Only this core is shared — the surrounding setActive/filter/group
140
- // logic diverges between the two for real reasons. (code-quality audit Q12.)
141
+ // logic diverges between the two for real reasons.
141
142
  export function wrapIndex(cur, delta, len) {
142
143
  let next = cur + delta;
143
144
  if (next < 0) next = len - 1;
@@ -1,8 +1,3 @@
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
1
  /**
7
2
  * Wire `[data-bronto-legend]` interactive legends. Each entry is a
8
3
  * `.ui-legend__item` authored as a `<button aria-pressed>`; clicking it (or
@@ -1 +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,CAmF3C;;;;;YAvGa,MAAM,GAAG,MAAM;;;;YACf,OAAO"}
1
+ {"version":3,"file":"legend.d.ts","sourceRoot":"","sources":["legend.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAiH3C;;;;;YAvIa,MAAM,GAAG,MAAM;;;;YACf,OAAO"}
@@ -1,4 +1,4 @@
1
- import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
2
2
 
3
3
  /**
4
4
  * @typedef {object} LegendToggleDetail
@@ -6,6 +6,8 @@ import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
6
6
  * @property {boolean} active The new state (`true` ⇒ series shown).
7
7
  */
8
8
 
9
+ const handledEvents = new WeakSet();
10
+
9
11
  /**
10
12
  * Wire `[data-bronto-legend]` interactive legends. Each entry is a
11
13
  * `.ui-legend__item` authored as a `<button aria-pressed>`; clicking it (or
@@ -28,6 +30,26 @@ export function initLegend({ root } = {}) {
28
30
  if (!hasDom()) return noop;
29
31
  const host = resolveHost(root);
30
32
  if (!host) return noop;
33
+ const snapshotAttrs = (el, names) => {
34
+ const attrs = {};
35
+ for (const name of names) {
36
+ attrs[name] = {
37
+ had: el.hasAttribute(name),
38
+ value: el.getAttribute(name),
39
+ };
40
+ }
41
+ return attrs;
42
+ };
43
+ const restoreAttrs = (el, attrs) => {
44
+ for (const [name, state] of Object.entries(attrs)) {
45
+ if (state.had) el.setAttribute(name, state.value);
46
+ else el.removeAttribute(name);
47
+ }
48
+ };
49
+ const directItems = (legend) =>
50
+ [...legend.querySelectorAll('.ui-legend__item')].filter(
51
+ (el) => el.closest('[data-bronto-legend]') === legend,
52
+ );
31
53
  const isButton = (el) => el.tagName === 'BUTTON' || el.getAttribute('role') === 'button';
32
54
  const legendFor = (item) => {
33
55
  if (!item || !host.contains(item)) return;
@@ -37,11 +59,11 @@ export function initLegend({ root } = {}) {
37
59
  };
38
60
  const toggle = (item) => {
39
61
  const legend = legendFor(item);
40
- if (!legend) return;
62
+ if (!legend) return false;
41
63
  // The contract requires a real `<button>` (keyboard-operable, focusable). A
42
64
  // non-button item is mouse-only unless role=button is keyboard-normalized
43
65
  // below — refuse anything else rather than ship a pointer-only control.
44
- if (!isButton(item)) return;
66
+ if (!isButton(item)) return false;
45
67
  const active = item.getAttribute('aria-pressed') !== 'false';
46
68
  const next = !active;
47
69
  item.setAttribute('aria-pressed', String(next));
@@ -57,25 +79,33 @@ export function initLegend({ root } = {}) {
57
79
  detail: { series: item.dataset.series ?? items.indexOf(item), active: next },
58
80
  }),
59
81
  );
82
+ return true;
60
83
  };
61
84
  const onClick = (e) => {
62
- toggle(e.target.closest('.ui-legend__item'));
85
+ if (handledEvents.has(e)) return;
86
+ if (toggle(closestSafe(e.target, '.ui-legend__item'))) handledEvents.add(e);
63
87
  };
64
88
  const onKey = (e) => {
89
+ if (handledEvents.has(e)) return;
65
90
  if (e.key !== 'Enter' && e.key !== ' ') return;
66
- const item = e.target.closest('.ui-legend__item');
91
+ const item = closestSafe(e.target, '.ui-legend__item');
67
92
  if (!item || item.tagName === 'BUTTON' || item.getAttribute('role') !== 'button') return;
68
93
  e.preventDefault();
69
- toggle(item);
94
+ if (toggle(item)) handledEvents.add(e);
70
95
  };
71
96
  return bindOnce(host, 'legend', () => {
72
97
  // Normalize role=button entries and warn once per unsupported non-button
73
98
  // item present at bind. A real <button> remains the recommended markup.
74
- const legends = [...(host.querySelectorAll?.('[data-bronto-legend]') ?? [])];
99
+ const legends = collectHosts(host, '[data-bronto-legend]');
100
+ const itemStates = [];
75
101
  for (const legend of legends) {
76
- for (const el of legend.querySelectorAll('.ui-legend__item')) {
77
- if (el.closest('[data-bronto-legend]') !== legend) continue;
78
- if (el.tagName === 'BUTTON' && !el.hasAttribute('type')) el.type = 'button';
102
+ for (const el of directItems(legend)) {
103
+ itemStates.push({
104
+ el,
105
+ attrs: snapshotAttrs(el, ['type', 'tabindex', 'aria-pressed']),
106
+ inactive: el.classList.contains('is-inactive'),
107
+ });
108
+ if (el.tagName === 'BUTTON' && !el.hasAttribute('type')) el.setAttribute('type', 'button');
79
109
  if (
80
110
  el.tagName !== 'BUTTON' &&
81
111
  el.getAttribute('role') === 'button' &&
@@ -87,9 +117,7 @@ export function initLegend({ root } = {}) {
87
117
  }
88
118
  if (typeof console !== 'undefined') {
89
119
  for (const legend of legends) {
90
- const stray = [...legend.querySelectorAll('.ui-legend__item')].some(
91
- (el) => el.closest('[data-bronto-legend]') === legend && !isButton(el),
92
- );
120
+ const stray = directItems(legend).some((el) => !isButton(el));
93
121
  if (stray) {
94
122
  console.warn(
95
123
  '[bronto] initLegend(): interactive legend entries must be <button> or role="button" — unsupported .ui-legend__item controls are ignored.',
@@ -103,6 +131,10 @@ export function initLegend({ root } = {}) {
103
131
  return () => {
104
132
  host.removeEventListener('click', onClick);
105
133
  host.removeEventListener('keydown', onKey);
134
+ for (const state of itemStates) {
135
+ restoreAttrs(state.el, state.attrs);
136
+ state.el.classList.toggle('is-inactive', state.inactive);
137
+ }
106
138
  };
107
139
  });
108
140
  }
@@ -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,CAmC3C"}
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,CAwC3C"}
package/behaviors/menu.js CHANGED
@@ -1,4 +1,4 @@
1
- import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, closestSafe, collectHosts } from './internal.js';
2
2
 
3
3
  /**
4
4
  * Dropdown-menu close affordances for a native `<details data-bronto-menu>`
@@ -18,32 +18,37 @@ export function initMenu({ root } = {}) {
18
18
  if (!hasDom()) return noop;
19
19
  const host = resolveHost(root);
20
20
  if (!host) return noop;
21
- const openMenus = () => host.querySelectorAll?.('[data-bronto-menu][open]') ?? [];
21
+ const doc = host.nodeType === 9 ? host : host.ownerDocument || document;
22
+ const openMenus = () => collectHosts(host, '[data-bronto-menu][open]');
22
23
  const shut = (menu) => {
23
24
  if (!menu || !menu.open) return;
24
25
  menu.open = false;
25
26
  menu.querySelector('summary')?.focus();
26
27
  };
27
28
  const onClick = (e) => {
28
- const menu = e.target.closest('[data-bronto-menu]');
29
+ const target = e.target;
30
+ const menu = closestSafe(target, '[data-bronto-menu]');
29
31
  // Activate an item → close its menu (and return focus to summary).
30
- if (menu && e.target.closest('.ui-menu__item')) {
32
+ if (menu && host.contains(menu) && closestSafe(target, '.ui-menu__item')) {
31
33
  shut(menu);
32
34
  return;
33
35
  }
34
36
  // Click outside any open menu → close them all (no focus move).
35
- for (const m of openMenus()) if (!m.contains(e.target)) m.open = false;
37
+ for (const m of openMenus()) if (!m.contains(target)) m.open = false;
36
38
  };
37
39
  const onKey = (e) => {
38
40
  if (e.key !== 'Escape') return;
39
- const menu = e.target.closest?.('[data-bronto-menu][open]') || openMenus()[0];
41
+ const menu = closestSafe(e.target, '[data-bronto-menu][open]') || openMenus()[0];
42
+ if (!menu) return;
43
+ e.preventDefault();
44
+ e.stopPropagation();
40
45
  shut(menu);
41
46
  };
42
47
  return bindOnce(host, 'menu', () => {
43
- host.addEventListener('click', onClick);
48
+ doc.addEventListener('click', onClick);
44
49
  host.addEventListener('keydown', onKey);
45
50
  return () => {
46
- host.removeEventListener('click', onClick);
51
+ doc.removeEventListener('click', onClick);
47
52
  host.removeEventListener('keydown', onKey);
48
53
  };
49
54
  });
@@ -1 +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"}
1
+ {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAsDA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+F3C;;;;;YA7Ha,QAAQ"}