@fusion.dev/helix-core 0.0.0-dev → 0.0.2

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 (89) hide show
  1. package/custom-elements.json +156 -124
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +2 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +44 -17
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +5 -6
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -6
  6. package/dist/controllers/command-controller/command-controller.d.ts +5 -7
  7. package/dist/controllers/command-controller/command-controller.js +5 -7
  8. package/dist/controllers/copy-controller/copy-controller.d.ts +2 -4
  9. package/dist/controllers/copy-controller/copy-controller.js +7 -9
  10. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +10 -17
  11. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +11 -18
  12. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +44 -6
  13. package/dist/controllers/dismiss-controller/dismiss-controller.js +77 -10
  14. package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +3 -6
  15. package/dist/controllers/drilldown-controller/drilldown-controller.js +3 -6
  16. package/dist/controllers/floating-controller/floating-controller.d.ts +7 -8
  17. package/dist/controllers/floating-controller/floating-controller.js +69 -38
  18. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +7 -10
  19. package/dist/controllers/indicator-controller/indicator-controller.d.ts +17 -29
  20. package/dist/controllers/indicator-controller/indicator-controller.js +11 -21
  21. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +3 -13
  22. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +11 -24
  23. package/dist/controllers/popover-controller/popover-controller.d.ts +2 -7
  24. package/dist/controllers/popover-controller/popover-controller.js +3 -9
  25. package/dist/controllers/source-controller/source-controller.d.ts +2 -5
  26. package/dist/controllers/source-controller/source-controller.js +2 -5
  27. package/dist/controllers/token-color-controller/token-color-controller.d.ts +2 -4
  28. package/dist/controllers/token-color-controller/token-color-controller.js +2 -4
  29. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +1 -1
  30. package/dist/controllers/virtualize-controller/virtualize-controller.js +15 -15
  31. package/dist/events/element-events.js +0 -5
  32. package/dist/events/events.d.ts +55 -93
  33. package/dist/events/events.js +68 -105
  34. package/dist/helix-element/helix-element.d.ts +26 -27
  35. package/dist/helix-element/helix-element.js +83 -100
  36. package/dist/helix-form-element/helix-form-element.d.ts +11 -17
  37. package/dist/helix-form-element/helix-form-element.js +15 -21
  38. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +6 -14
  39. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +12 -25
  40. package/dist/mixins/mixin-collection/mixin-collection.d.ts +1 -7
  41. package/dist/mixins/mixin-collection/mixin-collection.js +16 -23
  42. package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +15 -16
  43. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +28 -42
  44. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +15 -17
  45. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +33 -45
  46. package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +4 -4
  47. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +29 -45
  48. package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +10 -18
  49. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +14 -31
  50. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +10 -15
  51. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +10 -15
  52. package/dist/mixins/mixin-field/mixin-field.d.ts +10 -12
  53. package/dist/mixins/mixin-field/mixin-field.js +48 -64
  54. package/dist/utils/capture-pointer/capture-pointer.d.ts +1 -4
  55. package/dist/utils/capture-pointer/capture-pointer.js +1 -4
  56. package/dist/utils/closest-instance/closest-instance.d.ts +6 -10
  57. package/dist/utils/closest-instance/closest-instance.js +6 -10
  58. package/dist/utils/create-sheet/create-sheet.d.ts +1 -1
  59. package/dist/utils/create-sheet/create-sheet.js +1 -1
  60. package/dist/utils/custom-element/custom-element.d.ts +0 -21
  61. package/dist/utils/custom-element/custom-element.js +1 -1
  62. package/dist/utils/draw-icon/draw-icon.d.ts +3 -3
  63. package/dist/utils/draw-icon/draw-icon.js +3 -3
  64. package/dist/utils/elapsed-time/elapsed-time.js +1 -3
  65. package/dist/utils/focusable-elements/focusable-elements.d.ts +3 -4
  66. package/dist/utils/focusable-elements/focusable-elements.js +9 -11
  67. package/dist/utils/format-numbers/format-numbers.js +1 -4
  68. package/dist/utils/host-aria/host-aria.d.ts +2 -2
  69. package/dist/utils/host-aria/host-aria.js +2 -2
  70. package/dist/utils/line-path/line-path.js +1 -1
  71. package/dist/utils/on-unless-false/on-unless-false.d.ts +0 -5
  72. package/dist/utils/on-unless-false/on-unless-false.js +0 -5
  73. package/dist/utils/read-series/read-series.d.ts +26 -0
  74. package/dist/utils/read-series/read-series.js +26 -29
  75. package/dist/utils/relative-time/relative-time.js +5 -6
  76. package/dist/utils/request-change/request-change.d.ts +9 -20
  77. package/dist/utils/resolve-active-item/resolve-active-item.d.ts +3 -3
  78. package/dist/utils/resolve-active-item/resolve-active-item.js +3 -3
  79. package/dist/utils/restore-value/restore-value.js +1 -6
  80. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +4 -5
  81. package/dist/utils/slot-filled/slot-filled.d.ts +2 -4
  82. package/dist/utils/slot-filled/slot-filled.js +2 -4
  83. package/dist/utils/tell-disclosure/tell-disclosure.d.ts +15 -18
  84. package/dist/utils/tell-disclosure/tell-disclosure.js +15 -18
  85. package/dist/utils/tone-marks/tone-marks.d.ts +10 -16
  86. package/dist/utils/tone-marks/tone-marks.js +6 -10
  87. package/dist/utils/value-as/value-as.d.ts +0 -28
  88. package/dist/utils/value-as/value-as.js +5 -10
  89. 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
- * **The case that asked for it was a disclosure with a slotted trigger.**
16
- * `Megamenu` and `SpeedDial` write `aria-expanded` onto whatever a consumer
17
- * slots, and where that is a `<helix-button>` the attribute landed on a host the
18
- * tree reports as `generic`: measured through Chrome's own tree, a button named
19
- * "Mega" with no expanded state at all, on a component whose comment says the
20
- * write exists so a screen reader is not "told the control was a plain button
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, and getting that
25
- * wrong broke three components.** The first version removed any of these from
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 both were wiped on
30
- * every update by a controller carrying nothing. Their own specs caught it.
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
- target[property] =
113
- elements.length > 0 ? elements : null;
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, which `Button` does whenever `href` changes it
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
- * This was `tracks = true`, and every reading was resolved from the pointer's
46
- * x whatever the chart was doing. A horizontal bar chart runs its categories
47
- * *down* the box, so moving the pointer from the first row to the last never
48
- * changed the answer: measured on four rows, all four reported the third,
49
- * which is the slot the middle of the box falls in. It looked like a tooltip
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
- * This was `tracks = true`, and every reading was resolved from the pointer's
38
- * x whatever the chart was doing. A horizontal bar chart runs its categories
39
- * *down* the box, so moving the pointer from the first row to the last never
40
- * changed the answer: measured on four rows, all four reported the third,
41
- * which is the slot the middle of the box falls in. It looked like a tooltip
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
- * The alternative was a `data-` recipe of our own with a delegated listener on
60
- * the document, and it is worse in every direction: a global listener a
61
- * consumer cannot see, an id lookup we would write ourselves, activation
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` — are defined against native elements
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
- * The alternative was a `data-` recipe of our own with a delegated listener on
57
- * the document, and it is worse in every direction: a global listener a
58
- * consumer cannot see, an id lookup we would write ourselves, activation
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` — are defined against native elements
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: an insecure origin, an unfocused document, and a
25
- * Permissions Policy defaulting to `self`, which a sandboxed iframe is not. The
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: an insecure origin, an unfocused document, and a
61
- * Permissions Policy defaulting to `self`, which a sandboxed iframe is not. The
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: everything below this line
102
- used to run afterwards regardless, starting a fresh timer on a detached
103
- host and calling `requestUpdate` on it a second later. Checked here
104
- rather than in `reset`, because the thing to skip is the whole
105
- confirmation and not only its clock. */
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.** Four groups called that function by hand from inside whichever
25
- * method they happened to use for writing to their items: `place` in two of
26
- * them, `sync` in a third, `place` again in the fourth. That works, and it is
27
- * the wrong shape for three reasons.
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 runs when that method runs. A group whose items changed without going
30
- * through it left them holding the last answer, and which paths reach it is a
31
- * different set in each of the four.
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.** Four groups called that function by hand from inside whichever
7
- * method they happened to use for writing to their items: `place` in two of
8
- * them, `sync` in a third, `place` again in the fourth. That works, and it is
9
- * the wrong shape for three reasons.
10
- *
11
- * It runs when that method runs. A group whose items changed without going
12
- * through it left them holding the last answer, and which paths reach it is a
13
- * different set in each of the four.
14
- *
15
- * It has to be remembered. A fifth group gets no cascade at all, and nothing
16
- * says so: the property exists, the attribute reflects, and the members simply
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 honours only one is one half its readers cannot dismiss.
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, and where the frame is cross-origin there is nothing to
30
- * listen to inside it either. What is observable is that the window lost focus
31
- * and a frame on this page gained it, which is exactly a press the document
32
- * cannot see. Leaving the browser altogether blurs the window too, so what took
33
- * focus is the part that distinguishes them.
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 honours only one is one half its readers cannot dismiss.
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, and where the frame is cross-origin there is nothing to
53
- * listen to inside it either. What is observable is that the window lost focus
54
- * and a frame on this page gained it, which is exactly a press the document
55
- * cannot see. Leaving the browser altogether blurs the window too, so what took
56
- * focus is the part that distinguishes them.
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, which
73
- * is the version this replaced. A handler that runs before any deeper one
74
- * has no way to know a deeper one exists, so it dismisses and only then
75
- * loses the comparison. Registration order is the order the overlays
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
- * **Extracted from `Menu`, which had all of this and did not say so.** The menu
32
- * held a `page`, derived the rows on it, read a title off the row that opened
33
- * it, and reset on close; the sandbox's settings dropdown has been a drill-down
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
- * **Extracted from `Menu`, which had all of this and did not say so.** The menu
11
- * held a `page`, derived the rows on it, read a title off the row that opened
12
- * it, and reset on close; the sandbox's settings dropdown has been a drill-down
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. `check:hooks` pairs a `@cssproperty`
87
- * with a rule that reads one, in both directions, and the underscore these used
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 wrote its result onto a surface that had just been
103
- * taken down: the inline `position` and `translate` went back on, and so did
104
- * `data-placed`, which is the attribute five families gate visibility on.
105
- * Opening and closing inside one task is all it takes, since the write lands
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