@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
@@ -12,9 +12,7 @@ import { property, state } from 'lit/decorators.js';
12
12
  * **Empty and loading are different answers and a reader can tell them apart.**
13
13
  * "No results" under a spinner is a lie for as long as the request is in
14
14
  * flight, and a blank box is not an answer at all: a reader cannot know whether
15
- * a list is empty, still arriving, or broken. Six components each rendered rows
16
- * and none of them said either thing, so every consumer wrote their own and
17
- * wrote it differently.
15
+ * a list is empty, still arriving, or broken.
18
16
  *
19
17
  * **`aria-busy` on the collection, not on the message.** A reader with a screen
20
18
  * reader is told the region is changing and then told once, politely, what it
@@ -26,10 +24,6 @@ import { property, state } from 'lit/decorators.js';
26
24
  * which question it is answering, and this is the point in a component's life
27
25
  * when a consumer is most likely to be looking at stale content.
28
26
  *
29
- * Listbox had `empty-text` and nothing else, and Combobox forwarded it. Both
30
- * keep the name, so nothing a consumer wrote has moved.
31
- *
32
- *
33
27
  * **`empty` and `loading` are documented on each of the five families that
34
28
  * apply this, rather than here.** See `MixinField` for why a class-level tag on
35
29
  * a mixin reaches no manifest.
@@ -38,6 +32,10 @@ export const MixinCollection = (Base) => {
38
32
  class Collection extends Base {
39
33
  constructor() {
40
34
  super(...arguments);
35
+ /**
36
+ * What is said where there are none, and none are coming.
37
+ */
38
+ this.emptyText = 'Nothing to show';
41
39
  /**
42
40
  * Whether the rows are still on their way.
43
41
  *
@@ -50,10 +48,6 @@ export const MixinCollection = (Base) => {
50
48
  * What is said while they are on their way.
51
49
  */
52
50
  this.loadingText = 'Loading';
53
- /**
54
- * What is said where there are none, and none are coming.
55
- */
56
- this.emptyText = 'Nothing to show';
57
51
  /**
58
52
  * Whether the rows have been looked at yet.
59
53
  */
@@ -61,14 +55,13 @@ export const MixinCollection = (Base) => {
61
55
  /**
62
56
  * How many there were, the last time anyone looked.
63
57
  *
64
- * **Not `rows`, and the rename is the whole point of this comment.**
65
- * TypeScript's `private` is a compile-time claim and the runtime field is
66
- * whatever it is named, so a subclass declaring `@property() rows` for the
67
- * data a consumer hands it would share one field with this counter: the
68
- * slot count would overwrite the consumer's array on every `updated`, and
69
- * `hasRows` would read the array's length as a slot count. Nothing would
70
- * throw. `helix-data-table` takes exactly that property, which is what
71
- * found this before it happened.
58
+ * **Never name this `rows`.** TypeScript's `private` is a compile-time
59
+ * claim and the runtime field is whatever it is named, so a subclass
60
+ * declaring `@property() rows` for the data a consumer hands it would share
61
+ * one field with this counter: the slot count would overwrite the
62
+ * consumer's array on every `updated`, and `hasRows` would read the array's
63
+ * length as a slot count. Nothing would throw. `helix-data-table` takes
64
+ * exactly that property.
72
65
  */
73
66
  this.slotRows = 0;
74
67
  }
@@ -79,7 +72,7 @@ export const MixinCollection = (Base) => {
79
72
  * count. `slotchange` never fires there, so `counted` is false exactly when
80
73
  * the answer is unknown, and an unknown answer is not "empty": a list that
81
74
  * shipped "Nothing to show" over markup about to have rows in it is the one
82
- * thing an empty state must never do. Listbox's spec caught precisely that.
75
+ * thing an empty state must never do.
83
76
  */
84
77
  countRows(event) {
85
78
  this.slotCounted = true;
@@ -135,15 +128,15 @@ export const MixinCollection = (Base) => {
135
128
  return html `<p part="empty">${this.emptyText}</p>`;
136
129
  }
137
130
  }
131
+ __decorate([
132
+ property({ attribute: 'empty-text' })
133
+ ], Collection.prototype, "emptyText", void 0);
138
134
  __decorate([
139
135
  property({ reflect: true, type: Boolean })
140
136
  ], Collection.prototype, "loading", void 0);
141
137
  __decorate([
142
138
  property({ attribute: 'loading-text' })
143
139
  ], Collection.prototype, "loadingText", void 0);
144
- __decorate([
145
- property({ attribute: 'empty-text' })
146
- ], Collection.prototype, "emptyText", void 0);
147
140
  __decorate([
148
141
  state()
149
142
  ], Collection.prototype, "slotCounted", void 0);
@@ -11,8 +11,9 @@ export declare class MixinControllerActiveInterface {
11
11
  /**
12
12
  * Which item is current, by its `value`.
13
13
  *
14
- * Reading gives the real one, in either mode. Writing hands ownership to the
15
- * consumer: from then on the set announces changes and applies none of them.
14
+ * Reading gives the real one. Writing always applies, and is a seed and a
15
+ * correction rather than a claim of ownership: a consumer who wants the
16
+ * decision refuses the `helix-before-activate`.
16
17
  */
17
18
  active: string;
18
19
  /** Initial item for uncontrolled use. Ignored once `active` is written. */
@@ -22,8 +23,8 @@ export declare class MixinControllerActiveInterface {
22
23
  /**
23
24
  * Which item is current, given what there is to choose from.
24
25
  *
25
- * The first enabled one when `value` names nothing, which is a set before its
26
- * consumer has chosen and a set whose current item has just been removed.
26
+ * The first enabled one when `active` names nothing, which is a set before
27
+ * its consumer has chosen and a set whose current item has just been removed.
27
28
  */
28
29
  protected resolveActive<Item extends ActivatableItem>(items: Item[]): Item | undefined;
29
30
  }
@@ -35,19 +36,17 @@ export declare class MixinControllerActiveInterface {
35
36
  * is submitted and belongs to `MixinControllerValue`, which is why these are
36
37
  * two mixins and not one with a flag.
37
38
  *
38
- * **Announcing before applying is the part worth sharing.** `Tabs` and `Tree`
39
- * both wrote the current value straight from the item's event, so a consumer
40
- * had no way to refuse a change, while `Radio.Group` announced first. Neither
41
- * was wrong on its own and the two together are: whether a selection can be
42
- * refused should not depend on which component a reader is looking at.
39
+ * **Announcing before applying is the part worth sharing.** Writing the current
40
+ * value straight from the item's event leaves a consumer no way to refuse a
41
+ * change, and whether a selection can be refused must not depend on which
42
+ * component a reader is looking at.
43
43
  *
44
- * **The fallback is the other half.** Four components had written
45
- * `items.find(match) ?? items[0]`, and it is not the obvious line it looks
46
- * like: it is what makes a set with a `value` naming nothing still show
47
- * something, which is every set before its consumer has set one and every set
48
- * whose current item has just been removed. `resolveActive` also skips disabled
49
- * items, which the hand-written copies did not, so a set opening on a disabled
50
- * first item showed it as current.
44
+ * **The fallback is the other half.** `items.find(match) ?? items[0]` is not
45
+ * the obvious line it looks like: it is what makes a set whose `active` names
46
+ * nothing still show something, which is every set before its consumer has set
47
+ * one and every set whose current item has just been removed. `resolveActive`
48
+ * also skips disabled items in that fallback, or a set opening on a disabled
49
+ * first item shows it as current.
51
50
  */
52
51
  export declare const MixinControllerActive: <T extends Constructor<HelixElement>>(Base: T) => Constructor<MixinControllerActiveInterface> & T;
53
52
  export {};
@@ -16,19 +16,17 @@ import { resolveActiveItem, } from '../../utils/resolve-active-item/resolve-acti
16
16
  * is submitted and belongs to `MixinControllerValue`, which is why these are
17
17
  * two mixins and not one with a flag.
18
18
  *
19
- * **Announcing before applying is the part worth sharing.** `Tabs` and `Tree`
20
- * both wrote the current value straight from the item's event, so a consumer
21
- * had no way to refuse a change, while `Radio.Group` announced first. Neither
22
- * was wrong on its own and the two together are: whether a selection can be
23
- * refused should not depend on which component a reader is looking at.
19
+ * **Announcing before applying is the part worth sharing.** Writing the current
20
+ * value straight from the item's event leaves a consumer no way to refuse a
21
+ * change, and whether a selection can be refused must not depend on which
22
+ * component a reader is looking at.
24
23
  *
25
- * **The fallback is the other half.** Four components had written
26
- * `items.find(match) ?? items[0]`, and it is not the obvious line it looks
27
- * like: it is what makes a set with a `value` naming nothing still show
28
- * something, which is every set before its consumer has set one and every set
29
- * whose current item has just been removed. `resolveActive` also skips disabled
30
- * items, which the hand-written copies did not, so a set opening on a disabled
31
- * first item showed it as current.
24
+ * **The fallback is the other half.** `items.find(match) ?? items[0]` is not
25
+ * the obvious line it looks like: it is what makes a set whose `active` names
26
+ * nothing still show something, which is every set before its consumer has set
27
+ * one and every set whose current item has just been removed. `resolveActive`
28
+ * also skips disabled items in that fallback, or a set opening on a disabled
29
+ * first item shows it as current.
32
30
  */
33
31
  export const MixinControllerActive = (Base) => {
34
32
  // JSDoc is duplicated from the interface deliberately. The interface is what
@@ -47,27 +45,19 @@ export const MixinControllerActive = (Base) => {
47
45
  * The state, whoever set it, and `undefined` until somebody has.
48
46
  *
49
47
  * One field and not two. Writing always applies, the way a native control
50
- * does when a script assigns to it. What the write does not do is change
51
- * which mode the element is in.
48
+ * does when a script assigns to it.
52
49
  */
53
50
  this.activeState = undefined;
54
51
  }
55
52
  /**
56
53
  * Which item is current, by its `value`.
57
54
  *
58
- * Called `active`, which is what this mixin is named for and what it was
59
- * not: it declared `value`, so the one mixin whose whole subject is which
60
- * item is active had no such property, and `active` meant something else
61
- * entirely elsewhere in the library. It still does, and only there now:
62
- * `option.active` is the option the keyboard is on, which is a different
63
- * question from which one is chosen.
64
- *
65
- * An accessor pair, the same shape as the other three. Reading gives the
66
- * real item; writing hands ownership to the consumer, so `<helix-tabs
67
- * active="one">` is a set that stays on that tab and `default-active` is
68
- * how markup says where to start.
69
- *
55
+ * Not `option.active`, which is the option the keyboard is on: that is a
56
+ * different question from which one is chosen.
70
57
  *
58
+ * Writing always applies, so `<helix-tabs active="one">` starts on that
59
+ * tab, and `default-active` is how markup says where to start without
60
+ * naming the current one.
71
61
  */
72
62
  get active() {
73
63
  return this.activeState ?? this.defaultActive;
@@ -85,7 +75,7 @@ export const MixinControllerActive = (Base) => {
85
75
  * by the time this runs, so refusing is simply not writing: there is
86
76
  * nothing to put back. Nobody refusing means the set updates itself,
87
77
  * whether or not a consumer declared `active`, a declared property being a
88
- * seed and a correction. See 294.
78
+ * seed and a correction.
89
79
  */
90
80
  requestActive(next) {
91
81
  if (next === this.active) {
@@ -109,25 +99,21 @@ export const MixinControllerActive = (Base) => {
109
99
  /**
110
100
  * Says the current child moved, once it has.
111
101
  *
112
- * **This was the only one of the four mixins with no notification**, and the
113
- * event class said so deliberately: no native word means "a set chose a
114
- * different member", so borrowing `change` would be a lie about what moved.
115
- * That half still holds, and is why the pair is `helix-activate`. What did
116
- * not follow is that a set should therefore announce nothing.
102
+ * `helix-activate` rather than a native word: nothing on the platform means
103
+ * "a set chose a different member", and borrowing `change` would be a lie
104
+ * about what moved.
117
105
  *
118
- * The cost is the one 294 fixed for values and left here. A consumer who
119
- * only wants to know which child is current had one channel and it fires
120
- * *before* `apply`, so `currentTarget.active` is the previous value inside
121
- * the handler and `detail.value` is a proposal another listener may refuse.
122
- * Measured across the four families taking this: `Tabs` and `Tree` looked
123
- * covered because `helix-select` bubbles up from a child, which fires when an
124
- * item is *chosen* and not when `active` is assigned, and `Steps` and
125
- * `Video Playlist` announced nothing at all.
106
+ * A consumer who only wants to know which child is current cannot use
107
+ * `helix-before-activate` for it, that one firing *before* `apply`, so
108
+ * `currentTarget.active` is the previous value inside the handler and
109
+ * `detail.value` is a proposal another listener may refuse. `helix-select`
110
+ * is no substitute either: it fires when an item is *chosen* and not when
111
+ * `active` is assigned.
126
112
  *
127
113
  * **After `apply`, which is the whole reason it is a second event**, so the
128
114
  * state is on the element by the time a handler reads it. Not cancelable:
129
115
  * the decision was taken by the event before this one, and two refusable
130
- * events for one transition are two answers to one question. See #325.
116
+ * events for one transition are two answers to one question.
131
117
  */
132
118
  announceActivated(value) {
133
119
  this.dispatchEvent(new HelixActivateEvent({ value }));
@@ -135,7 +121,7 @@ export const MixinControllerActive = (Base) => {
135
121
  /**
136
122
  * Which item is current, given what there is to choose from.
137
123
  *
138
- * The first enabled one when `value` names nothing, which is a set before
124
+ * The first enabled one when `active` names nothing, which is a set before
139
125
  * its consumer has chosen and a set whose current item has just been
140
126
  * removed. Disabled items are skipped in the fallback but not in the match:
141
127
  * a consumer who names a disabled item meant it, and quietly moving to
@@ -11,9 +11,9 @@ export declare class MixinControllerCheckedInterface {
11
11
  /**
12
12
  * Whether the control is checked.
13
13
  *
14
- * Reading gives the real state, in either mode. Writing hands ownership to
15
- * the consumer: from then on the control announces changes and applies none
16
- * of them.
14
+ * Reading gives the real state. Writing always applies, and is a seed and a
15
+ * correction rather than a claim of ownership: a consumer who wants the
16
+ * decision refuses the `helix-before-toggle`.
17
17
  */
18
18
  checked: boolean;
19
19
  /** Initial checked state for uncontrolled use. Ignored once `checked` is set. */
@@ -28,27 +28,25 @@ export declare class MixinControllerCheckedInterface {
28
28
  /** Requests the opposite of the current state. */
29
29
  toggleChecked(): boolean;
30
30
  /**
31
- * Drops the uncontrolled state, so the control reads `defaultChecked` again.
31
+ * Drops the recorded state, so the control reads `defaultChecked` again.
32
32
  *
33
- * A form reset has to return a control to what its markup declared. Clearing
34
- * `checked` alone cannot do it: that only leaves controlled mode, and the
35
- * uncontrolled value the element has been keeping is private to the mixin
36
- * and would still be what `checked` reports.
33
+ * A form reset has to return a control to what its markup declared, and
34
+ * assigning `checked` cannot do it: any assignment records a state, and the
35
+ * recorded state is private to the mixin. This is the only way to clear it.
37
36
  */
38
37
  resetChecked(): void;
39
38
  }
40
39
  /**
41
- * Grafts controlled and uncontrolled checked state onto a base class.
40
+ * Grafts checked state onto a base class.
42
41
  *
43
- * Which mode it is in is read once, at the first update, from whether anything
44
- * had set `checked` by then: markup, a hydrating server's attributes and a
45
- * framework's initial properties have all arrived, and nothing else has
46
- * happened yet. Set, and the consumer owns it and the element only announces.
47
- * Unset, and the element owns it and `defaultChecked` says where it starts.
42
+ * `checked` is a seed and a correction rather than a claim: the element applies
43
+ * its own changes from wherever markup started it, `defaultChecked` is what a
44
+ * reset returns to, and a consumer takes the decision by refusing the
45
+ * `helix-before-toggle`.
48
46
  *
49
- * Records `:state(checked)` whenever the element is checked, in either mode.
50
- * Styled tiers select on that rather than on `[checked]`, which is absent for
51
- * uncontrolled consumers.
47
+ * Records `:state(checked)` whenever the element is checked. Styled tiers
48
+ * select on that rather than on `[checked]`, which is absent until somebody
49
+ * writes it.
52
50
  */
53
51
  export declare const MixinControllerChecked: <T extends Constructor<HelixElement>>(Base: T) => Constructor<MixinControllerCheckedInterface> & T;
54
52
  export {};
@@ -8,17 +8,16 @@ import { property } from 'lit/decorators.js';
8
8
  import { HelixBeforeToggleEvent } from '../../events/events.js';
9
9
  import { requestChange } from '../../utils/request-change/request-change.js';
10
10
  /**
11
- * Grafts controlled and uncontrolled checked state onto a base class.
11
+ * Grafts checked state onto a base class.
12
12
  *
13
- * Which mode it is in is read once, at the first update, from whether anything
14
- * had set `checked` by then: markup, a hydrating server's attributes and a
15
- * framework's initial properties have all arrived, and nothing else has
16
- * happened yet. Set, and the consumer owns it and the element only announces.
17
- * Unset, and the element owns it and `defaultChecked` says where it starts.
13
+ * `checked` is a seed and a correction rather than a claim: the element applies
14
+ * its own changes from wherever markup started it, `defaultChecked` is what a
15
+ * reset returns to, and a consumer takes the decision by refusing the
16
+ * `helix-before-toggle`.
18
17
  *
19
- * Records `:state(checked)` whenever the element is checked, in either mode.
20
- * Styled tiers select on that rather than on `[checked]`, which is absent for
21
- * uncontrolled consumers.
18
+ * Records `:state(checked)` whenever the element is checked. Styled tiers
19
+ * select on that rather than on `[checked]`, which is absent until somebody
20
+ * writes it.
22
21
  */
23
22
  export const MixinControllerChecked = (Base) => {
24
23
  // JSDoc is duplicated from MixinControllerCheckedInterface deliberately. The
@@ -35,25 +34,19 @@ export const MixinControllerChecked = (Base) => {
35
34
  * The state, whoever set it, and `undefined` until somebody has.
36
35
  *
37
36
  * One field and not two. Writing always applies, the way a native control
38
- * does when a script assigns to it. What the write does not do is change
39
- * which mode the element is in.
37
+ * does when a script assigns to it.
40
38
  */
41
39
  this.checkedState = undefined;
42
40
  }
43
41
  /**
44
42
  * Whether the control is checked.
45
43
  *
46
- * One property rather than two. Reading gives the real state whichever
47
- * mode the control is in; writing hands ownership to the consumer, which
48
- * is what the attribute in `<helix-checkbox checked>` does as well. The
49
- * same shape all four of these mixins have, for the reason given on
50
- * `MixinControllerOpen.open`.
44
+ * One property rather than two. Writing always applies;
45
+ * `<helix-checkbox checked>` is the same statement in markup.
51
46
  *
52
47
  * Not reflected: the attribute is the consumer's input and stays as they
53
48
  * wrote it, and `:state(checked)` carries the resolved state for
54
49
  * stylesheets.
55
- *
56
- *
57
50
  */
58
51
  get checked() {
59
52
  return this.checkedState ?? this.defaultChecked;
@@ -66,19 +59,15 @@ export const MixinControllerChecked = (Base) => {
66
59
  /**
67
60
  * Records the resolved state as the custom state `checked`.
68
61
  *
69
- * **A state and not `data-checked`, which is what this wrote until 27 Aug
70
- * 2026.** The attribute went on in `willUpdate`, so `<helix-checkbox
71
- * default-checked>` came back from a consumer's server without it and
72
- * gained it the moment the definition landed, which React lists as a
73
- * hydration mismatch and does not patch up. `HelixElement.toggleState` says
74
- * why the host is the wrong place to keep a component's own bookkeeping.
62
+ * **A custom state and never `data-checked`.** An attribute written here
63
+ * means `<helix-checkbox default-checked>` comes back from a consumer's
64
+ * server without it and gains it the moment the definition lands, which
65
+ * React lists as a hydration mismatch and does not patch up.
66
+ * `HelixElement.toggleState` says why the host is the wrong place to keep a
67
+ * component's own bookkeeping.
75
68
  *
76
- * Still in `willUpdate`, which is where the resolved state is first known
77
- * and is what a subclass rendering from it expects. The old note here said
78
- * SSR calls `willUpdate` and not `connectedCallback`, so the attribute
79
- * reached server output: true, and no longer a reason for anything, a
80
- * custom state being a fact about a running element that a server has
81
- * nothing to say about.
69
+ * In `willUpdate`, which is where the resolved state is first known and is
70
+ * what a subclass rendering from it expects.
82
71
  */
83
72
  willUpdate(changed) {
84
73
  super.willUpdate(changed);
@@ -88,10 +77,11 @@ export const MixinControllerChecked = (Base) => {
88
77
  * Requests a checked change, and reports whether it was applied.
89
78
  *
90
79
  * Announces before it acts, so `preventDefault()` on the
91
- * `helix-before-toggle` is a refusal and nothing moves. Nobody refusing means the element updates itself, whether or not a consumer
92
- * declared the property: a declared property is a seed and a correction,
93
- * the way `<input value>` is, and a consumer who wants the change for
94
- * themselves refuses it and assigns their own. See 294.
80
+ * `helix-before-toggle` is a refusal and nothing moves. Nobody refusing
81
+ * means the element updates itself, whether or not a consumer declared the
82
+ * property: a declared property is a seed and a correction, the way
83
+ * `<input value>` is, and a consumer who wants the change for themselves
84
+ * refuses it and assigns their own.
95
85
  */
96
86
  requestChecked(next) {
97
87
  if (this.checked === next) {
@@ -122,24 +112,22 @@ export const MixinControllerChecked = (Base) => {
122
112
  /**
123
113
  * Restores the control when the browser brings a form back.
124
114
  *
125
- * Hand-written on the checkbox and nowhere else, so `Switch`, the mixin's
126
- * only other consumer, had no restore at all: a reader who navigated away
127
- * and came back to a restored form found their switch flipped off. The
128
- * mixin owns `checked`, so the mixin owns putting it back.
115
+ * The mixin owns `checked`, so the mixin owns putting it back: written on
116
+ * one consumer and not the other, a reader who navigates away and comes
117
+ * back to a restored form finds that control flipped off.
129
118
  *
130
- * Through `checked` rather than the private state, so a controlled consumer
131
- * is asked the same way every other change asks.
119
+ * Through the public accessor rather than the private field, so a restore
120
+ * takes the same path a script assignment does.
132
121
  */
133
122
  formStateRestoreCallback(state) {
134
123
  this.checked = state !== null;
135
124
  }
136
125
  /**
137
- * Drops the uncontrolled state, so the control reads `defaultChecked`
138
- * again.
126
+ * Drops the recorded state, so the control reads `defaultChecked` again.
139
127
  *
140
- * A form reset has to return a control to what its markup declared, and
141
- * clearing `checked` alone only leaves controlled mode. The value the
142
- * element has been keeping lives here, so this is where it is dropped.
128
+ * A form reset has to return a control to what its markup declared, and no
129
+ * assignment to `checked` can do that: any assignment records a state. The
130
+ * recorded state lives here, so this is where it is dropped.
143
131
  */
144
132
  resetChecked() {
145
133
  const previous = this.checked;
@@ -13,9 +13,9 @@ export declare class MixinControllerOpenInterface {
13
13
  /**
14
14
  * Whether the element is open.
15
15
  *
16
- * Reading gives the real state, in either mode. Writing hands ownership to
17
- * the consumer: from then on the element announces changes and applies none
18
- * of them, and waits to be told.
16
+ * Reading gives the real state. Writing always applies, and is a seed and a
17
+ * correction rather than a claim of ownership: a consumer who wants the
18
+ * decision refuses the `helix-before-open` or `helix-before-close`.
19
19
  */
20
20
  open: boolean;
21
21
  /**
@@ -36,7 +36,7 @@ export declare class MixinControllerOpenInterface {
36
36
  * `toggle` announces it afterwards, once the state has actually gone, carrying
37
37
  * `newState` and `oldState` the way the platform does on `<details>` and on a
38
38
  * popover. The first is how a consumer takes the decision; the second is what
39
- * they bind when they only want to know. See 294 and 296.
39
+ * they bind when they only want to know.
40
40
  *
41
41
  * `open` is a seed and a correction rather than a claim: the element applies
42
42
  * its own changes from wherever markup started it, and `defaultOpen` is what a
@@ -15,7 +15,7 @@ import { requestChange } from '../../utils/request-change/request-change.js';
15
15
  * `toggle` announces it afterwards, once the state has actually gone, carrying
16
16
  * `newState` and `oldState` the way the platform does on `<details>` and on a
17
17
  * popover. The first is how a consumer takes the decision; the second is what
18
- * they bind when they only want to know. See 294 and 296.
18
+ * they bind when they only want to know.
19
19
  *
20
20
  * `open` is a seed and a correction rather than a claim: the element applies
21
21
  * its own changes from wherever markup started it, and `defaultOpen` is what a
@@ -45,37 +45,24 @@ export const MixinControllerOpen = (Base) => {
45
45
  * silently overwrites the first. Measured, as a select whose `value` came
46
46
  * back holding its `open` boolean.
47
47
  *
48
- * One field and not two. Writing always applies, which is what a native
49
- * control does when a script assigns to it, and is the behavior an
50
- * imperative `element.open = true` should have whatever mode the element is
51
- * in. What the write does *not* do is change which mode that is.
48
+ * One field and not two. Writing always applies, the way a native control
49
+ * does when a script assigns to it.
52
50
  */
53
51
  this.openState = undefined;
54
52
  }
55
53
  /**
56
54
  * Whether the element is open.
57
55
  *
58
- * One property, not two. Reading gives the real state whichever mode the
59
- * element is in, so `dialog.open` answers the question anyone asking it
60
- * meant. Writing hands ownership to the consumer, which is what the
61
- * attribute in `<helix-dialog open>` does as well: from then on the element
62
- * announces changes and applies none of them.
63
- *
64
- * It reads as an accessor pair rather than a field because those are two
65
- * different questions sharing one name, and the alternative was a second
66
- * property answering the first. That worked for a boolean, where `isOpen`
67
- * reads well; there is no such word for a value or an active item, so all
68
- * four of these mixins now have this shape instead of two shapes.
56
+ * One property, not two, so `dialog.open` answers the question anyone
57
+ * asking it meant. Writing always applies; `<helix-dialog open>` is the
58
+ * same statement in markup.
69
59
  *
70
60
  * **Not reflected, and that is required rather than tidy.** Reflection
71
- * writes the resolved state back as an attribute, and writing the attribute
72
- * is how a consumer says they own it. So an uncontrolled element that
73
- * opened itself would put `open` on its own host and, from the next thing
74
- * that read it, be indistinguishable from one the consumer controls:
75
- * cloned, serialised for hydration, or diffed by a framework, it would come
76
- * back holding whatever the last reader did rather than what the author
77
- * wrote. `:state(open)` carries the resolved state for stylesheets, which
78
- * is what the mirror has always been for.
61
+ * writes the resolved state back as an attribute, so an element that opened
62
+ * itself would put `open` on its own host: cloned, serialized for
63
+ * hydration, or diffed by a framework, it comes back holding whatever the
64
+ * last reader did rather than what the author wrote. `:state(open)` carries
65
+ * the resolved state for stylesheets, which is what the mirror is for.
79
66
  */
80
67
  get open() {
81
68
  return this.openState ?? this.defaultOpen;
@@ -88,14 +75,14 @@ export const MixinControllerOpen = (Base) => {
88
75
  /**
89
76
  * Records the resolved state as the custom state `open`.
90
77
  *
91
- * **A state and not `data-open`, which is what this wrote until 27 Aug
92
- * 2026.** An `<helix-accordion-item default-open>` was served without the
93
- * attribute and gained it on upgrade, which is a hydration mismatch a React
94
- * consumer reports. See `HelixElement.toggleState`, and see `Toast.Item`
95
- * for the other answer to the same fault: inverting the hook so the write
96
- * falls on the side that only happens after somebody acts. Inversion works
97
- * where the resting state is the loud one and cannot help here, `checked`
98
- * and `open` being true only because a consumer asked for them.
78
+ * **A custom state and never `data-open`.** An attribute mirror is in the
79
+ * DOM, so an `<helix-accordion-item default-open>` served without it gains
80
+ * it on upgrade, which is a hydration mismatch a React consumer reports.
81
+ * See `HelixElement.toggleState`, and see `Toast.Item` for the other answer
82
+ * to the same fault: inverting the hook so the write falls on the side that
83
+ * only happens after somebody acts. Inversion works where the resting state
84
+ * is the loud one and cannot help here, `checked` and `open` being true
85
+ * only because a consumer asked for them.
99
86
  */
100
87
  willUpdate(changed) {
101
88
  super.willUpdate(changed);
@@ -109,7 +96,6 @@ export const MixinControllerOpen = (Base) => {
109
96
  * element opens or closes itself, whether or not a consumer declared
110
97
  * `open`: a declared property is a seed and a correction, and a consumer
111
98
  * who wants the decision for themselves refuses it and assigns their own.
112
- * See 294.
113
99
  */
114
100
  requestOpen(next) {
115
101
  if (this.open === next) {
@@ -133,19 +119,17 @@ export const MixinControllerOpen = (Base) => {
133
119
  /**
134
120
  * Says the state moved, under the platform's own name and in its shape.
135
121
  *
136
- * **`<details>` fires `toggle` and a popover fires `toggle`, and sixteen
137
- * families here fired neither.** Two of them render a real `<dialog>` and
138
- * ten show a `popover="manual"` panel, so the browser was already firing
139
- * something on an element inside a shadow root, where `toggle` neither
140
- * bubbles nor composes and so reaches nobody. The rest, the disclosures,
141
- * have no native element to fire anything at all: `AccordionItem` and
142
- * `TreeItem` are `aria-expanded` and `hidden` written by hand, which is
143
- * `<details>`'s job done manually.
122
+ * **A native `toggle` from inside a shadow root reaches nobody**, because
123
+ * it neither bubbles nor composes, so the ten families showing a
124
+ * `popover="manual"` panel and the two rendering a real `<dialog>` announce
125
+ * nothing to a consumer. The disclosures have no native element to fire
126
+ * anything at all: `AccordionItem` and `TreeItem` are `aria-expanded` and
127
+ * `hidden` written by hand, which is `<details>`'s job done manually.
144
128
  *
145
- * **From here rather than from `PopoverController`, which had it first.**
146
- * The controller covers eleven families and the mixin covers sixteen, and
147
- * the overlap is ten that would have announced twice. What `toggle` reports
148
- * is the *state*, and the state is this mixin's; the controller is a way of
129
+ * **From here rather than from `PopoverController`.** The controller covers
130
+ * eleven families and the mixin covers sixteen, so announcing from both
131
+ * would announce twice on the ten they share. What `toggle` reports is the
132
+ * *state*, and the state is this mixin's; the controller is a way of
149
133
  * putting a panel on the screen.
150
134
  *
151
135
  * **`newState` and `oldState`, because that is the platform's shape**, so a