@fusion.dev/helix-core 0.0.0-dev

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/custom-elements.json +8005 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
  4. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
  7. package/dist/controllers/command-controller/command-controller.d.ts +83 -0
  8. package/dist/controllers/command-controller/command-controller.js +93 -0
  9. package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
  10. package/dist/controllers/copy-controller/copy-controller.js +128 -0
  11. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
  12. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
  13. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
  14. package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
  15. package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
  16. package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
  17. package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
  18. package/dist/controllers/floating-controller/floating-controller.js +227 -0
  19. package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
  20. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
  21. package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
  22. package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
  23. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
  24. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
  25. package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
  26. package/dist/controllers/popover-controller/popover-controller.js +150 -0
  27. package/dist/controllers/source-controller/source-controller.d.ts +55 -0
  28. package/dist/controllers/source-controller/source-controller.js +94 -0
  29. package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
  30. package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
  31. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
  32. package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
  33. package/dist/events/element-events.d.ts +4 -0
  34. package/dist/events/element-events.js +34 -0
  35. package/dist/events/events.d.ts +743 -0
  36. package/dist/events/events.js +799 -0
  37. package/dist/helix-element/helix-element.d.ts +201 -0
  38. package/dist/helix-element/helix-element.js +695 -0
  39. package/dist/helix-form-element/helix-form-element.d.ts +152 -0
  40. package/dist/helix-form-element/helix-form-element.js +194 -0
  41. package/dist/index.d.ts +54 -0
  42. package/dist/index.js +54 -0
  43. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
  44. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
  45. package/dist/mixins/mixin-collection/index.d.ts +1 -0
  46. package/dist/mixins/mixin-collection/index.js +1 -0
  47. package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
  48. package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
  49. package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
  50. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
  51. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
  52. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
  53. package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
  54. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
  55. package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
  56. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
  57. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
  58. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
  59. package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
  60. package/dist/mixins/mixin-field/mixin-field.js +423 -0
  61. package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
  62. package/dist/utils/anchor-element/anchor-element.js +39 -0
  63. package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
  64. package/dist/utils/capture-pointer/capture-pointer.js +30 -0
  65. package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
  66. package/dist/utils/closest-instance/closest-instance.js +34 -0
  67. package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
  68. package/dist/utils/create-sheet/create-sheet.js +16 -0
  69. package/dist/utils/custom-element/custom-element.d.ts +26 -0
  70. package/dist/utils/custom-element/custom-element.js +114 -0
  71. package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
  72. package/dist/utils/draw-icon/draw-icon.js +39 -0
  73. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
  74. package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
  75. package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
  76. package/dist/utils/elapsed-time/elapsed-time.js +117 -0
  77. package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
  78. package/dist/utils/focusable-elements/focusable-elements.js +88 -0
  79. package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
  80. package/dist/utils/format-numbers/format-numbers.js +29 -0
  81. package/dist/utils/host-aria/host-aria.d.ts +38 -0
  82. package/dist/utils/host-aria/host-aria.js +41 -0
  83. package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
  84. package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
  85. package/dist/utils/line-path/line-path.d.ts +23 -0
  86. package/dist/utils/line-path/line-path.js +107 -0
  87. package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
  88. package/dist/utils/on-unless-false/on-unless-false.js +32 -0
  89. package/dist/utils/read-series/read-series.d.ts +78 -0
  90. package/dist/utils/read-series/read-series.js +187 -0
  91. package/dist/utils/relative-time/relative-time.d.ts +24 -0
  92. package/dist/utils/relative-time/relative-time.js +84 -0
  93. package/dist/utils/request-change/request-change.d.ts +48 -0
  94. package/dist/utils/request-change/request-change.js +7 -0
  95. package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
  96. package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
  97. package/dist/utils/restore-value/restore-value.d.ts +32 -0
  98. package/dist/utils/restore-value/restore-value.js +84 -0
  99. package/dist/utils/scale/scale.d.ts +23 -0
  100. package/dist/utils/scale/scale.js +32 -0
  101. package/dist/utils/scale/ticks.d.ts +27 -0
  102. package/dist/utils/scale/ticks.js +52 -0
  103. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
  104. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
  105. package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
  106. package/dist/utils/slot-filled/slot-filled.js +26 -0
  107. package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
  108. package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
  109. package/dist/utils/ticker/ticker.d.ts +8 -0
  110. package/dist/utils/ticker/ticker.js +55 -0
  111. package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
  112. package/dist/utils/tone-marks/tone-marks.js +36 -0
  113. package/dist/utils/value-as/value-as.d.ts +64 -0
  114. package/dist/utils/value-as/value-as.js +105 -0
  115. package/package.json +58 -0
@@ -0,0 +1,79 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { property } from 'lit/decorators.js';
8
+ import { HelixDismissEvent } from '../../events/events.js';
9
+ /**
10
+ * Grafts `dismissible`, its label, and the event that says so.
11
+ *
12
+ * Three surfaces had written this: `Announcement`, `Tag` and `Toast.Item`, each
13
+ * with the same two properties, the same event, and the same expression for the
14
+ * label. They had not diverged, which is the argument for extracting rather than
15
+ * against it, because the fourth would have.
16
+ *
17
+ * **The label reads the default slot, not the whole element**, and that is a bug
18
+ * fix rather than a move. Every copy said `this.textContent`, which is every
19
+ * light-DOM descendant including the ones assigned elsewhere. On `Announcement`
20
+ * and `Tag` that is the same thing, because they have one slot. On `Toast.Item`
21
+ * it swept in the actions: a message reading "Could not reach the server" with a
22
+ * `Retry` button announced itself as "Could not reach the server Retry", and the
23
+ * consumer removing it from their list got that string to match on.
24
+ *
25
+ * **`dismissEvent()` is separate from `dismiss()` for the one that needs it.**
26
+ * `Announcement` and `Tag` dispatch and are done: they own no state, so a
27
+ * refusal is the whole of the consumer's answer. `Toast.Item` holds an `open`
28
+ * it may or may not own, so its event has to go through the controlled-change
29
+ * path with an `apply` beside it. Giving it the event rather than the method
30
+ * lets it keep the contract and skip the default.
31
+ */
32
+ export const MixinDismissible = (Base) => {
33
+ class Dismissible extends Base {
34
+ constructor() {
35
+ super(...arguments);
36
+ /**
37
+ * Whether it offers a control to take it away.
38
+ */
39
+ this.dismissible = false;
40
+ /**
41
+ * Accessible name for that control.
42
+ *
43
+ * `attribute` is spelled out because Lit lowercases a property name to
44
+ * derive one rather than dash-casing it, so this would otherwise be
45
+ * reachable only as `dismisslabel`, which nobody would guess and no linter
46
+ * would catch.
47
+ */
48
+ this.dismissLabel = 'Dismiss';
49
+ }
50
+ /**
51
+ * The event, built and not dispatched.
52
+ *
53
+ * Reading text is safe here: nothing calls this on a server, because the
54
+ * only things that do are a press and a timer.
55
+ */
56
+ dismissEvent() {
57
+ /* Default slot only. A node assigned to a named slot belongs to another
58
+ region and is not what this surface is called. */
59
+ const own = [...this.childNodes]
60
+ .filter((node) => !(node instanceof Element) || !node.slot)
61
+ .map((node) => node.textContent ?? '')
62
+ .join('');
63
+ return new HelixDismissEvent({ label: own.trim() });
64
+ }
65
+ /** Announces that it should go, and reports whether the request stood. */
66
+ dismiss() {
67
+ const event = this.dismissEvent();
68
+ this.dispatchEvent(event);
69
+ return !event.defaultPrevented;
70
+ }
71
+ }
72
+ __decorate([
73
+ property({ reflect: true, type: Boolean })
74
+ ], Dismissible.prototype, "dismissible", void 0);
75
+ __decorate([
76
+ property({ attribute: 'dismiss-label' })
77
+ ], Dismissible.prototype, "dismissLabel", void 0);
78
+ return Dismissible;
79
+ };
@@ -0,0 +1,145 @@
1
+ import { nothing, type TemplateResult } from 'lit';
2
+ import type { HelixElement } from '../../helix-element/helix-element.js';
3
+ type Constructor<T> = new (...args: any[]) => T;
4
+ /**
5
+ * The public surface `MixinField` adds.
6
+ *
7
+ * Declared separately because TypeScript cannot emit declarations for an
8
+ * anonymous class that has private members (TS4094). Casting the mixin's return
9
+ * to this interface keeps the internals private and the build working.
10
+ */
11
+ export declare class MixinFieldInterface {
12
+ /**
13
+ * Help under the control, shown while there is nothing wrong.
14
+ *
15
+ * Fallback content for the `hint` slot, so anything richer replaces it.
16
+ */
17
+ hint: string;
18
+ /**
19
+ * Ids of the description nodes, for `aria-describedby` on the control.
20
+ *
21
+ * `undefined` when there is nothing to point at, so the attribute is absent
22
+ * rather than pointing at an id that renders nothing.
23
+ */
24
+ readonly describedBy: string | undefined;
25
+ /**
26
+ * What is wrong, in the reader's words rather than the browser's.
27
+ *
28
+ * Overrides the platform's message. Left empty, the platform's own is shown
29
+ * once the reader has had a go at the field, never before. Either way it
30
+ * takes the hint's place rather than appearing under it.
31
+ */
32
+ feedback: string;
33
+ /**
34
+ * The accessible name, and the fallback for the `label` slot.
35
+ *
36
+ * Content rather than configuration, so it is the slot's fallback and never a
37
+ * second thing rendered beside it.
38
+ */
39
+ label: string;
40
+ /**
41
+ * Points the host at its own label and description nodes, for a group.
42
+ *
43
+ * Client only, and an enhancement rather than the whole story: a group's name
44
+ * is also set as a plain string so the server can serve one, and this
45
+ * overrides it where a consumer slotted something richer than a string.
46
+ */
47
+ protected linkHost(): void;
48
+ /** Help under the control, unless something has gone wrong. */
49
+ protected renderHint(): TemplateResult | typeof nothing;
50
+ /** The validation message, in the hint's place. */
51
+ protected renderFeedback(): TemplateResult | typeof nothing;
52
+ /** A label over a set, for a group, which has nothing to point `for` at. */
53
+ protected renderGroupLabel(): TemplateResult | typeof nothing;
54
+ /** A label above the control, pointing at it with a real `for`. */
55
+ protected renderLabel(): TemplateResult | typeof nothing;
56
+ /** Whether the form refuses to submit without it. */
57
+ required: boolean;
58
+ /**
59
+ * Re-reads the platform's verdict, for a control that has just reported one.
60
+ *
61
+ * Call it straight after `setValidity`. Nothing else can: validity changes
62
+ * through `ElementInternals` rather than through a property, so no update is
63
+ * scheduled for it and the message on screen would be the one from before.
64
+ */
65
+ protected refreshFeedback(): void;
66
+ /**
67
+ * Reports "required and empty" against a rendered part, or clears it.
68
+ *
69
+ * `empty` is the control's own answer to whether it holds nothing, `message`
70
+ * is what to say about it, and `anchor` names the part the browser should
71
+ * point its bubble at. Seven families wrote this out by hand.
72
+ */
73
+ protected reportRequired(empty: boolean, message: string, anchor: string): void;
74
+ /** The message actually shown: the consumer's, or the platform's, or none. */
75
+ readonly resolvedFeedback: string;
76
+ }
77
+ /**
78
+ * Grafts a label, help text, and a validation message onto a control.
79
+ *
80
+ * **Over `HelixElement` rather than `HelixFormElement`, because not every
81
+ * control that wants a label is form-associated.** A checkbox group is the
82
+ * case: each box in it submits itself already, so the group deliberately
83
+ * reports no value of its own, and every form-related member of
84
+ * `ElementInternals` throws `NotSupportedError` on an element that is not
85
+ * form-associated. Measured, and the ARIA members are fine on the same
86
+ * element, which is why the naming and describing work there and only the
87
+ * validity has to ask first.
88
+ *
89
+ * Every form control wants the same three things around it and wants them in a
90
+ * different arrangement: a label above an input, beside a checkbox, and over a
91
+ * group. So this owns the properties, the parts, and the wiring, and each
92
+ * control places the nodes where its own geometry needs them. Same part names
93
+ * either way, so one stylesheet in the styled tier dresses all of them.
94
+ *
95
+ * **Not a wrapping `Field` component, and that is not a style preference.** A
96
+ * wrapper cannot name what it wraps. Its label would live in its own shadow
97
+ * root and the control in another, and measured in Chrome 148: an
98
+ * `aria-labelledby` IDREF cannot see across, and
99
+ * `control.ariaLabelledByElements = [labelInTheWrapper]` reads back as zero
100
+ * elements, the reference dropped. The same assignment with a label in the
101
+ * control's own tree is kept. That pattern works in React libraries because
102
+ * they have no shadow boundary; bringing it here brings the ergonomics and none
103
+ * of the conditions that made it work.
104
+ *
105
+ * **A control whose label is its content renders its own.** A checkbox is named
106
+ * by the words beside it, inside the `<label>` that forwards the press, so it
107
+ * uses its default slot and nesting `renderLabel`'s `<label>` in there would be
108
+ * invalid markup. `label` is that slot's fallback either way.
109
+ *
110
+ * Worth knowing, because it costs a label and gives no warning: **a
111
+ * whitespace-only text node is assigned to the default slot and defeats its
112
+ * fallback.** Measured in Chrome 148, and a newline between the tags is enough,
113
+ * so `<helix-checkbox label="Accept">` closed on the next line renders no label
114
+ * at all. A named slot does not have this problem, because `slot="label"` is an
115
+ * attribute and a text node has none. It is the platform's behavior rather
116
+ * than ours, and the same is already true of every slot fallback here.
117
+ *
118
+ * **The ids are fixed rather than generated.** IDREFs resolve within the tree
119
+ * that holds them, so two controls on a page each carrying `helix-hint` in
120
+ * their own shadow root do not collide. That also removes the usual hydration
121
+ * hazard, since there is no generated value for a server and a client to
122
+ * disagree about.
123
+ *
124
+ * **A group has to say it differently.** A control's `<input>` and its hint
125
+ * are both in the shadow root, so a plain `aria-describedby` resolves. A
126
+ * group's host is in the light DOM and its hint is not, so the IDREF cannot
127
+ * reach and `element.ariaDescribedByElements` drops the reference. Only
128
+ * `internals.ariaDescribedByElements` keeps it, which is what `describeHost`
129
+ * uses. Measured the same way, and the failure is silent either way: the
130
+ * description simply never reaches the accessibility tree.
131
+ *
132
+ *
133
+ * **The label, the hint and the feedback are documented on each component that
134
+ * applies this, rather than here.** Eight of the eighteen did and ten did not,
135
+ * which is what `check:parts` now refuses in both directions. Declaring them
136
+ * once on this mixin is the obvious fix and does not work: the analyzer reads
137
+ * `@csspart` off a custom element class, so a class-level tag here reaches the
138
+ * manifest as nothing at all, on this JSDoc and on the inner class alike.
139
+ * Measured both ways, and the inner-class version is the dangerous one, since
140
+ * it satisfies `check:parts` while every API page stays exactly as wrong as
141
+ * before. Members are different and do inherit, which is what the note about
142
+ * the inner class in the area rules is about.
143
+ */
144
+ export declare const MixinField: <T extends Constructor<HelixElement>>(Base: T) => Constructor<MixinFieldInterface> & T;
145
+ export {};
@@ -0,0 +1,423 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, nothing } from 'lit';
8
+ import { property, state } from 'lit/decorators.js';
9
+ import { hostAria } from '../../utils/host-aria/host-aria.js';
10
+ /** Fixed ids, scoped to the shadow root that holds them. See the mixin's note. */
11
+ const HINT_ID = 'helix-hint';
12
+ const FEEDBACK_ID = 'helix-feedback';
13
+ /**
14
+ * Grafts a label, help text, and a validation message onto a control.
15
+ *
16
+ * **Over `HelixElement` rather than `HelixFormElement`, because not every
17
+ * control that wants a label is form-associated.** A checkbox group is the
18
+ * case: each box in it submits itself already, so the group deliberately
19
+ * reports no value of its own, and every form-related member of
20
+ * `ElementInternals` throws `NotSupportedError` on an element that is not
21
+ * form-associated. Measured, and the ARIA members are fine on the same
22
+ * element, which is why the naming and describing work there and only the
23
+ * validity has to ask first.
24
+ *
25
+ * Every form control wants the same three things around it and wants them in a
26
+ * different arrangement: a label above an input, beside a checkbox, and over a
27
+ * group. So this owns the properties, the parts, and the wiring, and each
28
+ * control places the nodes where its own geometry needs them. Same part names
29
+ * either way, so one stylesheet in the styled tier dresses all of them.
30
+ *
31
+ * **Not a wrapping `Field` component, and that is not a style preference.** A
32
+ * wrapper cannot name what it wraps. Its label would live in its own shadow
33
+ * root and the control in another, and measured in Chrome 148: an
34
+ * `aria-labelledby` IDREF cannot see across, and
35
+ * `control.ariaLabelledByElements = [labelInTheWrapper]` reads back as zero
36
+ * elements, the reference dropped. The same assignment with a label in the
37
+ * control's own tree is kept. That pattern works in React libraries because
38
+ * they have no shadow boundary; bringing it here brings the ergonomics and none
39
+ * of the conditions that made it work.
40
+ *
41
+ * **A control whose label is its content renders its own.** A checkbox is named
42
+ * by the words beside it, inside the `<label>` that forwards the press, so it
43
+ * uses its default slot and nesting `renderLabel`'s `<label>` in there would be
44
+ * invalid markup. `label` is that slot's fallback either way.
45
+ *
46
+ * Worth knowing, because it costs a label and gives no warning: **a
47
+ * whitespace-only text node is assigned to the default slot and defeats its
48
+ * fallback.** Measured in Chrome 148, and a newline between the tags is enough,
49
+ * so `<helix-checkbox label="Accept">` closed on the next line renders no label
50
+ * at all. A named slot does not have this problem, because `slot="label"` is an
51
+ * attribute and a text node has none. It is the platform's behavior rather
52
+ * than ours, and the same is already true of every slot fallback here.
53
+ *
54
+ * **The ids are fixed rather than generated.** IDREFs resolve within the tree
55
+ * that holds them, so two controls on a page each carrying `helix-hint` in
56
+ * their own shadow root do not collide. That also removes the usual hydration
57
+ * hazard, since there is no generated value for a server and a client to
58
+ * disagree about.
59
+ *
60
+ * **A group has to say it differently.** A control's `<input>` and its hint
61
+ * are both in the shadow root, so a plain `aria-describedby` resolves. A
62
+ * group's host is in the light DOM and its hint is not, so the IDREF cannot
63
+ * reach and `element.ariaDescribedByElements` drops the reference. Only
64
+ * `internals.ariaDescribedByElements` keeps it, which is what `describeHost`
65
+ * uses. Measured the same way, and the failure is silent either way: the
66
+ * description simply never reaches the accessibility tree.
67
+ *
68
+ *
69
+ * **The label, the hint and the feedback are documented on each component that
70
+ * applies this, rather than here.** Eight of the eighteen did and ten did not,
71
+ * which is what `check:parts` now refuses in both directions. Declaring them
72
+ * once on this mixin is the obvious fix and does not work: the analyzer reads
73
+ * `@csspart` off a custom element class, so a class-level tag here reaches the
74
+ * manifest as nothing at all, on this JSDoc and on the inner class alike.
75
+ * Measured both ways, and the inner-class version is the dangerous one, since
76
+ * it satisfies `check:parts` while every API page stays exactly as wrong as
77
+ * before. Members are different and do inherit, which is what the note about
78
+ * the inner class in the area rules is about.
79
+ */
80
+ export const MixinField = (Base) => {
81
+ // JSDoc is duplicated from MixinFieldInterface deliberately. The interface is
82
+ // what TypeScript consumers see; this class is what the manifest analyzer
83
+ // reads when resolving inherited members into dependent packages.
84
+ class FieldElement extends Base {
85
+ constructor() {
86
+ super(...arguments);
87
+ /**
88
+ * Help under the control, shown while there is nothing wrong.
89
+ */
90
+ this.hint = '';
91
+ /**
92
+ * What is wrong, in the reader's words rather than the browser's.
93
+ */
94
+ this.feedback = '';
95
+ /**
96
+ * The accessible name, and the fallback for the `label` slot.
97
+ */
98
+ this.label = '';
99
+ /**
100
+ * Whether the form refuses to submit without it.
101
+ */
102
+ this.required = false;
103
+ /**
104
+ * Whether the reader has had a go at the field yet.
105
+ *
106
+ * This was `:user-invalid`, which is the platform's own answer to "is this
107
+ * field wrong *yet*" and is the right idea. It does not work here, and the
108
+ * reason is structural rather than a browser bug: `:user-invalid` tracks
109
+ * interaction with the element it is on, and nobody interacts with this
110
+ * element. The reader types into an `<input>` inside its shadow root, so
111
+ * the host's interaction flag is never set and the selector never matches.
112
+ * Measured after submitting an invalid required field and again after
113
+ * editing one and leaving it: false both times, while `:invalid` matched
114
+ * both times.
115
+ *
116
+ * So the moments are listened for instead, and both of them demonstrably
117
+ * reach the host: `invalid` when a form is submitted with this control
118
+ * failing, and `change` when the reader has edited and committed. Focus
119
+ * alone is deliberately not one of them. Tabbing past an empty required
120
+ * field should not be answered with an error.
121
+ *
122
+ * **A composite control has to construct its own `change`, and this mixin
123
+ * deliberately does not do it for them.** A native `change` is
124
+ * `composed: false`, so a control built from several inner inputs never
125
+ * relays one to its host: `Input` gets this free from its one control, and
126
+ * `PinInput` had four boxes, none of whose `change` means the code is
127
+ * entered, so a half-entered code was never `touched` and could never say
128
+ * what was wrong with it. See 311.
129
+ *
130
+ * **The obvious shared fix is for `touched` to take a `focusout`, and it is
131
+ * the wrong one**, because it is the paragraph above undone: a `focusout`
132
+ * fires on the reader who tabbed past and typed nothing, which is exactly
133
+ * the case this refuses to call an error. What a composite needs is not a
134
+ * focus moment, it is the native meaning of `change`, "edited, then left",
135
+ * and the editing half is the part a mixin up here cannot see.
136
+ *
137
+ * **Nor is there one shape to extract yet.** The two composites in the
138
+ * library solve it differently and both are right: `PinInput` is
139
+ * focus-driven, taking a baseline when focus arrives from outside the row
140
+ * and announcing on the way out if it moved, and `DateInput` is
141
+ * value-driven, stopping its inner `change` and announcing once the date
142
+ * itself moves. Generalizing from those two would be generalizing from one,
143
+ * since only one of them has a leaving moment at all. The trigger for
144
+ * lifting it is a third control that needs the focus-driven shape, at which
145
+ * point what moves up is the baseline-and-commit pair rather than `touched`.
146
+ */
147
+ this.touched = false;
148
+ /** What the last render put on screen, so a change can be noticed. */
149
+ this.shownFeedback = '';
150
+ }
151
+ /**
152
+ * Whether this element may be asked about its own validity at all.
153
+ *
154
+ * Reading the static the platform itself reads to decide, rather than
155
+ * probing: `validity` and `validationMessage` throw `NotSupportedError` on
156
+ * an element that is not form-associated, and a group whose members each
157
+ * submit themselves is not. Such a control still shows a message, it just
158
+ * has to be given one.
159
+ */
160
+ get validates() {
161
+ return this.constructor.formAssociated === true;
162
+ }
163
+ /**
164
+ * @internal The message actually shown: the consumer's, or the platform's,
165
+ * or none. Wiring a field does to itself, read by its own template. A
166
+ * consumer sets `feedback` and reads validity from the platform.
167
+ */
168
+ get resolvedFeedback() {
169
+ if (this.feedback !== '') {
170
+ return this.feedback;
171
+ }
172
+ if (!this.validates || !this.touched || this.internals.validity.valid) {
173
+ return '';
174
+ }
175
+ return this.internals.validationMessage;
176
+ }
177
+ /**
178
+ * @internal Id of the description node, for `aria-describedby` on the
179
+ * control. Written by the field's own template into its own shadow root.
180
+ *
181
+ * One at a time, because only one of the two is ever rendered. Naming both
182
+ * would point a screen reader at an id that resolves to nothing, which it
183
+ * reads as a description it cannot find.
184
+ */
185
+ get describedBy() {
186
+ if (this.resolvedFeedback !== '') {
187
+ return FEEDBACK_ID;
188
+ }
189
+ return this.hint === '' ? undefined : HINT_ID;
190
+ }
191
+ /**
192
+ * A label over a set rather than beside a control.
193
+ *
194
+ * A plain element and not a `<label for>`, because a group is not a single
195
+ * control and there is nothing for `for` to point at. What names the group
196
+ * is `linkHost`, pointing the host's internals at this element.
197
+ */
198
+ renderGroupLabel() {
199
+ /* A set names itself through `hostAria`, so an `aria-label` here already
200
+ reaches the group. The label is still rendered and clipped, because
201
+ `renderGroupLabel` is what the styled tier lays out and a group that
202
+ drew nothing would collapse its own first row. */
203
+ const hidden = this.label === '' ? this.getAttribute('aria-label') : null;
204
+ if (this.label === '' && !hidden) {
205
+ return nothing;
206
+ }
207
+ return html `<span part="label" ?data-hidden=${hidden !== null}
208
+ ><slot name="label">${this.label || hidden}</slot></span
209
+ >`;
210
+ }
211
+ /**
212
+ * A label above the control, pointing at it.
213
+ *
214
+ * A real `<label for>`, so pressing the words focuses the field and the
215
+ * browser does the forwarding. It resolves because `for` looks within the
216
+ * same tree, and the control it names is in this shadow root.
217
+ *
218
+ * **Nothing at all when there is no label**, which is the same rule the
219
+ * hint and the feedback follow: none of the three is always used, and an
220
+ * element rendered empty is not free. It still generates a box, so it still
221
+ * takes a row of the grid and a gap on either side of it, which is a field
222
+ * that has moved down the page for a label nobody wrote. `nothing` leaves
223
+ * comment markers rather than a node, so the gap has nothing to sit between.
224
+ *
225
+ * **`aria-label` names without showing**, and it is forwarded rather than
226
+ * left where the consumer put it. A custom element has no way to delegate a
227
+ * label to the control inside it, so an `aria-label` on the host reaches
228
+ * nothing: measured, the inner input came back with no label and no name of
229
+ * its own while the attribute sat on the wrapper. What happens instead is
230
+ * that the label is rendered and clipped, so the name arrives through the
231
+ * same `<label for>` a visible one does.
232
+ *
233
+ * This replaced `hide-label`, which was a second attribute to say what the
234
+ * absence of the first should have said. The rule is now `label` means a
235
+ * label, `aria-label` means a name, and neither means an unnamed field a
236
+ * screen reader announces as "edit text"
237
+ */
238
+ renderLabel() {
239
+ const hidden = this.label === '' ? this.getAttribute('aria-label') : null;
240
+ if (this.label === '' && !hidden) {
241
+ return nothing;
242
+ }
243
+ return html `<label part="label" for="control" ?data-hidden=${hidden !== null}
244
+ ><slot name="label">${this.label || hidden}</slot></label
245
+ >`;
246
+ }
247
+ /**
248
+ * Help under the control, unless something has gone wrong.
249
+ *
250
+ * The two share a place rather than stacking, so a field is one line of
251
+ * small text either way and nothing below it moves when the message
252
+ * changes. What the reader needs at that moment is what is wrong, and a
253
+ * hint that survives the error it describes is a second line to read before
254
+ * getting to the one that matters.
255
+ */
256
+ renderHint() {
257
+ if (this.hint === '' || this.resolvedFeedback !== '') {
258
+ return nothing;
259
+ }
260
+ return html `<p part="hint" id=${HINT_ID}><slot name="hint">${this.hint}</slot></p>`;
261
+ }
262
+ /**
263
+ * The validation message, in the hint's place.
264
+ *
265
+ * `role="alert"` rather than a live region on the whole field, so what is
266
+ * announced is the message and not the label and help along with it.
267
+ */
268
+ renderFeedback() {
269
+ const message = this.resolvedFeedback;
270
+ if (message === '') {
271
+ return nothing;
272
+ }
273
+ return html `<p part="feedback" id=${FEEDBACK_ID} role="alert">
274
+ <slot name="feedback">${message}</slot>
275
+ </p>`;
276
+ }
277
+ /**
278
+ * Points the host at its own label and description nodes, for a group.
279
+ *
280
+ * A control's `<input>` and its hint are both in the shadow root, so a
281
+ * plain `aria-describedby` resolves between them. A group's host is in the
282
+ * light DOM and its hint is not, so the IDREF cannot reach and
283
+ * `element.ariaDescribedByElements` drops the reference outright. Only the
284
+ * `ElementInternals` form keeps it, measured in Chrome 148: two of two
285
+ * kept through internals, zero of one through the element.
286
+ *
287
+ * Client only, and deliberately, because element references need the nodes
288
+ * to exist and on the server they do not. The name is therefore also set as
289
+ * a plain string by the component, which a server can write, and the
290
+ * reference here overrides it wherever a consumer slotted something richer
291
+ * than a string. A group with only a property for a label reads the same
292
+ * either way.
293
+ */
294
+ linkHost() {
295
+ const label = this.renderRoot.querySelector('[part="label"]');
296
+ hostAria(this.internals, {
297
+ describedBy: [HINT_ID, FEEDBACK_ID]
298
+ .map((id) => this.renderRoot.querySelector(`#${id}`))
299
+ .filter((node) => node !== null),
300
+ labelledBy: label ? [label] : [],
301
+ });
302
+ }
303
+ /**
304
+ * Re-reads the platform's verdict, for a control that has just reported one.
305
+ *
306
+ * Validity changes through `ElementInternals` rather than through a
307
+ * property, so nothing schedules an update for it. Worse, the order is
308
+ * against us: this mixin sits above the control in the prototype chain, so
309
+ * its `updated` runs before the control's own, which is where `setValidity`
310
+ * is called. Reading the verdict here would always read the previous one,
311
+ * which is what left a corrected field still showing the error it had
312
+ * before. So the control says when it has reported, and this schedules the
313
+ * one further update that needs. It converges because the second pass finds
314
+ * nothing changed.
315
+ */
316
+ refreshFeedback() {
317
+ const next = this.resolvedFeedback;
318
+ if (next !== this.shownFeedback) {
319
+ this.shownFeedback = next;
320
+ this.requestUpdate();
321
+ }
322
+ }
323
+ /**
324
+ * Reports "required and empty" and refreshes the message, or clears both.
325
+ *
326
+ * Seven families had this written out: the same `valueMissing`, the same
327
+ * anchor lookup, the same `refreshFeedback` under the same comment about
328
+ * validity moving without a property moving. Seven copies of a five-line
329
+ * shape is the threshold at which this mixin itself was extracted, and two
330
+ * of them had already drifted, `checkbox-group` having no validity at all
331
+ * until yesterday.
332
+ *
333
+ * What differs between them is exactly two things, so those are the
334
+ * arguments: whether the control is empty, which only it can say, and the
335
+ * sentence to say about it, which is different for a box, a switch and a
336
+ * set. The anchor is looked up here because every one of them anchored on a
337
+ * rendered part and the browser needs somewhere to point the bubble.
338
+ *
339
+ * The message is not derived from the control's kind, deliberately. "Tick
340
+ * this box" and "Choose one" are not the same instruction and a lookup
341
+ * table would be one more thing to keep in step with the components.
342
+ */
343
+ reportRequired(empty, message, anchor) {
344
+ if (this.required && empty) {
345
+ this.internals.setValidity({ valueMissing: true }, message, this.renderRoot?.querySelector(`[part="${anchor}"]`) ?? undefined);
346
+ }
347
+ else {
348
+ this.internals.setValidity({});
349
+ }
350
+ // Straight after, because validity moved without any property moving.
351
+ this.refreshFeedback();
352
+ }
353
+ /**
354
+ * Records "wrong, and the reader knows it" as the custom state `invalid`.
355
+ *
356
+ * The styled tier selects on this rather than on `:host(:user-invalid)`,
357
+ * which cannot match for the reason above, so the invalid border never
358
+ * arrived either. One state, and the message and the border agree by
359
+ * construction.
360
+ *
361
+ * **A state and not `data-invalid`, since 27 Aug 2026.** A field served
362
+ * with a `feedback` message already failing was written invalid on its
363
+ * first update, which is an attribute the consumer's server never sent.
364
+ * `HelixElement.toggleState` carries the argument.
365
+ *
366
+ * In `willUpdate`, which is where the resolved message is first known.
367
+ */
368
+ willUpdate(changed) {
369
+ super.willUpdate(changed);
370
+ this.shownFeedback = this.resolvedFeedback;
371
+ this.toggleState('invalid', this.shownFeedback !== '');
372
+ /* And say it to the accessibility tree, which nothing did.
373
+ *
374
+ * A field carrying a `feedback` message painted red, and was described by
375
+ * that message through `aria-describedby`, and was never announced as
376
+ * being *wrong*: a reader heard the message as though it were help text.
377
+ * `aria-invalid` is the one word for it, and this is the one place that
378
+ * knows, so every field in the library gets it from here. */
379
+ this.internals.ariaInvalid = this.shownFeedback === '' ? null : 'true';
380
+ }
381
+ /**
382
+ * The two moments a field becomes wrong, neither of which is a property
383
+ * change, so nothing else would schedule an update for them.
384
+ *
385
+ * In `firstUpdated` rather than `connectedCallback`, which SSR never calls.
386
+ */
387
+ firstUpdated(changed) {
388
+ super.firstUpdated(changed);
389
+ const touch = () => {
390
+ this.touched = true;
391
+ };
392
+ this.addEventListener('invalid', touch);
393
+ this.addEventListener('change', touch);
394
+ }
395
+ }
396
+ __decorate([
397
+ property()
398
+ ], FieldElement.prototype, "hint", void 0);
399
+ __decorate([
400
+ property()
401
+ ], FieldElement.prototype, "feedback", void 0);
402
+ __decorate([
403
+ property()
404
+ ], FieldElement.prototype, "label", void 0);
405
+ __decorate([
406
+ property({ reflect: true, type: Boolean })
407
+ ], FieldElement.prototype, "required", void 0);
408
+ __decorate([
409
+ state()
410
+ ], FieldElement.prototype, "touched", void 0);
411
+ /* Through `unknown`, which the protected members make necessary: TypeScript
412
+ treats two protected members as compatible only when they come from the
413
+ same declaration, and these come from two, the class here and the declared
414
+ interface above. Making them public would type-check without a word and put
415
+ five render helpers on the public API page of every form control, which is
416
+ what the interface exists to prevent.
417
+
418
+ The contract is enforced by use rather than by the cast. Every helper is
419
+ called from `Input` or `Checkbox`, so an interface that drifts from the
420
+ class fails their builds, which is exactly how the missing declarations
421
+ were found. */
422
+ return FieldElement;
423
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The element a floating surface should open against, given the slot it wraps.
3
+ *
4
+ * **A wrapper with no box is not an anchor.** `display: contents` puts an
5
+ * element's children into the layout and leaves the element itself without a
6
+ * box, so its `getBoundingClientRect()` is `0 0 0 0`. Measured: a tooltip host
7
+ * reports exactly that while the button inside it reports 32 by 32. Anchoring
8
+ * to it opens the surface against the top left of the window, and focusing it
9
+ * on close puts focus nowhere.
10
+ *
11
+ * That is not an edge case, because wrapping is how a tooltip attaches to the
12
+ * thing it describes. Without this, an icon button used as a trigger is the one
13
+ * button on a toolbar that cannot have a tooltip.
14
+ *
15
+ * So this descends past anything that draws nothing, and the first child with a
16
+ * box is the anchor. The wrapper is transparent by construction, so the thing
17
+ * with the box is the thing the reader is pointing at.
18
+ *
19
+ * Bounded rather than a walk to the leaves: a chain of box-less wrappers is
20
+ * already unusual, and descending without a limit through a tree this library
21
+ * does not own is worse than stopping and anchoring to something imperfect.
22
+ */
23
+ export declare const anchorElement: (slot: HTMLSlotElement | null | undefined, fallback: HTMLElement) => HTMLElement;