@fusion.dev/helix-core 0.0.0-dev → 0.0.3
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 +156 -124
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +2 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +44 -17
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +5 -6
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -6
- package/dist/controllers/command-controller/command-controller.d.ts +5 -7
- package/dist/controllers/command-controller/command-controller.js +5 -7
- package/dist/controllers/copy-controller/copy-controller.d.ts +2 -4
- package/dist/controllers/copy-controller/copy-controller.js +7 -9
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +10 -17
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +11 -18
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +44 -6
- package/dist/controllers/dismiss-controller/dismiss-controller.js +77 -10
- package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +3 -6
- package/dist/controllers/drilldown-controller/drilldown-controller.js +3 -6
- package/dist/controllers/floating-controller/floating-controller.d.ts +7 -8
- package/dist/controllers/floating-controller/floating-controller.js +69 -38
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +7 -10
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +17 -29
- package/dist/controllers/indicator-controller/indicator-controller.js +11 -21
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +3 -13
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +11 -24
- package/dist/controllers/popover-controller/popover-controller.d.ts +2 -7
- package/dist/controllers/popover-controller/popover-controller.js +3 -9
- package/dist/controllers/source-controller/source-controller.d.ts +2 -5
- package/dist/controllers/source-controller/source-controller.js +2 -5
- package/dist/controllers/token-color-controller/token-color-controller.d.ts +2 -4
- package/dist/controllers/token-color-controller/token-color-controller.js +2 -4
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +1 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.js +15 -15
- package/dist/events/element-events.js +0 -5
- package/dist/events/events.d.ts +55 -93
- package/dist/events/events.js +68 -105
- package/dist/helix-element/helix-element.d.ts +26 -27
- package/dist/helix-element/helix-element.js +83 -100
- package/dist/helix-form-element/helix-form-element.d.ts +11 -17
- package/dist/helix-form-element/helix-form-element.js +15 -21
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +6 -14
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +12 -25
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +1 -7
- package/dist/mixins/mixin-collection/mixin-collection.js +16 -23
- package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +15 -16
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +28 -42
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +15 -17
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +33 -45
- package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +4 -4
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +29 -45
- package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +10 -18
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +14 -31
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +10 -15
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +10 -15
- package/dist/mixins/mixin-field/mixin-field.d.ts +10 -12
- package/dist/mixins/mixin-field/mixin-field.js +48 -64
- package/dist/utils/capture-pointer/capture-pointer.d.ts +1 -4
- package/dist/utils/capture-pointer/capture-pointer.js +1 -4
- package/dist/utils/closest-instance/closest-instance.d.ts +6 -10
- package/dist/utils/closest-instance/closest-instance.js +6 -10
- package/dist/utils/create-sheet/create-sheet.d.ts +1 -1
- package/dist/utils/create-sheet/create-sheet.js +1 -1
- package/dist/utils/custom-element/custom-element.d.ts +0 -21
- package/dist/utils/custom-element/custom-element.js +1 -1
- package/dist/utils/draw-icon/draw-icon.d.ts +3 -3
- package/dist/utils/draw-icon/draw-icon.js +3 -3
- package/dist/utils/elapsed-time/elapsed-time.js +1 -3
- package/dist/utils/focusable-elements/focusable-elements.d.ts +3 -4
- package/dist/utils/focusable-elements/focusable-elements.js +9 -11
- package/dist/utils/format-numbers/format-numbers.js +1 -4
- package/dist/utils/host-aria/host-aria.d.ts +2 -2
- package/dist/utils/host-aria/host-aria.js +2 -2
- package/dist/utils/line-path/line-path.js +1 -1
- package/dist/utils/on-unless-false/on-unless-false.d.ts +0 -5
- package/dist/utils/on-unless-false/on-unless-false.js +0 -5
- package/dist/utils/read-series/read-series.d.ts +26 -0
- package/dist/utils/read-series/read-series.js +26 -29
- package/dist/utils/relative-time/relative-time.js +5 -6
- package/dist/utils/request-change/request-change.d.ts +9 -20
- package/dist/utils/resolve-active-item/resolve-active-item.d.ts +3 -3
- package/dist/utils/resolve-active-item/resolve-active-item.js +3 -3
- package/dist/utils/restore-value/restore-value.js +1 -6
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +4 -5
- package/dist/utils/slot-filled/slot-filled.d.ts +2 -4
- package/dist/utils/slot-filled/slot-filled.js +2 -4
- package/dist/utils/tell-disclosure/tell-disclosure.d.ts +15 -18
- package/dist/utils/tell-disclosure/tell-disclosure.js +15 -18
- package/dist/utils/tone-marks/tone-marks.d.ts +10 -16
- package/dist/utils/tone-marks/tone-marks.js +6 -10
- package/dist/utils/value-as/value-as.d.ts +0 -28
- package/dist/utils/value-as/value-as.js +5 -10
- package/package.json +6 -6
|
@@ -49,6 +49,8 @@ export declare class AriaForwardController implements ReactiveController {
|
|
|
49
49
|
private observer;
|
|
50
50
|
/** Which state attributes this controller wrote, so only those are removed. */
|
|
51
51
|
private readonly forwarded;
|
|
52
|
+
/** The control the record above is about, so a replacement resets it. */
|
|
53
|
+
private recorded;
|
|
52
54
|
constructor(host: ReactiveControllerHost & HTMLElement, options: AriaForwardOptions);
|
|
53
55
|
/**
|
|
54
56
|
* Resolves the host's references and hands them to the control.
|
|
@@ -12,22 +12,19 @@ const RELATIONSHIPS = [
|
|
|
12
12
|
* wherever it is written, so the whole of the work is putting it on the node the
|
|
13
13
|
* accessibility tree actually exposes.
|
|
14
14
|
*
|
|
15
|
-
* **
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* forever". A plain `<button slot="trigger">` never had the problem, because
|
|
22
|
-
* there the host *is* the focusable node.
|
|
15
|
+
* **A state written on a host reaches nobody.** `Megamenu` and `SpeedDial` write
|
|
16
|
+
* `aria-expanded` onto whatever a consumer slots, and where that is a
|
|
17
|
+
* `<helix-button>` it lands on a host the tree reports as `generic`: measured
|
|
18
|
+
* through Chrome's own tree, a button named "Mega" with no expanded state at
|
|
19
|
+
* all. A plain `<button slot="trigger">` never had the problem, because there
|
|
20
|
+
* the host *is* the focusable node.
|
|
23
21
|
*
|
|
24
|
-
* **Only what this controller put there is ever taken away
|
|
25
|
-
*
|
|
26
|
-
* the control whenever the host lacked it, which reads as the obvious way to
|
|
22
|
+
* **Only what this controller put there is ever taken away.** Removing any of
|
|
23
|
+
* these from the control whenever the host lacks it reads as the obvious way to
|
|
27
24
|
* keep the two in step and quietly deletes what the component renders for
|
|
28
25
|
* itself: `Combobox` writes `aria-controls="listbox"` onto its own input and
|
|
29
|
-
* `Select` writes `aria-expanded` onto its own trigger, and
|
|
30
|
-
*
|
|
26
|
+
* `Select` writes `aria-expanded` onto its own trigger, and a controller
|
|
27
|
+
* carrying nothing would wipe both on every update.
|
|
31
28
|
*
|
|
32
29
|
* So the record below holds what was forwarded last time, and a host that stops
|
|
33
30
|
* carrying one removes exactly that and nothing else. A control's own attribute
|
|
@@ -97,6 +94,21 @@ export class AriaForwardController {
|
|
|
97
94
|
if (!target) {
|
|
98
95
|
return;
|
|
99
96
|
}
|
|
97
|
+
/* **The record belongs to the element it was written about.** `forwarded`
|
|
98
|
+
says what this controller put on the control. Kept across a replacement,
|
|
99
|
+
it claims a value the new element never had, and the clearing branches
|
|
100
|
+
below then take off what the component wrote itself: the fault the record
|
|
101
|
+
exists to prevent, one element later.
|
|
102
|
+
|
|
103
|
+
**No component in this library replaces its control**, which is worth
|
|
104
|
+
saying because the comment here used to name one. All fifteen consumers
|
|
105
|
+
resolve to a single fixed part rendered once. `target` is a callback and
|
|
106
|
+
its contract permits a swap, so a controller in `core` is held to the
|
|
107
|
+
contract rather than to the fifteen callers it happens to have today. */
|
|
108
|
+
if (target !== this.recorded) {
|
|
109
|
+
this.forwarded.clear();
|
|
110
|
+
this.recorded = target;
|
|
111
|
+
}
|
|
100
112
|
const root = this.host.getRootNode();
|
|
101
113
|
for (const [attribute, property] of RELATIONSHIPS) {
|
|
102
114
|
if (!(property in target)) {
|
|
@@ -109,8 +121,24 @@ export class AriaForwardController {
|
|
|
109
121
|
const elements = ids
|
|
110
122
|
.map((id) => root.getElementById?.(id) ?? null)
|
|
111
123
|
.filter((element) => element !== null);
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
const reflect = target;
|
|
125
|
+
if (elements.length > 0) {
|
|
126
|
+
reflect[property] = elements;
|
|
127
|
+
this.forwarded.add(attribute);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
/* Only if this controller is what put it there, which is the same rule
|
|
131
|
+
the states below keep and for a worse reason. `MixinField` writes
|
|
132
|
+
`aria-describedby` on the control for its own hint, and assigning null
|
|
133
|
+
here removes the content attribute, so forwarding nothing wiped the
|
|
134
|
+
component's own description on all eighteen form controls. Invisible to
|
|
135
|
+
every DOM spec: happy-dom implements no ARIA element reflection, so the
|
|
136
|
+
`property in target` guard above skips this whole branch and the suite
|
|
137
|
+
is green against code that clears a description in every browser. */
|
|
138
|
+
if (this.forwarded.has(attribute)) {
|
|
139
|
+
reflect[property] = null;
|
|
140
|
+
this.forwarded.delete(attribute);
|
|
141
|
+
}
|
|
114
142
|
}
|
|
115
143
|
for (const attribute of STATES) {
|
|
116
144
|
const value = this.host.getAttribute(attribute);
|
|
@@ -138,8 +166,7 @@ export class AriaForwardController {
|
|
|
138
166
|
}
|
|
139
167
|
/* After the render that created the control, because there is nothing to
|
|
140
168
|
forward to before it exists. Every update, not only the first: a component
|
|
141
|
-
that re-renders its control
|
|
142
|
-
between an anchor and a button, produces a new element with none of this. */
|
|
169
|
+
that re-renders its control produces a new element with none of this. */
|
|
143
170
|
hostUpdated() {
|
|
144
171
|
this.forward();
|
|
145
172
|
}
|
|
@@ -42,12 +42,11 @@ export declare class ChartPointerController implements ReactiveController {
|
|
|
42
42
|
* Which axis the readings are laid out along.
|
|
43
43
|
*
|
|
44
44
|
* **An axis and not a boolean, because two of the four charts do not use x.**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* that would not follow and was a tooltip reading the wrong axis.
|
|
45
|
+
* A horizontal bar chart runs its categories *down* the box, so resolving
|
|
46
|
+
* every reading from the pointer's x leaves a move from the first row to the
|
|
47
|
+
* last never changing the answer: measured on four rows, all four reported
|
|
48
|
+
* the third, which is the slot the middle of the box falls in. It looks like
|
|
49
|
+
* a tooltip that will not follow and is a tooltip reading the wrong axis.
|
|
51
50
|
*
|
|
52
51
|
* `none` is the ring. Its slices are real elements at every angle, so the
|
|
53
52
|
* reading under the pointer is the one the pointer is over, and arithmetic on
|
|
@@ -34,12 +34,11 @@ export class ChartPointerController {
|
|
|
34
34
|
* Which axis the readings are laid out along.
|
|
35
35
|
*
|
|
36
36
|
* **An axis and not a boolean, because two of the four charts do not use x.**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* that would not follow and was a tooltip reading the wrong axis.
|
|
37
|
+
* A horizontal bar chart runs its categories *down* the box, so resolving
|
|
38
|
+
* every reading from the pointer's x leaves a move from the first row to the
|
|
39
|
+
* last never changing the answer: measured on four rows, all four reported
|
|
40
|
+
* the third, which is the slot the middle of the box falls in. It looks like
|
|
41
|
+
* a tooltip that will not follow and is a tooltip reading the wrong axis.
|
|
43
42
|
*
|
|
44
43
|
* `none` is the ring. Its slices are real elements at every angle, so the
|
|
45
44
|
* reading under the pointer is the one the pointer is over, and arithmetic on
|
|
@@ -56,14 +56,12 @@ export interface CommandOptions {
|
|
|
56
56
|
* for a click, for Enter, for Space, and for anything else the platform decides
|
|
57
57
|
* counts as activation later.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* semantics we would have to guess at, and a convention that is ours to
|
|
63
|
-
* document and theirs to learn.
|
|
59
|
+
* A `data-` recipe of our own would mean a delegated document listener a
|
|
60
|
+
* consumer cannot see, an id lookup and activation semantics we would write
|
|
61
|
+
* ourselves, and a convention that is ours to document and theirs to learn.
|
|
64
62
|
*
|
|
65
|
-
* **Only custom commands reach a custom element.** The built-in ones
|
|
66
|
-
* `show-modal`, `close`, `toggle-popover`
|
|
63
|
+
* **Only custom commands reach a custom element.** The built-in ones
|
|
64
|
+
* (`show-modal`, `close`, `toggle-popover`) are defined against native elements
|
|
67
65
|
* and the browser applies them itself; pointed at a custom element they do
|
|
68
66
|
* nothing. So the names here start with `--`, which is the platform's marker
|
|
69
67
|
* for "the target decides what this means", and the target deciding is exactly
|
|
@@ -53,14 +53,12 @@ export class HelixFallbackCommandEvent extends Event {
|
|
|
53
53
|
* for a click, for Enter, for Space, and for anything else the platform decides
|
|
54
54
|
* counts as activation later.
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* semantics we would have to guess at, and a convention that is ours to
|
|
60
|
-
* document and theirs to learn.
|
|
56
|
+
* A `data-` recipe of our own would mean a delegated document listener a
|
|
57
|
+
* consumer cannot see, an id lookup and activation semantics we would write
|
|
58
|
+
* ourselves, and a convention that is ours to document and theirs to learn.
|
|
61
59
|
*
|
|
62
|
-
* **Only custom commands reach a custom element.** The built-in ones
|
|
63
|
-
* `show-modal`, `close`, `toggle-popover`
|
|
60
|
+
* **Only custom commands reach a custom element.** The built-in ones
|
|
61
|
+
* (`show-modal`, `close`, `toggle-popover`) are defined against native elements
|
|
64
62
|
* and the browser applies them itself; pointed at a custom element they do
|
|
65
63
|
* nothing. So the names here start with `--`, which is the platform's marker
|
|
66
64
|
* for "the target decides what this means", and the target deciding is exactly
|
|
@@ -21,10 +21,8 @@ export interface CopyOptions {
|
|
|
21
21
|
* case wrong.
|
|
22
22
|
*
|
|
23
23
|
* **There are two routes and both are tried**, because the modern one is gated
|
|
24
|
-
* in more places than it looks
|
|
25
|
-
*
|
|
26
|
-
* component using this reported as not working, and a control that does nothing
|
|
27
|
-
* is indistinguishable from a broken one. See `write` above.
|
|
24
|
+
* in more places than it looks and a control that does nothing is
|
|
25
|
+
* indistinguishable from a broken one. See `write` above.
|
|
28
26
|
*
|
|
29
27
|
* **Where neither route is allowed, the failure is silence.** The confirmation
|
|
30
28
|
* does not appear, which is the honest report: a message about a clipboard is
|
|
@@ -57,10 +57,8 @@ const write = async (text) => {
|
|
|
57
57
|
* case wrong.
|
|
58
58
|
*
|
|
59
59
|
* **There are two routes and both are tried**, because the modern one is gated
|
|
60
|
-
* in more places than it looks
|
|
61
|
-
*
|
|
62
|
-
* component using this reported as not working, and a control that does nothing
|
|
63
|
-
* is indistinguishable from a broken one. See `write` above.
|
|
60
|
+
* in more places than it looks and a control that does nothing is
|
|
61
|
+
* indistinguishable from a broken one. See `write` above.
|
|
64
62
|
*
|
|
65
63
|
* **Where neither route is allowed, the failure is silence.** The confirmation
|
|
66
64
|
* does not appear, which is the honest report: a message about a clipboard is
|
|
@@ -98,11 +96,11 @@ export class CopyController {
|
|
|
98
96
|
}
|
|
99
97
|
/* **The host may have gone while the clipboard was being written.** Writing
|
|
100
98
|
is asynchronous, `hostDisconnected` runs the moment the element leaves,
|
|
101
|
-
and it clears a timer that does not exist yet
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
and it clears a timer that does not exist yet, so without this the
|
|
100
|
+
confirmation starts a fresh timer on a detached host and calls
|
|
101
|
+
`requestUpdate` on it a second later. Checked here rather than in
|
|
102
|
+
`reset`, because the thing to skip is the whole confirmation and not only
|
|
103
|
+
its clock. */
|
|
106
104
|
if (this.host instanceof HTMLElement && !this.host.isConnected) {
|
|
107
105
|
return;
|
|
108
106
|
}
|
|
@@ -21,24 +21,17 @@ type DisableableHost = ReactiveControllerHost & HTMLElement & {
|
|
|
21
21
|
* Pushes a group's `disabled` onto its members, on every update, unasked.
|
|
22
22
|
*
|
|
23
23
|
* **The cascade itself is `inheritDisabled`, and this is what makes it
|
|
24
|
-
* automatic.**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* the
|
|
24
|
+
* automatic.** Called by hand it runs only when whichever method holds the call
|
|
25
|
+
* runs, so a group whose items changed by some other path leaves them holding
|
|
26
|
+
* the last answer, and a group that forgets the call gets no cascade at all
|
|
27
|
+
* with nothing to say so: the property exists, the attribute reflects, and the
|
|
28
|
+
* members simply ignore it. That is invisible in review and shows up only when
|
|
29
|
+
* someone disables a set.
|
|
28
30
|
*
|
|
29
|
-
* It
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* It has to be remembered. A fifth group gets no cascade at all, and nothing
|
|
34
|
-
* says so: the property exists, the attribute reflects, and the members simply
|
|
35
|
-
* ignore it. That is the failure this exists to make impossible, because it is
|
|
36
|
-
* invisible in review and only shows up when someone disables a set.
|
|
37
|
-
*
|
|
38
|
-
* And it adds no API. A group already has `disabled`, from `HelixFormElement`
|
|
39
|
-
* or from the consumer; nothing here introduces a property or a method for
|
|
40
|
-
* anyone to call. By this repo's own rule that makes it a controller and not a
|
|
41
|
-
* mixin: it reacts to a lifecycle and nothing else.
|
|
31
|
+
* It adds no API. A group already has `disabled`, from `HelixFormElement` or
|
|
32
|
+
* from the consumer; nothing here introduces a property or a method for anyone
|
|
33
|
+
* to call. By this repo's own rule that makes it a controller and not a mixin:
|
|
34
|
+
* it reacts to a lifecycle and nothing else.
|
|
42
35
|
*
|
|
43
36
|
* So a group states its members once, at construction, and never thinks about
|
|
44
37
|
* it again:
|
|
@@ -3,24 +3,17 @@ import { inheritDisabled, } from '../../utils/inherit-disabled/inherit-disabled.
|
|
|
3
3
|
* Pushes a group's `disabled` onto its members, on every update, unasked.
|
|
4
4
|
*
|
|
5
5
|
* **The cascade itself is `inheritDisabled`, and this is what makes it
|
|
6
|
-
* automatic.**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* ignore it. That is the failure this exists to make impossible, because it is
|
|
18
|
-
* invisible in review and only shows up when someone disables a set.
|
|
19
|
-
*
|
|
20
|
-
* And it adds no API. A group already has `disabled`, from `HelixFormElement`
|
|
21
|
-
* or from the consumer; nothing here introduces a property or a method for
|
|
22
|
-
* anyone to call. By this repo's own rule that makes it a controller and not a
|
|
23
|
-
* mixin: it reacts to a lifecycle and nothing else.
|
|
6
|
+
* automatic.** Called by hand it runs only when whichever method holds the call
|
|
7
|
+
* runs, so a group whose items changed by some other path leaves them holding
|
|
8
|
+
* the last answer, and a group that forgets the call gets no cascade at all
|
|
9
|
+
* with nothing to say so: the property exists, the attribute reflects, and the
|
|
10
|
+
* members simply ignore it. That is invisible in review and shows up only when
|
|
11
|
+
* someone disables a set.
|
|
12
|
+
*
|
|
13
|
+
* It adds no API. A group already has `disabled`, from `HelixFormElement` or
|
|
14
|
+
* from the consumer; nothing here introduces a property or a method for anyone
|
|
15
|
+
* to call. By this repo's own rule that makes it a controller and not a mixin:
|
|
16
|
+
* it reacts to a lifecycle and nothing else.
|
|
24
17
|
*
|
|
25
18
|
* So a group states its members once, at construction, and never thinks about
|
|
26
19
|
* it again:
|
|
@@ -18,7 +18,7 @@ export interface DismissOptions {
|
|
|
18
18
|
*
|
|
19
19
|
* Both, because they are different intentions with the same meaning: Escape is
|
|
20
20
|
* the keyboard's way out and clicking away is the pointer's, and an overlay
|
|
21
|
-
* that
|
|
21
|
+
* that honors only one is one half its readers cannot dismiss.
|
|
22
22
|
*
|
|
23
23
|
* The outside press is watched on `pointerdown` rather than `click`, so a press
|
|
24
24
|
* that starts outside and drags across the overlay still counts as leaving
|
|
@@ -26,11 +26,24 @@ export interface DismissOptions {
|
|
|
26
26
|
*
|
|
27
27
|
* A press inside a frame is the third case, and it needs its own signal. The
|
|
28
28
|
* frame swallows the event, so the document that owns the overlay never sees a
|
|
29
|
-
* `pointerdown` at all
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* `pointerdown` at all.
|
|
30
|
+
*
|
|
31
|
+
* Where the frame is same-origin there is a document to listen to, and this
|
|
32
|
+
* listens to it: the same handler, on every reachable frame document, so a
|
|
33
|
+
* press in there arrives as a press whose path holds nothing of the overlay,
|
|
34
|
+
* which is what it is.
|
|
35
|
+
*
|
|
36
|
+
* **The focus signal alone was not enough, and a browser difference is why.**
|
|
37
|
+
* A press in a frame moves focus into it in Chromium, so the window blurs and a
|
|
38
|
+
* frame is what took focus. WebKit does neither: focus stays on the menu item
|
|
39
|
+
* the reader pressed to open the overlay, the window never blurs, and every
|
|
40
|
+
* observable thing about that tap says nothing happened. Measured on the
|
|
41
|
+
* sandbox, whose preview is a frame filling half the tool, the settings menu
|
|
42
|
+
* could not be dismissed by pressing the component it configures.
|
|
43
|
+
*
|
|
44
|
+
* Both signals are kept, because they answer different frames. The listener
|
|
45
|
+
* only reaches same-origin ones; the blur only fires where the browser moves
|
|
46
|
+
* focus. A cross-origin frame in Chromium has the second and not the first.
|
|
34
47
|
*/
|
|
35
48
|
export declare class DismissController implements ReactiveController {
|
|
36
49
|
private readonly host;
|
|
@@ -50,6 +63,31 @@ export declare class DismissController implements ReactiveController {
|
|
|
50
63
|
*/
|
|
51
64
|
private readonly handleWindowBlur;
|
|
52
65
|
private readonly handlePointerDown;
|
|
66
|
+
/**
|
|
67
|
+
* Every frame document on the page this one is allowed to read.
|
|
68
|
+
*
|
|
69
|
+
* Through the shadow roots as well, because a component that renders a frame
|
|
70
|
+
* renders it inside itself: the sandbox's preview is one, and
|
|
71
|
+
* `document.querySelectorAll` cannot see it.
|
|
72
|
+
*
|
|
73
|
+
* A cross-origin frame throws on `contentDocument` rather than returning
|
|
74
|
+
* null in some browsers, so the read is guarded and the frame is simply
|
|
75
|
+
* skipped. Nothing is lost by skipping it that was not already lost: there is
|
|
76
|
+
* no listener to add to a document this one may not touch.
|
|
77
|
+
*/
|
|
78
|
+
private static frameDocuments;
|
|
79
|
+
/**
|
|
80
|
+
* The frame documents being listened to, held so the same ones are released.
|
|
81
|
+
*
|
|
82
|
+
* Gathered when the overlay opens rather than watched for: an overlay is open
|
|
83
|
+
* for as long as a reader is deciding something, and a frame that arrives
|
|
84
|
+
* inside that window is a page rebuilding itself under a menu, which is not a
|
|
85
|
+
* case worth a `MutationObserver` on every open surface. Held rather than
|
|
86
|
+
* re-collected on the way out, because a frame removed while the overlay was
|
|
87
|
+
* open would otherwise be missed and one navigated elsewhere would be a
|
|
88
|
+
* different document.
|
|
89
|
+
*/
|
|
90
|
+
private frames;
|
|
53
91
|
/** Starts listening. */
|
|
54
92
|
activate(): void;
|
|
55
93
|
/** Stops listening. */
|
|
@@ -41,7 +41,7 @@ const listening = new Set();
|
|
|
41
41
|
*
|
|
42
42
|
* Both, because they are different intentions with the same meaning: Escape is
|
|
43
43
|
* the keyboard's way out and clicking away is the pointer's, and an overlay
|
|
44
|
-
* that
|
|
44
|
+
* that honors only one is one half its readers cannot dismiss.
|
|
45
45
|
*
|
|
46
46
|
* The outside press is watched on `pointerdown` rather than `click`, so a press
|
|
47
47
|
* that starts outside and drags across the overlay still counts as leaving
|
|
@@ -49,11 +49,24 @@ const listening = new Set();
|
|
|
49
49
|
*
|
|
50
50
|
* A press inside a frame is the third case, and it needs its own signal. The
|
|
51
51
|
* frame swallows the event, so the document that owns the overlay never sees a
|
|
52
|
-
* `pointerdown` at all
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* `pointerdown` at all.
|
|
53
|
+
*
|
|
54
|
+
* Where the frame is same-origin there is a document to listen to, and this
|
|
55
|
+
* listens to it: the same handler, on every reachable frame document, so a
|
|
56
|
+
* press in there arrives as a press whose path holds nothing of the overlay,
|
|
57
|
+
* which is what it is.
|
|
58
|
+
*
|
|
59
|
+
* **The focus signal alone was not enough, and a browser difference is why.**
|
|
60
|
+
* A press in a frame moves focus into it in Chromium, so the window blurs and a
|
|
61
|
+
* frame is what took focus. WebKit does neither: focus stays on the menu item
|
|
62
|
+
* the reader pressed to open the overlay, the window never blurs, and every
|
|
63
|
+
* observable thing about that tap says nothing happened. Measured on the
|
|
64
|
+
* sandbox, whose preview is a frame filling half the tool, the settings menu
|
|
65
|
+
* could not be dismissed by pressing the component it configures.
|
|
66
|
+
*
|
|
67
|
+
* Both signals are kept, because they answer different frames. The listener
|
|
68
|
+
* only reaches same-origin ones; the blur only fires where the browser moves
|
|
69
|
+
* focus. A cross-origin frame in Chromium has the second and not the first.
|
|
57
70
|
*/
|
|
58
71
|
export class DismissController {
|
|
59
72
|
constructor(host, { boundary, onDismiss }) {
|
|
@@ -69,10 +82,10 @@ export class DismissController {
|
|
|
69
82
|
* same listener on `document`, so they are siblings on one node where the
|
|
70
83
|
* only ordering is registration, and propagation is about ancestors.
|
|
71
84
|
*
|
|
72
|
-
* Carrying the winning depth on the event does not arrange it either
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
85
|
+
* Carrying the winning depth on the event does not arrange it either. A
|
|
86
|
+
* handler that runs before any deeper one has no way to know a deeper one
|
|
87
|
+
* exists, so it dismisses and only then loses the comparison. Registration
|
|
88
|
+
* order is the order the overlays
|
|
76
89
|
* *opened*, and opening the outer one first is the ordinary case: a dialog,
|
|
77
90
|
* then a select inside it. Measured, that closed both.
|
|
78
91
|
*
|
|
@@ -132,11 +145,58 @@ export class DismissController {
|
|
|
132
145
|
this.onDismiss();
|
|
133
146
|
}
|
|
134
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* The frame documents being listened to, held so the same ones are released.
|
|
150
|
+
*
|
|
151
|
+
* Gathered when the overlay opens rather than watched for: an overlay is open
|
|
152
|
+
* for as long as a reader is deciding something, and a frame that arrives
|
|
153
|
+
* inside that window is a page rebuilding itself under a menu, which is not a
|
|
154
|
+
* case worth a `MutationObserver` on every open surface. Held rather than
|
|
155
|
+
* re-collected on the way out, because a frame removed while the overlay was
|
|
156
|
+
* open would otherwise be missed and one navigated elsewhere would be a
|
|
157
|
+
* different document.
|
|
158
|
+
*/
|
|
159
|
+
this.frames = [];
|
|
135
160
|
this.host = host;
|
|
136
161
|
this.boundary = boundary;
|
|
137
162
|
this.onDismiss = onDismiss;
|
|
138
163
|
host.addController(this);
|
|
139
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Every frame document on the page this one is allowed to read.
|
|
167
|
+
*
|
|
168
|
+
* Through the shadow roots as well, because a component that renders a frame
|
|
169
|
+
* renders it inside itself: the sandbox's preview is one, and
|
|
170
|
+
* `document.querySelectorAll` cannot see it.
|
|
171
|
+
*
|
|
172
|
+
* A cross-origin frame throws on `contentDocument` rather than returning
|
|
173
|
+
* null in some browsers, so the read is guarded and the frame is simply
|
|
174
|
+
* skipped. Nothing is lost by skipping it that was not already lost: there is
|
|
175
|
+
* no listener to add to a document this one may not touch.
|
|
176
|
+
*/
|
|
177
|
+
static frameDocuments() {
|
|
178
|
+
const found = [];
|
|
179
|
+
const walk = (root) => {
|
|
180
|
+
for (const element of root.querySelectorAll('*')) {
|
|
181
|
+
if (element instanceof HTMLIFrameElement) {
|
|
182
|
+
try {
|
|
183
|
+
const inner = element.contentDocument;
|
|
184
|
+
if (inner) {
|
|
185
|
+
found.push(inner);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Cross-origin. The blur signal is what covers those.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (element.shadowRoot) {
|
|
193
|
+
walk(element.shadowRoot);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
walk(document);
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
140
200
|
/** Starts listening. */
|
|
141
201
|
activate() {
|
|
142
202
|
if (this.active) {
|
|
@@ -147,6 +207,10 @@ export class DismissController {
|
|
|
147
207
|
document.addEventListener('keydown', this.handleKeydown);
|
|
148
208
|
document.addEventListener('pointerdown', this.handlePointerDown);
|
|
149
209
|
window.addEventListener('blur', this.handleWindowBlur);
|
|
210
|
+
this.frames = DismissController.frameDocuments();
|
|
211
|
+
for (const inner of this.frames) {
|
|
212
|
+
inner.addEventListener('pointerdown', this.handlePointerDown);
|
|
213
|
+
}
|
|
150
214
|
}
|
|
151
215
|
/** Stops listening. */
|
|
152
216
|
deactivate() {
|
|
@@ -158,6 +222,9 @@ export class DismissController {
|
|
|
158
222
|
document.removeEventListener('keydown', this.handleKeydown);
|
|
159
223
|
document.removeEventListener('pointerdown', this.handlePointerDown);
|
|
160
224
|
window.removeEventListener('blur', this.handleWindowBlur);
|
|
225
|
+
for (const inner of this.frames.splice(0)) {
|
|
226
|
+
inner.removeEventListener('pointerdown', this.handlePointerDown);
|
|
227
|
+
}
|
|
161
228
|
}
|
|
162
229
|
hostDisconnected() {
|
|
163
230
|
this.deactivate();
|
|
@@ -28,12 +28,9 @@ export interface DrilldownOptions {
|
|
|
28
28
|
* with. What a reader gets instead is one panel, a title saying where they are,
|
|
29
29
|
* and a way back.
|
|
30
30
|
*
|
|
31
|
-
* **
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* for as long as it has existed. None of that is about menus. Taking it out is
|
|
35
|
-
* what lets a settings panel, a filter stack or a navigation tree have the same
|
|
36
|
-
* behavior without one of them being a menu that is pretending.
|
|
31
|
+
* **None of this is about menus.** A settings panel, a filter stack or a
|
|
32
|
+
* navigation tree gets the same behavior without being a menu that is
|
|
33
|
+
* pretending.
|
|
37
34
|
*
|
|
38
35
|
* **The tree is the state.** A row's page is its own children, assigned to a
|
|
39
36
|
* slot, so the controller holds one reference, the row currently drilled into,
|
|
@@ -7,12 +7,9 @@
|
|
|
7
7
|
* with. What a reader gets instead is one panel, a title saying where they are,
|
|
8
8
|
* and a way back.
|
|
9
9
|
*
|
|
10
|
-
* **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* for as long as it has existed. None of that is about menus. Taking it out is
|
|
14
|
-
* what lets a settings panel, a filter stack or a navigation tree have the same
|
|
15
|
-
* behavior without one of them being a menu that is pretending.
|
|
10
|
+
* **None of this is about menus.** A settings panel, a filter stack or a
|
|
11
|
+
* navigation tree gets the same behavior without being a menu that is
|
|
12
|
+
* pretending.
|
|
16
13
|
*
|
|
17
14
|
* **The tree is the state.** A row's page is its own children, assigned to a
|
|
18
15
|
* slot, so the controller holds one reference, the row currently drilled into,
|
|
@@ -83,9 +83,8 @@ export interface FloatingOptions {
|
|
|
83
83
|
* **The custom properties are declared here because this is what writes them.**
|
|
84
84
|
* Each carries a measurement taken against the viewport that no stylesheet can
|
|
85
85
|
* take for itself, and `core` ships no CSS, so there is no reader inside this
|
|
86
|
-
* package and none of them is private. `
|
|
87
|
-
* with a rule that reads one, in both directions
|
|
88
|
-
* to carry was what exempted them from it.
|
|
86
|
+
* package and none of them is private. the `hooks` rule pairs a documented
|
|
87
|
+
* hook with a rule that reads one, in both directions.
|
|
89
88
|
*
|
|
90
89
|
* @cssproperty --floating-anchor-inline-size - How wide the anchor is, for a panel that matches it.
|
|
91
90
|
* @cssproperty --floating-arrow-x - Where the pointer meets the anchor, along the inline axis.
|
|
@@ -99,11 +98,11 @@ export declare class FloatingController implements ReactiveController {
|
|
|
99
98
|
* Which run of placements the results coming back belong to.
|
|
100
99
|
*
|
|
101
100
|
* `computePosition` is async, so a measurement started before `stop()` still
|
|
102
|
-
* resolves after it and
|
|
103
|
-
* taken down: the inline `position` and `translate`
|
|
104
|
-
* `data-placed`, which is the attribute five families gate visibility
|
|
105
|
-
* Opening and closing inside one task is all it takes, since the write
|
|
106
|
-
* a microtask later.
|
|
101
|
+
* resolves after it and would write its result onto a surface that had just
|
|
102
|
+
* been taken down: the inline `position` and `translate` back on, and with
|
|
103
|
+
* them `data-placed`, which is the attribute five families gate visibility
|
|
104
|
+
* on. Opening and closing inside one task is all it takes, since the write
|
|
105
|
+
* lands a microtask later.
|
|
107
106
|
*
|
|
108
107
|
* A counter rather than an `AbortController`, because there is nothing to
|
|
109
108
|
* abort: the work is already done by the time the answer arrives and the only
|