@fusion.dev/helix-core 0.109.3 → 0.110.1

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.
@@ -8362,6 +8362,15 @@
8362
8362
  ],
8363
8363
  "description": "The same words for a screen reader, in a region that is always there.\n\nRendered by the component outside its rows, where a status is allowed."
8364
8364
  },
8365
+ {
8366
+ "kind": "field",
8367
+ "name": "announced",
8368
+ "type": {
8369
+ "text": "string"
8370
+ },
8371
+ "privacy": "protected",
8372
+ "description": "News the component has for the live region while there are rows and nothing is loading."
8373
+ },
8365
8374
  {
8366
8375
  "kind": "method",
8367
8376
  "name": "countRows",
@@ -8535,6 +8544,16 @@
8535
8544
  }
8536
8545
  ],
8537
8546
  "description": "The same words for a screen reader, in a region that is always there.\n\n**A region is heard when what it holds changes, not when it arrives.**\nThe loading line was a `role=\"status\"` inserted with its words already\nin it, which is read inconsistently at best, and the empty line was no\nregion at all, so a filter that emptied a list said nothing. This one is\nrendered on every pass and only its words move: \"Loading\", then\n\"No matches\", then nothing once there are rows.\n\n**Outside the rows, which is the component's to place.** A status is not\na child a `listbox`, a `table` or a `tree` may hold, so the component\nrenders this beside its container rather than inside it. The styled\ntier clips it; the visible lines are `renderStatus`."
8547
+ },
8548
+ {
8549
+ "kind": "field",
8550
+ "name": "announced",
8551
+ "type": {
8552
+ "text": "string"
8553
+ },
8554
+ "privacy": "protected",
8555
+ "default": "''",
8556
+ "description": "What the region says while there are rows and nothing is loading, which\nis nothing unless the component has news of its own.\n\n**One region, so two messages never talk over each other.** Data Table\nsays a sort and a selection here; a second `role=\"status\"` beside this\none would be heard in whichever order the reader happens to queue them.\nLoading and empty win, being the larger news. Set before the render that\nshows it, in `willUpdate`, and not reactive: it changes only when\nsomething else already asked for a render."
8538
8557
  }
8539
8558
  ],
8540
8559
  "attributes": [
@@ -11575,6 +11594,25 @@
11575
11594
  ],
11576
11595
  "description": "What one pan control is called, for a family that draws its own."
11577
11596
  },
11597
+ {
11598
+ "kind": "method",
11599
+ "name": "sideOf",
11600
+ "privacy": "protected",
11601
+ "return": {
11602
+ "type": {
11603
+ "text": "string"
11604
+ }
11605
+ },
11606
+ "parameters": [
11607
+ {
11608
+ "name": "way",
11609
+ "type": {
11610
+ "text": "ViewportWay"
11611
+ }
11612
+ }
11613
+ ],
11614
+ "description": "The side of the screen a reading-order way is on: `up`, `down`, `left` or `right`."
11615
+ },
11578
11616
  {
11579
11617
  "kind": "method",
11580
11618
  "name": "panGlyph",
@@ -11911,7 +11949,7 @@
11911
11949
  {
11912
11950
  "kind": "method",
11913
11951
  "name": "sideOf",
11914
- "privacy": "private",
11952
+ "privacy": "protected",
11915
11953
  "return": {
11916
11954
  "type": {
11917
11955
  "text": "string"
@@ -13839,9 +13877,16 @@
13839
13877
  "type": {
13840
13878
  "text": "boolean"
13841
13879
  }
13880
+ },
13881
+ {
13882
+ "name": "popup",
13883
+ "optional": true,
13884
+ "type": {
13885
+ "text": "'dialog' | 'grid' | 'listbox' | 'menu' | 'tree'"
13886
+ }
13842
13887
  }
13843
13888
  ],
13844
- "description": "Tells a slotted trigger that it opens something, and whether it is open.\n\n**Writing `aria-expanded` onto the consumer's element is a hydration\nmismatch, so it is not written.** The gap that answer was reaching for is\nreal: a consumer who has to mirror `open` back onto their own trigger is a\nconsumer who will forget, and the failure is silent to everyone who can see\nthe screen. It is still the wrong answer, because a component decorating a\nconsumer's markup after upgrading is one whose output a React consumer's\nhydration disagrees with.\n\n**Measured 28 Aug 2026 against React 19.2.8.** Four\nfamilies were server-rendered as React emits them and hydrated with the\nelement definitions loaded either side of `hydrateRoot`:\n\n```\ncase writes to trigger hydration mismatch\nmegamenu aria-expanded yes, both load orders\nspeed dial aria-expanded yes, both load orders\npopover nothing no\nmenu nothing no\n```\n\nReact reports \"some attributes of the server rendered HTML didn't match the\nclient properties. This won't be patched up\", and the cost is not the console\nline: it answers a mismatch by discarding the server's HTML for that subtree\nand rebuilding it, which is what the carousel's own note records blinding an\n`<helix-aura>` two sections away.\n\n**A property crosses the gap that an attribute cannot.** React diffs the\nattributes it rendered and never looks at a property, so the state reaches\nthe trigger, lands as `aria-expanded` inside *that element's* shadow root,\nand the consumer's markup is exactly what they served. Both positions get\nwhat they were protecting: nothing is written onto the consumer's element,\nand the reader is still told what the control does.\n\n**Set before upgrade is safe**, which is what lets this run without waiting\nfor a definition: `ReactiveElement` captures an instance property assigned\nbefore it upgrades and re-applies it, so a trigger whose module has not\nloaded yet still ends up with the value.\n\n**A plain element is warned about rather than written to.** A\n`<button slot=\"trigger\">` has no shadow root to keep the state out of the\nmarkup and no property to take it, and a consumer cannot mirror it themselves\nbecause the open state belongs to the component. Saying so once is the same\nanswer `Tooltip` gives about the link it will not write for them, and not at\nall to a trigger that already carries `aria-expanded`, which is a consumer\nmirroring the state from the component's `toggle` event."
13889
+ "description": "Tells a slotted trigger that it opens something, whether it is open, and,\nwhere `popup` names it, what kind of thing it opens.\n\n**Writing `aria-expanded` onto the consumer's element is a hydration\nmismatch, so it is not written.** The gap that answer was reaching for is\nreal: a consumer who has to mirror `open` back onto their own trigger is a\nconsumer who will forget, and the failure is silent to everyone who can see\nthe screen. It is still the wrong answer, because a component decorating a\nconsumer's markup after upgrading is one whose output a React consumer's\nhydration disagrees with.\n\n**Measured 28 Aug 2026 against React 19.2.8.** Four\nfamilies were server-rendered as React emits them and hydrated with the\nelement definitions loaded either side of `hydrateRoot`:\n\n```\ncase writes to trigger hydration mismatch\nmegamenu aria-expanded yes, both load orders\nspeed dial aria-expanded yes, both load orders\npopover nothing no\nmenu nothing no\n```\n\nReact reports \"some attributes of the server rendered HTML didn't match the\nclient properties. This won't be patched up\", and the cost is not the console\nline: it answers a mismatch by discarding the server's HTML for that subtree\nand rebuilding it, which is what the carousel's own note records blinding an\n`<helix-aura>` two sections away.\n\n**A property crosses the gap that an attribute cannot.** React diffs the\nattributes it rendered and never looks at a property, so the state reaches\nthe trigger, lands as `aria-expanded` inside *that element's* shadow root,\nand the consumer's markup is exactly what they served. Both positions get\nwhat they were protecting: nothing is written onto the consumer's element,\nand the reader is still told what the control does.\n\n**Set before upgrade is safe**, which is what lets this run without waiting\nfor a definition: `ReactiveElement` captures an instance property assigned\nbefore it upgrades and re-applies it, so a trigger whose module has not\nloaded yet still ends up with the value.\n\n**A plain element is warned about rather than written to.** A\n`<button slot=\"trigger\">` has no shadow root to keep the state out of the\nmarkup and no property to take it, and a consumer cannot mirror it themselves\nbecause the open state belongs to the component. Saying so once is the same\nanswer `Tooltip` gives about the link it will not write for them, and not at\nall to a trigger that already carries `aria-expanded`, which is a consumer\nmirroring the state from the component's `toggle` event."
13845
13890
  }
13846
13891
  ],
13847
13892
  "exports": [
@@ -3,7 +3,7 @@ const RELATIONSHIPS = [
3
3
  ['aria-describedby', 'ariaDescribedByElements'],
4
4
  ['aria-labelledby', 'ariaLabelledByElements'],
5
5
  ];
6
- const STATES = ['aria-expanded', 'aria-haspopup', 'aria-controls'];
6
+ const STATES = ['aria-expanded', 'aria-haspopup', 'aria-controls', 'aria-invalid'];
7
7
  export class AriaForwardController {
8
8
  constructor(host, options) {
9
9
  this.forwarded = new Set();
@@ -25,6 +25,8 @@ export declare class CollectionElement {
25
25
  * Rendered by the component outside its rows, where a status is allowed.
26
26
  */
27
27
  protected renderLive(hasContent: boolean): TemplateResult;
28
+ /** News the component has for the live region while there are rows and nothing is loading. */
29
+ protected announced: string;
28
30
  /** Binds to a rows slot's `slotchange`, and answers `hasRows` from it. */
29
31
  protected countRows(event: Event): void;
30
32
  /** What the rows slot is given, read from the host so a first render has it. */
@@ -15,6 +15,7 @@ export const MixinCollection = (Base) => {
15
15
  this.loadingText = 'Loading';
16
16
  this.slotCounted = false;
17
17
  this.slotRows = 0;
18
+ this.announced = '';
18
19
  }
19
20
  countRows(event) {
20
21
  this.slotCounted = true;
@@ -51,7 +52,7 @@ export const MixinCollection = (Base) => {
51
52
  return html `<p part="empty">${this.emptyText}</p>`;
52
53
  }
53
54
  renderLive(hasContent) {
54
- const said = this.loading ? this.loadingText : hasContent ? '' : this.emptyText;
55
+ const said = this.loading ? this.loadingText : hasContent ? this.announced : this.emptyText;
55
56
  return html `<span part="live" role="status">${said}</span>`;
56
57
  }
57
58
  }
@@ -71,6 +71,8 @@ export declare class ViewportElement {
71
71
  protected handleMoreKeys(event: KeyboardEvent): boolean;
72
72
  /** What one pan control is called, for a family that draws its own. */
73
73
  protected panLabel(way: ViewportWay): string;
74
+ /** The side of the screen a reading-order way is on: `up`, `down`, `left` or `right`. */
75
+ protected sideOf(way: ViewportWay): string;
74
76
  /** The arrow drawn on one pan control, for a family that draws its own. */
75
77
  protected panGlyph(way: ViewportWay): string;
76
78
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Tells a slotted trigger that it opens something, and whether it is open.
2
+ * Tells a slotted trigger that it opens something, whether it is open, and,
3
+ * where `popup` names it, what kind of thing it opens.
3
4
  *
4
5
  * **Writing `aria-expanded` onto the consumer's element is a hydration
5
6
  * mismatch, so it is not written.** The gap that answer was reaching for is
@@ -47,4 +48,4 @@
47
48
  * all to a trigger that already carries `aria-expanded`, which is a consumer
48
49
  * mirroring the state from the component's `toggle` event.
49
50
  */
50
- export declare const tellDisclosure: (trigger: Element | null | undefined, open: boolean) => void;
51
+ export declare const tellDisclosure: (trigger: Element | null | undefined, open: boolean, popup?: 'dialog' | 'grid' | 'listbox' | 'menu' | 'tree') => void;
@@ -1,11 +1,15 @@
1
1
  const warned = new WeakSet();
2
- export const tellDisclosure = (trigger, open) => {
2
+ export const tellDisclosure = (trigger, open, popup) => {
3
3
  if (!trigger) {
4
4
  return;
5
5
  }
6
6
  if (trigger.localName.includes('-')) {
7
7
  ;
8
8
  trigger.expanded = open;
9
+ if (popup) {
10
+ ;
11
+ trigger.popup = popup;
12
+ }
9
13
  return;
10
14
  }
11
15
  if (warned.has(trigger) || trigger.hasAttribute('aria-expanded')) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.109.3",
4
+ "version": "0.110.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -34,11 +34,11 @@
34
34
  "bugs": "https://github.com/project-fusion/fusion/issues",
35
35
  "dependencies": {
36
36
  "@floating-ui/dom": "^1.8.0",
37
- "@fusion.dev/helix-icons": "0.109.3"
37
+ "@fusion.dev/helix-icons": "0.110.1"
38
38
  },
39
39
  "devDependencies": {
40
- "@fusion.dev/helix-cem": "0.109.3",
41
- "@fusion.dev/helix-testing": "0.109.3",
40
+ "@fusion.dev/helix-cem": "0.110.1",
41
+ "@fusion.dev/helix-testing": "0.110.1",
42
42
  "@lit-labs/ssr": "^4.1.0",
43
43
  "lit": "^3.2.1"
44
44
  },