@adia-ai/web-components 0.7.1 → 0.7.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.7.2] — 2026-06-01
4
+
5
+ ### Fixed
6
+
7
+ - **template engine — ARIA-transparent `display:contents` wrapper spans (FB-94)** — the interpolation wrapper span the engine emits for `${…}` parts (`.map()` / conditional / `repeat()`) now carries `role="presentation"`. `display:contents` removes the span's box from layout but it still generated a generic accessibility node, which broke `aria-required-children` / `aria-required-parent` when the span sat between a role-bearing parent and role-bearing children rendered via interpolation — e.g. a `.map()`-rendered `<tree-ui>` (the mainstream consumer case) tripped axe `aria-required-children` even after the FB-91 static-markup fix landed. `role="presentation"` promotes the children to the real parent in the a11y tree. Fixed at all three wrapper sites (`wrap()` + array branch + keyed `repeat()`); +3 regression tests assert the wrappers carry `role="presentation"`. This is the consumer-preferred + peer-recommended central fix and is structurally sound (the spurious generic node between `tree`→`treeitem` is removed). **Honest verification note:** `aria-required-children` could not be made to fire on any in-repo tree (shallow *or* deeply-nested, axe-core, `:5300`) — it triggers only on color-app's exact published-package `dts-token-tree` — so local axe reads 0 both before and after; the firing→cleared transition is **pending the consumer's confirmation** on their next `@adia-ai/web-components` upgrade. (Tokens Studio / color-app FB-94)
8
+ - **`<menu-item-ui>` no longer an orphan `menuitem` when the menu is closed (FB-95)** — `UIMenuItem.connected()` set `role="menuitem"` unconditionally, but a closed `<menu-ui>` moves its items back into its own light DOM (no `role="menu"` ancestor), so every closed declarative menu tripped axe `aria-required-parent`. The role (+ `tabindex="-1"`) is now applied only when the item has a `role="menu"`/`menubar` ancestor; the open popover adopts items via `appendChild` (which re-fires `connected()`), so the role re-applies on open and drops on close. **Verified with axe-core**: closed menu → **0** `aria-required-parent` violations (was 3); open menu → items are `role="menuitem"`, 0 violations. (Tokens Studio / color-app FB-95)
9
+
10
+ ### Changed — styles + build
11
+
12
+ - **Tokens cascade-layer completed (`styles/`)** — `typography.css` (`type/scale.css`) + `roles.css` `:root` role-token blocks moved into `@layer(tokens)`, finishing the `@layer` migration's tokens cut (the type-scale + role tokens had stayed unlayered). Behavior-neutral — the `[variant]`/state rules keep their existing precedence; only the base `:root` token declarations join the layer.
13
+ - **`dist/` CDN bundles regenerated** (`web-components.min.css` + `web-components.min.js`) for the template-engine + tokens-layer changes.
14
+
3
15
  ## [0.7.1] — 2026-05-31
4
16
 
5
17
  ### Fixed
@@ -266,8 +266,20 @@ export class UIMenuItem extends UIElement {
266
266
  static template = () => null;
267
267
 
268
268
  connected() {
269
- this.setAttribute('role', 'menuitem');
270
- if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '-1');
269
+ // role="menuitem" ONLY when actually inside a menu container. A menu-ui
270
+ // closes by moving its items back into its own light DOM (see #hide()),
271
+ // where there is no role="menu"/menubar ancestor — a bare role="menuitem"
272
+ // there trips axe `aria-required-parent` on every closed menu (FB-95). The
273
+ // open popover (role="menu") adopts items via appendChild, which re-fires
274
+ // connected(), so the role re-applies when the menu opens and is dropped
275
+ // again on close.
276
+ if (this.closest('[role="menu"], [role="menubar"]')) {
277
+ this.setAttribute('role', 'menuitem');
278
+ if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '-1');
279
+ } else {
280
+ this.removeAttribute('role');
281
+ this.removeAttribute('tabindex');
282
+ }
271
283
  this.#stamp();
272
284
  this.#syncAria();
273
285
  }
package/core/template.js CHANGED
@@ -430,6 +430,13 @@ function wrap(part) {
430
430
  if (part.n.nodeType === 1 && part.n[PART]) return part.n;
431
431
  const c = document.createElement('span');
432
432
  c.style.display = 'contents';
433
+ // ARIA-transparent: display:contents removes the box from layout, but the
434
+ // span still generates a generic accessibility node — which breaks
435
+ // required-owned / required-parent relationships when it sits between a
436
+ // role-bearing parent and role-bearing children rendered via interpolation
437
+ // (tree→treeitem, menu→menuitem via `.map()`/`repeat()`). role="presentation"
438
+ // promotes the children to the real parent in the a11y tree. (FB-94 / FB-95)
439
+ c.setAttribute('role', 'presentation');
433
440
  c[PART] = true;
434
441
  part.n.replaceWith(c);
435
442
  part.n = c;
@@ -453,6 +460,7 @@ function applyValue(p, v) {
453
460
  if (isResult(item)) {
454
461
  const el = document.createElement('span');
455
462
  el.style.display = 'contents';
463
+ el.setAttribute('role', 'presentation'); // ARIA-transparent — see wrap()
456
464
  c.appendChild(el);
457
465
  stamp(item, el);
458
466
  } else {
@@ -526,6 +534,7 @@ export function repeat(items, keyFn, tplFn) {
526
534
  } else {
527
535
  const el = document.createElement('span');
528
536
  el.style.display = 'contents';
537
+ el.setAttribute('role', 'presentation'); // ARIA-transparent — see wrap()
529
538
  stamp(result, el);
530
539
  newMap.set(key, el);
531
540
  }
@@ -573,3 +573,39 @@ describe('html template — FB-57 (v0.6.8) nested SVG/MathML template namespace
573
573
  expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
574
574
  });
575
575
  });
576
+
577
+ describe('html template — FB-94/FB-95 (v0.7.x) ARIA-transparent wrapper spans', () => {
578
+ let container;
579
+ beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); });
580
+ afterEach(() => container.remove());
581
+
582
+ // The display:contents wrapper span the engine emits for interpolated parts
583
+ // must carry role="presentation" — display:contents removes the box from
584
+ // layout but the span still generates a generic a11y node, which breaks
585
+ // required-owned / required-parent relationships when it sits between a
586
+ // role-bearing parent and role-bearing children rendered via .map()/repeat()
587
+ // (tree→treeitem = FB-94, menu→menuitem = FB-95). Verified end-to-end with
588
+ // axe-core; these lock the substrate behaviour against regression.
589
+ const isContentsSpan = (s) => /display:\s*contents/.test(s.getAttribute('style') || '');
590
+
591
+ it('a `.map()` of templates wraps each item in a role="presentation" display:contents span', () => {
592
+ stamp(html`<ul>${['a', 'b', 'c'].map((x) => html`<li>${x}</li>`)}</ul>`, container);
593
+ const spans = [...container.querySelectorAll('span')].filter(isContentsSpan);
594
+ expect(spans.length).toBeGreaterThan(0);
595
+ spans.forEach((s) => expect(s.getAttribute('role')).toBe('presentation'));
596
+ });
597
+
598
+ it('a conditional `${cond ? html`` : null}` branch wraps in role="presentation"', () => {
599
+ stamp(html`<div>${true ? html`<span data-x>hi</span>` : null}</div>`, container);
600
+ const wrap = [...container.querySelectorAll('div > span')].find(isContentsSpan);
601
+ expect(wrap).toBeTruthy();
602
+ expect(wrap.getAttribute('role')).toBe('presentation');
603
+ });
604
+
605
+ it('`repeat()` keyed items wrap in role="presentation"', () => {
606
+ stamp(html`<ul>${repeat(['a', 'b'], (x) => x, (x) => html`<li>${x}</li>`)}</ul>`, container);
607
+ const spans = [...container.querySelectorAll('span')].filter(isContentsSpan);
608
+ expect(spans.length).toBeGreaterThan(0);
609
+ spans.forEach((s) => expect(s.getAttribute('role')).toBe('presentation'));
610
+ });
611
+ });