@fusion.dev/helix-headless 0.2.0 → 0.4.0

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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -9,89 +9,10 @@ import { x } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
- /**
13
- * @summary One message in the toast region.
14
- *
15
- * @description
16
- * An element rather than a slot, because it carries a timeout, a tone, a
17
- * dismiss control and a lifetime that pauses: behavior, state and API, which
18
- * is the test for whether a region becomes an element in this library.
19
- *
20
- * **The timer is the item's own.** Held by the region, one message arriving
21
- * would restart another's, so a stack of three would go from three seconds each
22
- * to nine for the last one. Each message is on its own clock from the moment it
23
- * is rendered.
24
- *
25
- * **A message is the opposite of the page it is over.** Inverted by default,
26
- * because a thing arriving in a corner has to be told apart from the page in
27
- * the moment before it is read, and inverting does that without spending a
28
- * color that means something. It is a flip of `color-scheme` rather than a
29
- * named inverse surface: every token in the subtree re-resolves, so a `Retry`
30
- * a consumer slots in comes out right without knowing where it is. `Tooltip`
31
- * can name the inverse surface directly because it holds no controls; this
32
- * cannot. Set `data-color-mode` on the region to take the page's mode instead.
33
- *
34
- * **No duration means it stays.** That is the safe default rather than the
35
- * common one: anything carrying an action has to still be there when the reader
36
- * decides to take it, and a component cannot tell from the outside whether the
37
- * message it was given is disposable.
38
- *
39
- * **The clock stops while it is being read.** Hover pauses it, and so does
40
- * focus landing anywhere inside, which is the case that matters more: a
41
- * keyboard reader tabbing towards the action in a toast is exactly the person a
42
- * timer must not race. Both resume where they left off rather than starting
43
- * again, so a message half-read does not get a full second life every time a
44
- * pointer crosses it.
45
- *
46
- * **Going away is a request, never an act.** It dispatches a cancelable
47
- * `helix-dismiss` and does nothing else: the consumer owns the list, so the
48
- * item removing itself would leave the two disagreeing. Auto-dismiss and the
49
- * dismiss control raise the same event, so a consumer handles one path and not
50
- * two.
51
- *
52
- * @since 0.1.0
53
- * @status experimental
54
- * @documentation https://helix.fusion.dev/components/toast
55
- *
56
- * @slot - The message.
57
- * @slot actions - What to do about it, usually a button or a link.
58
- * @slot dismiss-icon - Replaces the glyph inside the dismiss control.
59
- * @slot icon - Replaces the tone mark. Matches `Callout`, which is why the
60
- * dismiss control's own glyph is `dismiss-icon` rather than this: two slots
61
- * cannot share a name, assignment goes to whichever comes first in the tree,
62
- * and the tone mark is the one a consumer reaches for.
63
- *
64
- * @event {HelixDismissEvent} helix-dismiss - Fired when it asks to go, whether by the control or by its timer. Cancelable: refusing it takes the decision and nothing moves.
65
- *
66
- * @csspart actions - The region holding what to do about it.
67
- * @csspart content - The message.
68
- * @csspart dismiss - The control that asks to close.
69
- * @csspart dismiss-mark - The glyph on the dismiss control, where none is slotted.
70
- * @csspart icon - The tone mark, where the message has one.
71
- * @csspart icon-mark - The tone glyph, where none is slotted.
72
- * @csspart item - The message's own surface.
73
- * @csspart timer - The bar showing how long is left, where a duration was given.
74
- *
75
- * @cssproperty --toast-duration - How long this message stays, in milliseconds, for a progress hint.
76
- */
77
12
  let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissible(HelixElement) {
78
13
  constructor() {
79
14
  super(...arguments);
80
- /** Whether it is showing, whoever said so, and `undefined` until somebody has. */
81
15
  this.openState = undefined;
82
- /* Arrows, so `this` is the item wherever the listener ends up. Lit does bind
83
- a template listener to its host, and relying on that puts a load-bearing
84
- fact about a library in a place nobody reading this file would look. */
85
- /**
86
- * How many reasons there are to hold the clock, rather than whether there is
87
- * one.
88
- *
89
- * Hover and focus stop the same timer, and the two are genuinely independent:
90
- * a pointer can leave while focus stays, and focus can leave while the
91
- * pointer stays. Uncounted, a reader who tabs into a message and then moves
92
- * the pointer off it has the clock restarted underneath them, which is the
93
- * case the pause exists for. Only the last one out starts it again.
94
- */
95
16
  this.holds = 0;
96
17
  this.handlePause = () => {
97
18
  this.holds += 1;
@@ -104,19 +25,6 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
104
25
  }
105
26
  };
106
27
  }
107
- /**
108
- * Whether the message is still showing.
109
- *
110
- * The item always applies: it goes when it is asked to, having announced
111
- * `helix-dismiss` first, and a consumer who owns the list refuses that event
112
- * to take the decision instead.
113
- *
114
- * **No `default-open`, unlike the four mixins.** A `default-` seed exists so
115
- * markup can choose a starting state without claiming ownership, and there is
116
- * only one starting state that makes sense: a message that has been rendered
117
- * is showing. An attribute whose only legal value is the default is an
118
- * attribute nobody can use.
119
- */
120
28
  get open() {
121
29
  return this.openState ?? true;
122
30
  }
@@ -125,21 +33,10 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
125
33
  this.openState = next;
126
34
  this.requestUpdate('open', previous);
127
35
  }
128
- /**
129
- * Asks to be taken away, and reports whether the request stood.
130
- *
131
- * Announced before anything happens, so `preventDefault()` is a refusal and
132
- * the message stays. The event still carries the label, because a consumer
133
- * removing this from their own state has to know which one went, and that is
134
- * true whether or not the item hid itself first.
135
- */
136
36
  dismiss() {
137
37
  if (!this.open) {
138
38
  return true;
139
39
  }
140
- /* The clock stops either way. A timer left running on a message that has
141
- gone would fire into nothing, and would hold the item alive until it
142
- did. */
143
40
  clearTimeout(this.timer);
144
41
  this.timer = undefined;
145
42
  return requestChange({
@@ -148,18 +45,11 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
148
45
  this.openState = false;
149
46
  this.requestUpdate('open', previous);
150
47
  },
151
- /* `helix-dismiss` answers a controlled `open`, which reads oddly and is
152
- exact. A toast is not an overlay that opens and closes: `next` below is
153
- `false` and nothing here ever calls `requestOpen(true)`, so it only
154
- ever asks in one direction, and the verb for that direction is
155
- `dismiss`. A consumer controlling `open` therefore answers
156
- `onhelix-dismiss`, and the generated types ask for exactly that. */
157
48
  event: () => this.dismissEvent(),
158
49
  host: this,
159
50
  next: false,
160
51
  });
161
52
  }
162
- /** Stops the clock, leaving whatever is left of it. */
163
53
  pause() {
164
54
  if (this.timer === undefined || this.startedAt === undefined) {
165
55
  return;
@@ -168,13 +58,8 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
168
58
  this.timer = undefined;
169
59
  this.remaining = Math.max((this.remaining ?? 0) - (Date.now() - this.startedAt), 0);
170
60
  this.startedAt = undefined;
171
- /* Said on the host so the indicator can freeze with the clock. A `data-`
172
- attribute rather than a property, because it is written after upgrade
173
- from a pointer moving: nothing declares it and no server rendered it,
174
- which is the same reason `Carousel` names its slides from here. */
175
61
  this.toggleAttribute('data-paused', true);
176
62
  }
177
- /** Starts it again from where it stopped. */
178
63
  resume() {
179
64
  if (this.timer !== undefined || this.remaining === undefined || this.remaining <= 0) {
180
65
  return;
@@ -188,56 +73,21 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
188
73
  }
189
74
  disconnectedCallback() {
190
75
  super.disconnectedCallback();
191
- // A message removed while its timer is running would otherwise fire into
192
- // nothing, and hold the item alive until it did.
193
76
  clearTimeout(this.timer);
194
77
  this.timer = undefined;
195
78
  }
196
79
  willUpdate(changed) {
197
80
  super.willUpdate(changed);
198
- /* Mirrored for CSS rather than reflected onto `open` itself, which is the
199
- library's rule about state: a reflected `open` is indistinguishable from
200
- one the consumer declared, and that attribute is what ownership is read
201
- from. `data-closed` can never be mistaken for a declaration. */
202
- /* **`data-closed`, because open is the resting state.** A toast opens by
203
- default, so a `data-open` would appear on a consumer's server-rendered
204
- `<helix-toast-item>` without React having sent it. Inverting keeps the
205
- `data-` hook for the headless consumer this publishes it for, and moves
206
- the write to the side that only happens after somebody dismisses the
207
- message, which is long past hydration. */
208
81
  this.toggleAttribute('data-closed', !this.open);
209
- /* And `hidden` beside it, because `data-closed` is a hook and not a
210
- mechanism. With the hiding living only in the styled sheet's
211
- `opacity: 0` and `visibility: hidden`, a consumer on this tier has a
212
- dismissed message that never goes away: measured, a closed base toast
213
- printed its text on the page.
214
- It costs the styled tier nothing, the same as the accordion: `:host` sets
215
- `display: grid` unconditionally so the author rule beats the UA sheet,
216
- and the exit still animates its row down from `1fr`. */
217
82
  this.toggleAttribute('hidden', !this.open);
218
83
  }
219
84
  firstUpdated(changed) {
220
85
  super.firstUpdated(changed);
221
- /* Here rather than in `connectedCallback`, which the server never calls, and
222
- started only once: the clock belongs to this message's life on screen. */
223
86
  if (this.duration !== undefined && this.duration > 0) {
224
87
  this.remaining = this.duration;
225
88
  this.resume();
226
89
  }
227
90
  }
228
- /**
229
- * The mark for the message's tone, behind a seam.
230
- *
231
- * Slot fallback, so a consumer replaces it by slotting; and a method, so the
232
- * tier above replaces it by overriding. Those answer different people: the
233
- * slot is for the page and the method is for whoever is building the styled
234
- * layer, and a fallback with only the first is one a tier can restyle and
235
- * cannot change.
236
- *
237
- * The mark is passed in rather than looked up again, since `render` has
238
- * already resolved the tone and a second lookup is a second place for the
239
- * fallback to disagree with the tone around it.
240
- */
241
91
  renderIcon(mark) {
242
92
  return html `<slot name="icon">
243
93
  <svg part="icon-mark"
@@ -250,15 +100,6 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
250
100
  </svg>
251
101
  </slot>`;
252
102
  }
253
- /**
254
- * The mark on the dismiss control, behind a seam.
255
- *
256
- * Slot fallback, so a consumer replaces it by slotting; and a method, so the
257
- * tier above replaces it by overriding. Those answer different people: the
258
- * slot is for the page and the method is for whoever is building the styled
259
- * layer, and a fallback with only the first is one a tier can restyle and
260
- * cannot change.
261
- */
262
103
  renderDismissIcon() {
263
104
  return html `<slot name="dismiss-icon">
264
105
  <svg part="dismiss-mark" viewBox=${x.viewBox} fill="currentColor" aria-hidden="true">
@@ -267,12 +108,6 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
267
108
  </slot>`;
268
109
  }
269
110
  render() {
270
- /* Nothing when no tone was given, which is the common message: "Project
271
- saved" is not an error, a warning or an announcement, and an icon there
272
- would be decoration standing where meaning is supposed to be. `Callout`
273
- falls back to `info` because a callout exists to set a passage apart and
274
- is always *about* something; a toast is just the next thing that
275
- happened. */
276
111
  const mark = toneMark(this.tone);
277
112
  return html `<div
278
113
  part="item"
@@ -7,67 +7,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary The region messages arrive in, and the only thing that announces them.
12
- *
13
- * @description
14
- * **The region is why this is one family rather than two.** A live region has
15
- * to be in the document *before* a message is put into it: assistive technology
16
- * subscribes to the region, and a region that arrives already holding its
17
- * message is frequently not announced at all. So the container is a part of the
18
- * family and not an arranger of complete things, which is the same reasoning
19
- * that puts `Radio.Group` inside its own family.
20
- *
21
- * **Polite, never assertive.** `role="alert"` interrupts whatever a reader is
22
- * being told, and almost nothing that arrives in a corner of the screen has
23
- * earned that. Something that genuinely cannot wait is a `Dialog`, which stops
24
- * the page for everyone rather than only for the people who cannot see it.
25
- *
26
- * **It never creates or destroys anything.** The consumer renders items and
27
- * removes them; asking to go is a cancelable `helix-dismiss` from the item. An
28
- * item that has gone hides itself and stays in the tree, so a consumer whose
29
- * state still lists it does not have their framework fighting a node this
30
- * component took away. It hides without waiting to be told, unless the consumer
31
- * declared `open` and said the state is theirs.
32
- * There is no `toast.show()`, because an imperative queue inside a component is
33
- * a second copy of the application's state that nothing can render from: an app
34
- * that wants a `toast()` function writes a thin one over the state it already
35
- * has, and keeps the ability to render the same messages on the server, in a
36
- * test, or twice.
37
- *
38
- * @since 0.1.0
39
- * @status experimental
40
- * @category overlays
41
- * @aliases Toaster, Snackbar, Notifications
42
- * @documentation https://helix.fusion.dev/components/toast
43
- *
44
- * @slot - The messages, as `helix-toast-item` elements.
45
- *
46
- * @csspart region - The stack the messages are in.
47
- */
48
10
  let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
49
11
  constructor() {
50
12
  super(...arguments);
51
- /**
52
- * Accessible name for the region, such as "Notifications".
53
- */
54
13
  this.label = 'Notifications';
55
14
  }
56
15
  willUpdate(changed) {
57
16
  super.willUpdate(changed);
58
- /* On the host through internals, so the region is a landmark to anything
59
- walking the light DOM as well as to a screen reader, and so nothing is
60
- written onto the host as an attribute a server's markup never had. */
61
17
  hostAria(this.internals, { label: this.label, role: 'status' });
62
18
  this.internals.ariaLive = 'polite';
63
- /* Additions are what a reader needs; a message going away is not news, and
64
- announcing removals means every dismissal is read out. */
65
19
  this.internals.ariaRelevant = 'additions';
66
- /* `role="status"` carries an implicit `aria-atomic="true"`, which means the
67
- whole region is re-read whenever any part of it changes. With a stack of
68
- four, the fifth arrival announces all five. `additions` alone does not
69
- help: it says which mutations count, not how much is read out when one
70
- does. Both are needed, and only one was here. */
71
20
  this.internals.ariaAtomic = 'false';
72
21
  }
73
22
  render() {
@@ -8,80 +8,15 @@ import { customElement, DisabledCascadeController, HelixFormElement, hostAria, I
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessToggle } from '../root/toggle-root.js';
11
- /**
12
- * @summary A row of options with the selection marked by a moving indicator.
13
- *
14
- * @description
15
- * The indicator is one element that travels, not a background that appears on
16
- * whichever item is current. That difference is the whole point: a fill that
17
- * swaps is two things changing at once and reads as a flicker, while one that
18
- * moves is a single object the eye follows, and it says where the selection
19
- * came from as well as where it went.
20
- *
21
- * Its whole box is measured from the selected item and written onto
22
- * the indicator as custom properties, because labels are different lengths and
23
- * no proportion of the group is the right answer. Measurement is client-side by
24
- * necessity, so the server renders the indicator hidden and it fades in once it
25
- * has somewhere to be, which is the harmless direction: it appears rather than
26
- * jumping from a wrong place.
27
- *
28
- * A radio group, and only that. Navigation is links marked with `aria-current`
29
- * and styled by the `data-navigation-item` recipe: radio semantics would announce a
30
- * set of destinations as a set of choices. A tab controls a panel.
31
- *
32
- * Being a radio group, it is a form control, and it says so through
33
- * `HelixFormElement`: it reports its value under a `name`, comes back to the
34
- * markup's choice on reset, and restores the one a reader made when the browser
35
- * brings a form back.
36
- *
37
- * The semantics were already right: the host has carried `role="radiogroup"`
38
- * and the items `role="radio"` from the start. What none of it did was reach a
39
- * form. A button in a shadow root submits nothing whatever its role, so the
40
- * value has to be reported by the host, which is the only thing a form can
41
- * see.
42
- *
43
- * @since 0.1.0
44
- * @status experimental
45
- * @aliases SegmentedControl, Segmented, Navigation, Switcher
46
- * @documentation https://helix.fusion.dev/components/toggle-group
47
- *
48
- * @slot - The group's items.
49
- *
50
- * @event {Event} change - Announced once the value has moved.
51
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the answer moves, and cancelable. The only way to refuse one.
52
- * @event {HelixSelectEvent} helix-select - Fired when an item is chosen. Dispatched by the item and listened for here. Cancelable: refusing it takes the decision and nothing moves.
53
- * @event {Event} input - Announced once the value has moved.
54
- *
55
- * @csspart divider - The rule after an inline label, separating it from the items.
56
- * @csspart feedback - What is wrong with it.
57
- * @csspart hint - Help under the row, while nothing is wrong.
58
- * @csspart indicator - The moving marker behind the selected item.
59
- * @csspart label - The question, over the row.
60
- * @csspart row - The line the track sits on, with whatever flanks it.
61
- * @csspart track - The bar the items sit in.
62
- */
63
11
  let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinControllerValue(MixinField(HelixFormElement)) {
64
12
  constructor() {
65
13
  super(...arguments);
66
14
  this.indicator = new IndicatorController(this, {
67
- // The same exact match `sync` makes: nothing chosen means nothing marked,
68
- // and the controller takes the mark away rather than parking it on item one.
69
15
  active: () => this.items.find((item) => item.value === this.value),
70
16
  indicator: () => this.renderRoot?.querySelector('[part="indicator"]'),
71
17
  surface: () => this.renderRoot?.querySelector('[part="track"]'),
72
18
  });
73
- /* Moving and choosing are one act, which is the radio group pattern this
74
- control also follows, and is what the controller exists to stop being
75
- written per component. */
76
- /* Registered rather than held. A controller adds itself to the host, so there
77
- is no reference to keep: nothing here talks to it after construction, which
78
- is why it is `protected` and not `private`. A private field nothing reads
79
- is an unused one, and the honest reading is that the subclass tier is where
80
- a consumer would reach for it. */
81
19
  this.keys = new KeyboardNavigationController(this, { items: () => this.items });
82
- /* Stated once, and the group never thinks about it again. The controller is
83
- what stops the cascade being hand-called from whichever methods a component
84
- happens to have. */
85
20
  this.cascade = new DisabledCascadeController(this, {
86
21
  items: () => this.items,
87
22
  });
@@ -89,22 +24,8 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
89
24
  this.requestValue(event.detail.value);
90
25
  };
91
26
  }
92
- /** Marks the chosen item, separately from placing the indicator. */
93
27
  sync() {
94
- /* An exact match and nothing else, never `resolveActiveItem`, which falls
95
- * back to the first enabled item: a group with no value would mark one
96
- * anyway, `aria-checked="true"` and the thumb under it and full selected
97
- * styling, while the group reported nothing. A `required` group would then
98
- * tell a reader a value is missing while they are looking at one.
99
- *
100
- * That fallback is right where it came from. A tab set always shows a panel,
101
- * so something has to be current; a group that reports a form value has the
102
- * honest option of reporting none. */
103
28
  const active = this.items.find((item) => item.value === this.value);
104
- /* With nothing chosen there is no selection to carry the tab stop, so the
105
- first item holds one until there is. Otherwise an unanswered group is a
106
- control the keyboard cannot enter, which is what the fallback was quietly
107
- also doing for it. */
108
29
  const entry = active ? undefined : this.items.find((item) => !item.disabled);
109
30
  for (const item of this.items) {
110
31
  item.selected = item === active;
@@ -114,15 +35,6 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
114
35
  get items() {
115
36
  return [...this.children].filter((child) => child instanceof HelixHeadlessToggle);
116
37
  }
117
- /**
118
- * Watches the group and every item it currently holds.
119
- *
120
- * Called again on `slotchange`, because the set was read once at first paint:
121
- * an item added later was measured by nothing, so a group whose options
122
- * arrive with the data had a thumb that stopped following the moment one of
123
- * them changed width. Observing the same element twice is a no-op, so there
124
- * is nothing to unwind first.
125
- */
126
38
  watch() {
127
39
  this.observer?.observe(this);
128
40
  for (const item of this.items) {
@@ -132,14 +44,6 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
132
44
  connectedCallback() {
133
45
  super.connectedCallback();
134
46
  this.addEventListener('helix-select', this.handleSelection);
135
- /* The observer is rebuilt here, because nothing else does it.
136
- *
137
- * `firstUpdated` runs once for the life of the element and
138
- * `disconnectedCallback` runs every time it moves, so a framework
139
- * reordering a list left this permanently dead: the observer gone, nothing
140
- * to rebuild it, and the component still rendering as though it were
141
- * watching. Guarded on `hasUpdated`, because the first connection happens
142
- * before the first render and there is nothing to watch yet. */
143
47
  if (this.hasUpdated) {
144
48
  this.measure();
145
49
  this.watch();
@@ -154,85 +58,31 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
154
58
  super.firstUpdated(changed);
155
59
  this.sync();
156
60
  this.indicator.place();
157
- /* Per item, not only the host, which is what the controller's own observer
158
- covers. An item can change width inside a track that keeps its size, and
159
- the host never fires for that. */
160
61
  this.measure();
161
62
  this.watch();
162
63
  }
163
- /** The observer the items are registered on, rebuilt with them. */
164
64
  measure() {
165
65
  this.observer?.disconnect();
166
66
  this.observer = new ResizeObserver(() => this.indicator.place());
167
67
  }
168
68
  updated(changed) {
169
69
  super.updated(changed);
170
- // Every update, not only the ones that name `value`: see the note on
171
- // `ColorInput.updated`. The placement stays gated, because the indicator
172
- // controller already places on every update of its own host.
173
70
  this.sync();
174
71
  if (changed.has('value')) {
175
72
  this.indicator.place();
176
73
  }
177
74
  this.validate();
178
- /* Every update, because the label and the message come and go with the
179
- properties behind them, and an element reference to a node that has been
180
- re-rendered is a reference to nothing. */
181
75
  this.linkHost();
182
76
  }
183
77
  willUpdate(changed) {
184
78
  super.willUpdate(changed);
185
- // Through internals, so nothing lands on the host as an attribute. A nav
186
- // renders a real `<nav>` in the shadow root and takes its semantics from
187
- // that, so the host stays neutral.
188
- /* The label goes on as a string as well as the element reference
189
- `linkHost` sets, because this is the half a server can write. The
190
- reference wins on the client, which is what lets a consumer slot a label
191
- richer than a string. */
192
79
  hostAria(this.internals, { label: this.label, role: 'radiogroup' });
193
- // `null` rather than an empty string when nothing is chosen, which is the
194
- // difference between a control that submits nothing and one that submits a
195
- // blank.
196
80
  this.internals.setFormValue(this.value === '' ? null : this.value);
197
81
  }
198
- /**
199
- * The verdict on the answer `required` asks for.
200
- *
201
- * `required` arrives with `MixinField` and enforces nothing on its own: the
202
- * attribute reflects and the marker renders beside the label, and without this
203
- * a form with an unanswered group submits anyway. The marker is a promise to
204
- * the reader that something will stop them.
205
- *
206
- * The anchor is the track rather than the host, so the browser's own bubble
207
- * points at the control rather than at a box with nothing in it.
208
- */
209
82
  validate() {
210
83
  this.reportRequired(this.value === '', 'Choose one to continue.', 'track');
211
84
  }
212
- /**
213
- * The row the track sits in, so something can sit beside it.
214
- *
215
- * **Bare slots, and a wrapper only around the row itself.** An unassigned
216
- * slot is `display: contents` in the UA stylesheet and generates no box at
217
- * all, so a group with nothing slotted lays out exactly as it did before this
218
- * existed: no grid track, no flex item, no gap. Wrapping each side in a div
219
- * instead would paint an empty inset band on every group in the library that
220
- * never asked for one, which is the measurement Card already paid for.
221
- *
222
- * The row does always generate a box, and is entitled to: the track is always
223
- * in it.
224
- */
225
85
  render() {
226
- /* The track is an element rather than the host, and has to be: the label,
227
- the hint and the message are the host's children too, so a track on the
228
- host makes them flex items in the same row as the segments. The track also
229
- owns the box the indicator is measured against, which is why `place` reads
230
- it rather than `this`. */
231
- /* **Inside the track when it is placed at the start, not beside it.** A
232
- label sitting outside reads as a field's label that happens to be on one
233
- line; inside, with a rule after it, the group reads as one control that
234
- says what it is for. The divider is what makes that legible: without it
235
- the word is a first segment that cannot be pressed. */
236
86
  const inside = this.labelPlacement === 'start';
237
87
  const label = this.renderGroupLabel();
238
88
  return html `${inside ? nothing : label}
@@ -7,108 +7,30 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, HelixSelectEvent } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary One option in a toggle group.
12
- *
13
- * @description
14
- * Renders a `<button role="radio">`. This is a form control and nothing else.
15
- *
16
- * Navigation is not this. It is links marked with `aria-current` and styled by
17
- * the `data-navigation-item` recipe in the base layer, which carries the same look
18
- * without radio semantics announcing destinations as choices.
19
- *
20
- * @since 0.1.0
21
- * @status experimental
22
- * @category actions
23
- * @aliases Segment, NavItem, Option
24
- * @documentation https://helix.fusion.dev/components/toggle
25
- *
26
- * @slot - The item's label.
27
- *
28
- * @event {HelixSelectEvent} helix-select - Fired when the item is chosen. Cancelable: refusing it takes the decision and nothing moves.
29
- *
30
- * @csspart item - The radio button.
31
- */
32
10
  let HelixHeadlessToggle = class HelixHeadlessToggle extends HelixElement {
33
11
  constructor() {
34
12
  super(...arguments);
35
- /**
36
- * Whether the item can be chosen.
37
- */
38
13
  this.disabled = false;
39
- /** What the markup said, read once at construction for the reason above. */
40
14
  this.seeded = this.hasAttribute('selected');
41
- /**
42
- * Whether this item holds the group's tab stop while nothing is chosen.
43
- *
44
- * The stop normally rides on the selection, and an unanswered group has no
45
- * selection, so without this the keyboard cannot reach it. Written by the
46
- * group, with no attribute: it is coordination between elements, and
47
- * `tabindex` on a host the consumer rendered is the change a React consumer's
48
- * hydration reports and does not patch up.
49
- */
50
15
  this.focusable = false;
51
- /**
52
- * Whether this item is the current one.
53
- */
54
16
  this.selected = false;
55
- /**
56
- * Identifies the item to the group.
57
- */
58
17
  this.value = '';
59
18
  }
60
- /**
61
- * Whether the markup declared this one chosen, which is what a reset returns to.
62
- *
63
- * `<option selected>` reflects to `defaultSelected` and `option.selected` is
64
- * live state that does not reflect, and those are two different facts. This
65
- * element has one `selected` doing both jobs, so what is captured here is what
66
- * the markup said at upgrade, before anything could overwrite it: read the
67
- * attribute later and the listbox's own writes answer instead.
68
- *
69
- * **The deeper half is not this.** State reflecting under the declaration's
70
- * name is the fault `TreeItem` avoids by mirroring to `data-selected`.
71
- * Splitting them means moving what every stylesheet selects on, which is a
72
- * change of its own rather than a property being added.
73
- */
74
19
  get defaultSelected() {
75
20
  return this.seeded;
76
21
  }
77
22
  set defaultSelected(next) {
78
23
  this.seeded = next;
79
24
  }
80
- /**
81
- * Moves focus to this item's own control.
82
- *
83
- * The group drives arrow-key navigation and cannot reach the control itself,
84
- * which is inside this shadow root. Exposing the intent as a method keeps the
85
- * group out of the child's internals.
86
- */
87
25
  focusItem() {
88
26
  this.renderRoot.querySelector('[part="item"]')?.focus();
89
27
  }
90
- /** Chooses this item and announces it. */
91
28
  select() {
92
29
  if (this.disabled) {
93
30
  return;
94
31
  }
95
32
  this.dispatchEvent(new HelixSelectEvent({ value: this.value }));
96
33
  }
97
- /**
98
- * Mirrors the chosen state where a stylesheet can select on it.
99
- *
100
- * **A custom state rather than the `selected` attribute.** The parent writes
101
- * this: a listbox choosing a row, a tab strip moving the tab, a radio group
102
- * answering for its set. Reflected, that puts the element's own bookkeeping
103
- * onto markup a consumer wrote and a consumer's server never sent, which
104
- * React reconciles and reports as a mismatch, and which a serialized element
105
- * pasted back declares as a choice nobody made. `ElementInternals` holds it
106
- * instead, where it is not in the DOM at all.
107
- *
108
- * Reading `selected` is unaffected and so is writing it: the attribute still
109
- * seeds the property, which is what `<helix-toggle selected>` in a consumer's
110
- * markup means. There is no write back.
111
- */
112
34
  willUpdate(changed) {
113
35
  super.willUpdate(changed);
114
36
  this.toggleState('selected', this.selected);