@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.
- package/LICENSE +21 -0
- package/custom-elements.json +8005 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
- package/dist/controllers/command-controller/command-controller.d.ts +83 -0
- package/dist/controllers/command-controller/command-controller.js +93 -0
- package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
- package/dist/controllers/copy-controller/copy-controller.js +128 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
- package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
- package/dist/controllers/floating-controller/floating-controller.js +227 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
- package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
- package/dist/controllers/popover-controller/popover-controller.js +150 -0
- package/dist/controllers/source-controller/source-controller.d.ts +55 -0
- package/dist/controllers/source-controller/source-controller.js +94 -0
- package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
- package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
- package/dist/events/element-events.d.ts +4 -0
- package/dist/events/element-events.js +34 -0
- package/dist/events/events.d.ts +743 -0
- package/dist/events/events.js +799 -0
- package/dist/helix-element/helix-element.d.ts +201 -0
- package/dist/helix-element/helix-element.js +695 -0
- package/dist/helix-form-element/helix-form-element.d.ts +152 -0
- package/dist/helix-form-element/helix-form-element.js +194 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +54 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
- package/dist/mixins/mixin-collection/index.d.ts +1 -0
- package/dist/mixins/mixin-collection/index.js +1 -0
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
- package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
- package/dist/mixins/mixin-field/mixin-field.js +423 -0
- package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
- package/dist/utils/anchor-element/anchor-element.js +39 -0
- package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
- package/dist/utils/capture-pointer/capture-pointer.js +30 -0
- package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
- package/dist/utils/closest-instance/closest-instance.js +34 -0
- package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
- package/dist/utils/create-sheet/create-sheet.js +16 -0
- package/dist/utils/custom-element/custom-element.d.ts +26 -0
- package/dist/utils/custom-element/custom-element.js +114 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
- package/dist/utils/draw-icon/draw-icon.js +39 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
- package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
- package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
- package/dist/utils/elapsed-time/elapsed-time.js +117 -0
- package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
- package/dist/utils/focusable-elements/focusable-elements.js +88 -0
- package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
- package/dist/utils/format-numbers/format-numbers.js +29 -0
- package/dist/utils/host-aria/host-aria.d.ts +38 -0
- package/dist/utils/host-aria/host-aria.js +41 -0
- package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
- package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
- package/dist/utils/line-path/line-path.d.ts +23 -0
- package/dist/utils/line-path/line-path.js +107 -0
- package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
- package/dist/utils/on-unless-false/on-unless-false.js +32 -0
- package/dist/utils/read-series/read-series.d.ts +78 -0
- package/dist/utils/read-series/read-series.js +187 -0
- package/dist/utils/relative-time/relative-time.d.ts +24 -0
- package/dist/utils/relative-time/relative-time.js +84 -0
- package/dist/utils/request-change/request-change.d.ts +48 -0
- package/dist/utils/request-change/request-change.js +7 -0
- package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
- package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
- package/dist/utils/restore-value/restore-value.d.ts +32 -0
- package/dist/utils/restore-value/restore-value.js +84 -0
- package/dist/utils/scale/scale.d.ts +23 -0
- package/dist/utils/scale/scale.js +32 -0
- package/dist/utils/scale/ticks.d.ts +27 -0
- package/dist/utils/scale/ticks.js +52 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
- package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
- package/dist/utils/slot-filled/slot-filled.js +26 -0
- package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
- package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
- package/dist/utils/ticker/ticker.d.ts +8 -0
- package/dist/utils/ticker/ticker.js +55 -0
- package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
- package/dist/utils/tone-marks/tone-marks.js +36 -0
- package/dist/utils/value-as/value-as.d.ts +64 -0
- package/dist/utils/value-as/value-as.js +105 -0
- 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;
|