@fusion.dev/helix-core 0.109.2 → 0.110.0
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/custom-elements.json +56 -2
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +2 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +2 -1
- package/dist/mixins/mixin-field/mixin-field.d.ts +5 -0
- package/dist/mixins/mixin-field/mixin-field.js +3 -0
- package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +2 -0
- package/dist/utils/tell-disclosure/tell-disclosure.d.ts +3 -2
- package/dist/utils/tell-disclosure/tell-disclosure.js +5 -1
- package/package.json +4 -4
package/custom-elements.json
CHANGED
|
@@ -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": [
|
|
@@ -9640,6 +9659,15 @@
|
|
|
9640
9659
|
"readonly": true,
|
|
9641
9660
|
"description": "Id of the description node, for `aria-describedby` on the control.\n\n`undefined` when there is nothing to point at, so the attribute is absent\nrather than pointing at an id that renders nothing."
|
|
9642
9661
|
},
|
|
9662
|
+
{
|
|
9663
|
+
"kind": "field",
|
|
9664
|
+
"name": "invalidity",
|
|
9665
|
+
"type": {
|
|
9666
|
+
"text": "'true' | undefined"
|
|
9667
|
+
},
|
|
9668
|
+
"readonly": true,
|
|
9669
|
+
"description": "`'true'` for `aria-invalid` on the control while a message is shown, and\n`undefined` otherwise, so the attribute is absent."
|
|
9670
|
+
},
|
|
9643
9671
|
{
|
|
9644
9672
|
"kind": "field",
|
|
9645
9673
|
"name": "feedback",
|
|
@@ -11566,6 +11594,25 @@
|
|
|
11566
11594
|
],
|
|
11567
11595
|
"description": "What one pan control is called, for a family that draws its own."
|
|
11568
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
|
+
},
|
|
11569
11616
|
{
|
|
11570
11617
|
"kind": "method",
|
|
11571
11618
|
"name": "panGlyph",
|
|
@@ -11902,7 +11949,7 @@
|
|
|
11902
11949
|
{
|
|
11903
11950
|
"kind": "method",
|
|
11904
11951
|
"name": "sideOf",
|
|
11905
|
-
"privacy": "
|
|
11952
|
+
"privacy": "protected",
|
|
11906
11953
|
"return": {
|
|
11907
11954
|
"type": {
|
|
11908
11955
|
"text": "string"
|
|
@@ -13830,9 +13877,16 @@
|
|
|
13830
13877
|
"type": {
|
|
13831
13878
|
"text": "boolean"
|
|
13832
13879
|
}
|
|
13880
|
+
},
|
|
13881
|
+
{
|
|
13882
|
+
"name": "popup",
|
|
13883
|
+
"optional": true,
|
|
13884
|
+
"type": {
|
|
13885
|
+
"text": "'dialog' | 'grid' | 'listbox' | 'menu' | 'tree'"
|
|
13886
|
+
}
|
|
13833
13887
|
}
|
|
13834
13888
|
],
|
|
13835
|
-
"description": "Tells a slotted trigger that it opens something,
|
|
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."
|
|
13836
13890
|
}
|
|
13837
13891
|
],
|
|
13838
13892
|
"exports": [
|
|
@@ -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 ?
|
|
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
|
}
|
|
@@ -33,6 +33,11 @@ export declare class MixinFieldInterface {
|
|
|
33
33
|
* rather than pointing at an id that renders nothing.
|
|
34
34
|
*/
|
|
35
35
|
readonly describedBy: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* `'true'` for `aria-invalid` on the control while a message is shown, and
|
|
38
|
+
* `undefined` otherwise, so the attribute is absent.
|
|
39
|
+
*/
|
|
40
|
+
readonly invalidity: 'true' | undefined;
|
|
36
41
|
/**
|
|
37
42
|
* The validation message shown under the control. It replaces the browser's
|
|
38
43
|
* message.
|
|
@@ -44,6 +44,9 @@ export const MixinField = (Base) => {
|
|
|
44
44
|
}
|
|
45
45
|
return this.hint === '' ? undefined : HINT_ID;
|
|
46
46
|
}
|
|
47
|
+
get invalidity() {
|
|
48
|
+
return this.resolvedFeedback === '' ? undefined : 'true';
|
|
49
|
+
}
|
|
47
50
|
renderGroupLabel() {
|
|
48
51
|
const hidden = this.label === '' ? this.getAttribute('aria-label') : null;
|
|
49
52
|
if (this.label === '' && !hidden) {
|
|
@@ -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,
|
|
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.
|
|
4
|
+
"version": "0.110.0",
|
|
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.
|
|
37
|
+
"@fusion.dev/helix-icons": "0.110.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@fusion.dev/helix-cem": "0.
|
|
41
|
-
"@fusion.dev/helix-testing": "0.
|
|
40
|
+
"@fusion.dev/helix-cem": "0.110.0",
|
|
41
|
+
"@fusion.dev/helix-testing": "0.110.0",
|
|
42
42
|
"@lit-labs/ssr": "^4.1.0",
|
|
43
43
|
"lit": "^3.2.1"
|
|
44
44
|
},
|