@ponchia/ui 0.6.8 → 0.6.10

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 (60) hide show
  1. package/CHANGELOG.md +79 -4
  2. package/README.md +2 -2
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +5 -6
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +100 -60
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +167 -113
  9. package/behaviors/connectors.d.ts.map +1 -1
  10. package/behaviors/connectors.js +39 -23
  11. package/behaviors/forms.d.ts.map +1 -1
  12. package/behaviors/forms.js +211 -207
  13. package/behaviors/glyph.d.ts.map +1 -1
  14. package/behaviors/glyph.js +157 -132
  15. package/behaviors/inert.d.ts +1 -1
  16. package/behaviors/inert.d.ts.map +1 -1
  17. package/behaviors/inert.js +1 -1
  18. package/behaviors/internal.js +2 -2
  19. package/behaviors/modal.js +1 -1
  20. package/behaviors/popover.js +5 -5
  21. package/behaviors/table.d.ts +1 -1
  22. package/behaviors/table.d.ts.map +1 -1
  23. package/behaviors/table.js +7 -8
  24. package/behaviors/tabs.js +2 -2
  25. package/behaviors/toast.js +5 -5
  26. package/classes/classes.json +33 -2
  27. package/classes/index.d.ts +10 -0
  28. package/classes/index.js +59 -35
  29. package/connectors/index.d.ts +2 -2
  30. package/connectors/index.d.ts.map +1 -1
  31. package/connectors/index.js +7 -10
  32. package/css/app.css +3 -4
  33. package/css/base.css +1 -1
  34. package/css/content.css +3 -3
  35. package/css/disclosure.css +3 -3
  36. package/css/dots.css +4 -4
  37. package/css/feedback.css +6 -7
  38. package/css/forms.css +9 -12
  39. package/css/legend.css +1 -1
  40. package/css/marks.css +1 -1
  41. package/css/motion.css +6 -6
  42. package/css/overlay.css +5 -7
  43. package/css/primitives.css +14 -16
  44. package/css/sidenote.css +2 -2
  45. package/css/table.css +2 -2
  46. package/css/workbench.css +128 -0
  47. package/dist/css/workbench.css +1 -1
  48. package/docs/annotations.md +36 -0
  49. package/docs/architecture.md +28 -0
  50. package/docs/interop/react-flow.md +89 -0
  51. package/docs/package-contract.md +2 -0
  52. package/docs/reference.md +21 -1
  53. package/docs/reporting.md +8 -8
  54. package/docs/stability.md +67 -7
  55. package/docs/workbench.md +56 -9
  56. package/glyphs/glyphs.js +43 -33
  57. package/llms.txt +10 -4
  58. package/package.json +5 -2
  59. package/schemas/report-claims.v1.schema.json +1 -1
  60. package/tokens/index.js +2 -2
@@ -1,5 +1,196 @@
1
1
  import { hasDom, resolveHost, noop, bindOnce, nextFieldUid, collectHosts } from './internal.js';
2
2
 
3
+ function snapshotAttrs(el, names) {
4
+ const out = {};
5
+ for (const name of names) {
6
+ out[name] = {
7
+ had: el.hasAttribute(name),
8
+ value: el.getAttribute(name),
9
+ };
10
+ }
11
+ return out;
12
+ }
13
+
14
+ function restoreAttrs(el, attrs) {
15
+ for (const [name, attr] of Object.entries(attrs)) {
16
+ if (attr.had) el.setAttribute(name, attr.value);
17
+ else el.removeAttribute(name);
18
+ }
19
+ }
20
+
21
+ function createValidationState() {
22
+ return {
23
+ priorNoValidate: new Map(),
24
+ controlState: new Map(),
25
+ slotState: new Map(),
26
+ summaryState: new Map(),
27
+ // Borrowed `.ui-hint` help text is restored after an invalid field becomes valid.
28
+ hintHelp: new WeakMap(),
29
+ };
30
+ }
31
+
32
+ function suppressNativeValidation(form, state) {
33
+ if (!state.priorNoValidate.has(form)) state.priorNoValidate.set(form, form.noValidate);
34
+ form.noValidate = true;
35
+ }
36
+
37
+ function rememberControl(control, state) {
38
+ if (!state.controlState.has(control)) {
39
+ state.controlState.set(
40
+ control,
41
+ snapshotAttrs(control, ['aria-invalid', 'aria-describedby', 'id']),
42
+ );
43
+ }
44
+ }
45
+
46
+ function rememberSlot(slot, state) {
47
+ if (!state.slotState.has(slot)) {
48
+ state.slotState.set(slot, {
49
+ attrs: snapshotAttrs(slot, ['id']),
50
+ text: slot.textContent,
51
+ hadErrorClass: slot.classList.contains('ui-hint--error'),
52
+ });
53
+ }
54
+ }
55
+
56
+ function rememberSummary(summary, state) {
57
+ if (!state.summaryState.has(summary)) {
58
+ state.summaryState.set(summary, {
59
+ attrs: snapshotAttrs(summary, ['role', 'tabindex']),
60
+ children: [...summary.childNodes],
61
+ hidden: summary.hidden,
62
+ });
63
+ }
64
+ }
65
+
66
+ function ensureId(el, prefix) {
67
+ if (!el.id) el.id = `${prefix}-${nextFieldUid()}`;
68
+ return el.id;
69
+ }
70
+
71
+ function slotFor(control) {
72
+ const field = control.closest('.ui-field');
73
+ if (!field) return null;
74
+ const dedicated = field.querySelector('[data-bronto-error]');
75
+ if (dedicated) return dedicated;
76
+ return field.querySelector('.ui-hint');
77
+ }
78
+
79
+ function link(control, slot) {
80
+ const slotId = ensureId(slot, 'bronto-err');
81
+ const ids = (control.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
82
+ if (!ids.includes(slotId)) {
83
+ ids.push(slotId);
84
+ control.setAttribute('aria-describedby', ids.join(' '));
85
+ }
86
+ }
87
+
88
+ function unlink(control, slot) {
89
+ if (!slot.id) return;
90
+ const ids = (control.getAttribute('aria-describedby') || '')
91
+ .split(/\s+/)
92
+ .filter((id) => id && id !== slot.id);
93
+ if (ids.length) control.setAttribute('aria-describedby', ids.join(' '));
94
+ else control.removeAttribute('aria-describedby');
95
+ }
96
+
97
+ function slotKind(slot) {
98
+ // Decide the slot TYPE by `[data-bronto-error]`, not by `.ui-hint`: canonical
99
+ // dedicated error markup can carry both.
100
+ const dedicated = !!slot?.matches?.('[data-bronto-error]');
101
+ const hasHintClass = !!slot?.classList.contains('ui-hint');
102
+ return { hasHintClass, borrowedHint: hasHintClass && !dedicated };
103
+ }
104
+
105
+ function clearFieldError(control, slot, kind, state) {
106
+ control.removeAttribute('aria-invalid');
107
+ if (!slot) return;
108
+ if (kind.hasHintClass) slot.classList.remove('ui-hint--error');
109
+ if (kind.borrowedHint) {
110
+ slot.textContent = state.hintHelp.get(slot) ?? '';
111
+ return;
112
+ }
113
+ slot.textContent = '';
114
+ unlink(control, slot);
115
+ }
116
+
117
+ function showFieldError(control, slot, kind, state) {
118
+ control.setAttribute('aria-invalid', 'true');
119
+ if (!slot) return;
120
+ if (kind.borrowedHint && !state.hintHelp.has(slot)) state.hintHelp.set(slot, slot.textContent);
121
+ slot.textContent = control.validationMessage;
122
+ if (kind.hasHintClass) slot.classList.add('ui-hint--error');
123
+ link(control, slot);
124
+ }
125
+
126
+ function validateField(control, state) {
127
+ if (!control.willValidate) return true;
128
+ rememberControl(control, state);
129
+ const ok = control.validity.valid;
130
+ const slot = slotFor(control);
131
+ if (slot) rememberSlot(slot, state);
132
+ const kind = slotKind(slot);
133
+ if (ok) clearFieldError(control, slot, kind, state);
134
+ else showFieldError(control, slot, kind, state);
135
+ return ok;
136
+ }
137
+
138
+ function controlsOf(form) {
139
+ return [...form.elements].filter(
140
+ (el) => el.willValidate && el.type !== 'submit' && el.type !== 'button',
141
+ );
142
+ }
143
+
144
+ function summaryItem(control) {
145
+ const id = ensureId(control, 'bronto-field');
146
+ const li = document.createElement('li');
147
+ const a = document.createElement('a');
148
+ a.href = `#${id}`;
149
+ a.textContent = control.validationMessage;
150
+ a.addEventListener('click', (e) => {
151
+ e.preventDefault();
152
+ control.focus();
153
+ });
154
+ li.appendChild(a);
155
+ return li;
156
+ }
157
+
158
+ function refreshSummary(form, invalid, state) {
159
+ const summary = form.querySelector('[data-bronto-error-summary]');
160
+ if (!summary) return;
161
+ rememberSummary(summary, state);
162
+ if (!invalid.length) {
163
+ summary.hidden = true;
164
+ summary.replaceChildren();
165
+ return;
166
+ }
167
+ const title = document.createElement('p');
168
+ title.className = 'ui-error-summary__title';
169
+ title.textContent = 'There is a problem';
170
+ const list = document.createElement('ul');
171
+ list.className = 'ui-error-summary__list';
172
+ list.append(...invalid.map(summaryItem));
173
+ summary.replaceChildren(title, list);
174
+ summary.setAttribute('role', 'alert');
175
+ summary.tabIndex = -1;
176
+ summary.hidden = false;
177
+ }
178
+
179
+ function restoreValidationState(state) {
180
+ for (const [form, noValidate] of state.priorNoValidate) form.noValidate = noValidate;
181
+ for (const [summary, snapshot] of state.summaryState) {
182
+ summary.replaceChildren(...snapshot.children);
183
+ summary.hidden = snapshot.hidden;
184
+ restoreAttrs(summary, snapshot.attrs);
185
+ }
186
+ for (const [slot, snapshot] of state.slotState) {
187
+ slot.textContent = snapshot.text;
188
+ slot.classList.toggle('ui-hint--error', snapshot.hadErrorClass);
189
+ restoreAttrs(slot, snapshot.attrs);
190
+ }
191
+ for (const [control, attrs] of state.controlState) restoreAttrs(control, attrs);
192
+ }
193
+
3
194
  /**
4
195
  * Accessible form validation glue for `<form data-bronto-validate>`.
5
196
  * Progressive enhancement over the native Constraint Validation API —
@@ -29,206 +220,36 @@ export function initFormValidation({ root } = {}) {
29
220
  if (!hasDom()) return noop;
30
221
  const host = resolveHost(root);
31
222
  if (!host) return noop;
32
- let priorNoValidate = new Map();
33
- let controlState = new Map();
34
- let slotState = new Map();
35
- let summaryState = new Map();
36
-
37
- const suppressNativeValidation = (form) => {
38
- if (!priorNoValidate.has(form)) priorNoValidate.set(form, form.noValidate);
39
- form.noValidate = true;
40
- };
41
-
42
- const snapshotAttrs = (el, names) => {
43
- const out = {};
44
- for (const name of names) {
45
- out[name] = {
46
- had: el.hasAttribute(name),
47
- value: el.getAttribute(name),
48
- };
49
- }
50
- return out;
51
- };
52
-
53
- const restoreAttrs = (el, attrs) => {
54
- for (const [name, attr] of Object.entries(attrs)) {
55
- if (attr.had) el.setAttribute(name, attr.value);
56
- else el.removeAttribute(name);
57
- }
58
- };
59
-
60
- const rememberControl = (control) => {
61
- if (!controlState.has(control)) {
62
- controlState.set(control, snapshotAttrs(control, ['aria-invalid', 'aria-describedby', 'id']));
63
- }
64
- };
65
-
66
- const rememberSlot = (slot) => {
67
- if (!slotState.has(slot)) {
68
- slotState.set(slot, {
69
- attrs: snapshotAttrs(slot, ['id']),
70
- text: slot.textContent,
71
- hadErrorClass: slot.classList.contains('ui-hint--error'),
72
- });
73
- }
74
- };
75
-
76
- const rememberSummary = (summary) => {
77
- if (!summaryState.has(summary)) {
78
- summaryState.set(summary, {
79
- attrs: snapshotAttrs(summary, ['role', 'tabindex']),
80
- children: [...summary.childNodes],
81
- hidden: summary.hidden,
82
- });
83
- }
84
- };
85
-
86
- const ensureId = (el, prefix) => {
87
- if (!el.id) el.id = `${prefix}-${nextFieldUid()}`;
88
- return el.id;
89
- };
90
-
91
- // When the field has no dedicated `[data-bronto-error]` node we fall back to
92
- // the shared `.ui-hint` help slot. Snapshot its original help text the first
93
- // time we overwrite it with an error, so the valid branch can RESTORE the help
94
- // rather than blanking it permanently (component-audit C8).
95
- const hintHelp = new WeakMap();
96
-
97
- const slotFor = (control) => {
98
- const field = control.closest('.ui-field');
99
- if (!field) return null;
100
- const dedicated = field.querySelector('[data-bronto-error]');
101
- if (dedicated) return dedicated;
102
- return field.querySelector('.ui-hint');
103
- };
104
-
105
- const link = (control, slot) => {
106
- const slotId = ensureId(slot, 'bronto-err');
107
- const ids = (control.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
108
- if (!ids.includes(slotId)) {
109
- ids.push(slotId);
110
- control.setAttribute('aria-describedby', ids.join(' '));
111
- }
112
- };
113
-
114
- const unlink = (control, slot) => {
115
- if (!slot.id) return;
116
- const ids = (control.getAttribute('aria-describedby') || '')
117
- .split(/\s+/)
118
- .filter((id) => id && id !== slot.id);
119
- if (ids.length) control.setAttribute('aria-describedby', ids.join(' '));
120
- else control.removeAttribute('aria-describedby');
121
- };
122
-
123
- const validateField = (control) => {
124
- if (!control.willValidate) return true;
125
- rememberControl(control);
126
- const ok = control.validity.valid;
127
- const slot = slotFor(control);
128
- if (slot) rememberSlot(slot);
129
- // Decide the slot TYPE by the `[data-bronto-error]` attribute, NOT the
130
- // `.ui-hint` class: the canonical markup is `<p class="ui-hint"
131
- // data-bronto-error>`, which carries BOTH. Keying off `.ui-hint` sent that
132
- // dedicated error node down the help-hint branch, which never unlink()s — so
133
- // the field kept a dangling aria-describedby to an empty error node after it
134
- // was fixed (component-audit C6). Only a *borrowed* plain `.ui-hint` (a help
135
- // slot with no dedicated error node) snapshots/restores its help text and
136
- // stays linked in the valid state.
137
- const dedicated = !!slot?.matches?.('[data-bronto-error]');
138
- const hasHintClass = !!slot?.classList.contains('ui-hint');
139
- const borrowedHint = hasHintClass && !dedicated;
140
- if (ok) {
141
- control.removeAttribute('aria-invalid');
142
- if (slot) {
143
- if (hasHintClass) slot.classList.remove('ui-hint--error');
144
- if (borrowedHint) {
145
- // Restore the snapshotted help text (or clear if there was none); a
146
- // help-bearing hint stays linked via aria-describedby (it describes
147
- // the field in the valid state too).
148
- slot.textContent = hintHelp.get(slot) ?? '';
149
- } else {
150
- // Dedicated error node: clear it and drop the now-stale describedby
151
- // so AT doesn't announce an empty error association.
152
- slot.textContent = '';
153
- unlink(control, slot);
154
- }
155
- }
156
- } else {
157
- control.setAttribute('aria-invalid', 'true');
158
- if (slot) {
159
- if (borrowedHint && !hintHelp.has(slot)) hintHelp.set(slot, slot.textContent);
160
- slot.textContent = control.validationMessage;
161
- if (hasHintClass) slot.classList.add('ui-hint--error');
162
- link(control, slot);
163
- }
164
- }
165
- return ok;
166
- };
167
-
168
- const controlsOf = (form) =>
169
- [...form.elements].filter(
170
- (el) => el.willValidate && el.type !== 'submit' && el.type !== 'button',
171
- );
172
-
173
- const refreshSummary = (form, invalid) => {
174
- const summary = form.querySelector('[data-bronto-error-summary]');
175
- if (!summary) return;
176
- rememberSummary(summary);
177
- if (!invalid.length) {
178
- summary.hidden = true;
179
- summary.replaceChildren();
180
- return;
181
- }
182
- const title = document.createElement('p');
183
- title.className = 'ui-error-summary__title';
184
- title.textContent = 'There is a problem';
185
- const list = document.createElement('ul');
186
- list.className = 'ui-error-summary__list';
187
- for (const c of invalid) {
188
- const id = ensureId(c, 'bronto-field');
189
- const li = document.createElement('li');
190
- const a = document.createElement('a');
191
- a.href = `#${id}`;
192
- a.textContent = c.validationMessage;
193
- a.addEventListener('click', (e) => {
194
- e.preventDefault();
195
- c.focus();
196
- });
197
- li.appendChild(a);
198
- list.appendChild(li);
199
- }
200
- summary.replaceChildren(title, list);
201
- summary.setAttribute('role', 'alert');
202
- summary.tabIndex = -1;
203
- summary.hidden = false;
204
- };
223
+ let state = createValidationState();
205
224
 
206
- const onSubmit = (e) => {
207
- const form = e.target.closest?.('[data-bronto-validate]');
225
+ const onSubmit = (event) => {
226
+ const form = event.target.closest?.('[data-bronto-validate]');
208
227
  if (!form) return;
209
- suppressNativeValidation(form);
210
- const invalid = controlsOf(form).filter((c) => !validateField(c));
211
- refreshSummary(form, invalid);
228
+ suppressNativeValidation(form, state);
229
+ const invalid = controlsOf(form).filter((control) => !validateField(control, state));
230
+ refreshSummary(form, invalid, state);
212
231
  if (invalid.length) {
213
- e.preventDefault();
232
+ event.preventDefault();
214
233
  const summary = form.querySelector('[data-bronto-error-summary]');
215
234
  (summary && !summary.hidden ? summary : invalid[0]).focus();
216
235
  }
217
236
  };
218
237
 
219
- const onBlur = (e) => {
220
- const control = e.target;
238
+ const onBlur = (event) => {
239
+ const control = event.target;
221
240
  if (!control.willValidate) return;
222
241
  const form = control.closest?.('[data-bronto-validate]');
223
242
  if (!form) return;
224
- suppressNativeValidation(form);
225
- validateField(control);
243
+ suppressNativeValidation(form, state);
244
+ validateField(control, state);
226
245
  const summary = form.querySelector('[data-bronto-error-summary]');
227
- if (summary && !summary.hidden)
246
+ if (summary && !summary.hidden) {
228
247
  refreshSummary(
229
248
  form,
230
- controlsOf(form).filter((c) => !c.validity.valid),
249
+ controlsOf(form).filter((candidate) => !candidate.validity.valid),
250
+ state,
231
251
  );
252
+ }
232
253
  };
233
254
 
234
255
  return bindOnce(host, 'formValidation', () => {
@@ -239,34 +260,17 @@ export function initFormValidation({ root } = {}) {
239
260
  // summary — contradicting the documented contract. (Forms added
240
261
  // after init are still covered by the in-handler set.)
241
262
  const forms = collectHosts(host, '[data-bronto-validate]');
242
- priorNoValidate = new Map();
243
- controlState = new Map();
244
- slotState = new Map();
245
- summaryState = new Map();
246
- for (const f of forms) {
247
- suppressNativeValidation(f);
263
+ state = createValidationState();
264
+ for (const form of forms) {
265
+ suppressNativeValidation(form, state);
248
266
  }
249
267
  host.addEventListener('submit', onSubmit, true);
250
268
  host.addEventListener('focusout', onBlur);
251
269
  return () => {
252
270
  host.removeEventListener('submit', onSubmit, true);
253
271
  host.removeEventListener('focusout', onBlur);
254
- for (const [f, v] of priorNoValidate) f.noValidate = v;
255
- priorNoValidate.clear();
256
- for (const [summary, state] of summaryState) {
257
- summary.replaceChildren(...state.children);
258
- summary.hidden = state.hidden;
259
- restoreAttrs(summary, state.attrs);
260
- }
261
- summaryState.clear();
262
- for (const [slot, state] of slotState) {
263
- slot.textContent = state.text;
264
- slot.classList.toggle('ui-hint--error', state.hadErrorClass);
265
- restoreAttrs(slot, state.attrs);
266
- }
267
- slotState.clear();
268
- for (const [control, attrs] of controlState) restoreAttrs(control, attrs);
269
- controlState.clear();
272
+ restoreValidationState(state);
273
+ state = createValidationState();
270
274
  };
271
275
  });
272
276
  }
@@ -1 +1 @@
1
- {"version":3,"file":"glyph.d.ts","sourceRoot":"","sources":["glyph.js"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0J3C"}
1
+ {"version":3,"file":"glyph.d.ts","sourceRoot":"","sources":["glyph.js"],"names":[],"mappings":"AAwLA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwB3C"}