@fusion.dev/helix-headless 0.158.0 → 0.160.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 (58) hide show
  1. package/custom-elements.json +1925 -174
  2. package/dist/app-header/root/app-header-root.d.ts +59 -12
  3. package/dist/app-header/root/app-header-root.js +68 -29
  4. package/dist/button/root/button-root.d.ts +8 -0
  5. package/dist/button/root/button-root.js +4 -0
  6. package/dist/button-group/root/button-group-root.d.ts +20 -7
  7. package/dist/button-group/root/button-group-root.js +30 -5
  8. package/dist/date-input/root/date-input-root.d.ts +108 -19
  9. package/dist/date-input/root/date-input-root.js +112 -13
  10. package/dist/date-picker/root/date-picker-root.d.ts +81 -16
  11. package/dist/date-picker/root/date-picker-root.js +140 -36
  12. package/dist/element-events.generated.js +9 -1
  13. package/dist/format-date/fields.js +1 -0
  14. package/dist/format-date/options.d.ts +3 -1
  15. package/dist/format-date/options.js +4 -1
  16. package/dist/format-date/root/format-date-root.d.ts +27 -11
  17. package/dist/format-date/root/format-date-root.js +6 -0
  18. package/dist/lightbox/item/lightbox-item.d.ts +7 -1
  19. package/dist/lightbox/item/lightbox-item.js +15 -2
  20. package/dist/lightbox/root/lightbox-root.d.ts +8 -4
  21. package/dist/lightbox/root/lightbox-root.js +9 -1
  22. package/dist/lightbox/zoom-controller.d.ts +13 -4
  23. package/dist/lightbox/zoom-controller.js +17 -0
  24. package/dist/listbox/root/listbox-root.js +5 -0
  25. package/dist/notifications/notification/notification.d.ts +1 -0
  26. package/dist/notifications/notification/notification.js +2 -1
  27. package/dist/slider/root/slider-root.d.ts +54 -3
  28. package/dist/slider/root/slider-root.js +124 -2
  29. package/dist/steps/root/steps-root.js +8 -6
  30. package/dist/steps/step/step.d.ts +9 -0
  31. package/dist/steps/step/step.js +19 -2
  32. package/dist/tabs/panel/tab-panel.d.ts +22 -1
  33. package/dist/tabs/panel/tab-panel.js +22 -1
  34. package/dist/tabs/root/tabs-root.d.ts +21 -5
  35. package/dist/tabs/root/tabs-root.js +24 -13
  36. package/dist/tabs/tab/tab.d.ts +4 -2
  37. package/dist/tabs/tab/tab.js +3 -1
  38. package/dist/time-input/root/time-input-root.d.ts +55 -7
  39. package/dist/time-input/root/time-input-root.js +70 -10
  40. package/dist/time-picker/root/time-picker-root.d.ts +27 -5
  41. package/dist/time-picker/root/time-picker-root.js +52 -13
  42. package/dist/toast/item/toast-item.d.ts +27 -8
  43. package/dist/toast/item/toast-item.js +47 -11
  44. package/dist/toast/root/toast-root.d.ts +24 -1
  45. package/dist/toast/root/toast-root.js +54 -0
  46. package/dist/toggle/group/toggle-group.d.ts +18 -1
  47. package/dist/toggle/group/toggle-group.js +8 -2
  48. package/dist/toggle/root/toggle-root.d.ts +2 -2
  49. package/dist/toggle/root/toggle-root.js +2 -1
  50. package/dist/tour/root/tour-root.d.ts +34 -0
  51. package/dist/tour/root/tour-root.js +92 -3
  52. package/dist/tour/stop/tour-stop.d.ts +33 -1
  53. package/dist/tour/stop/tour-stop.js +12 -1
  54. package/lint.json +1 -1
  55. package/package.json +5 -5
  56. package/types/jsx.d.ts +1119 -218
  57. package/types/vue.d.ts +278 -51
  58. package/types/words.d.ts +72 -0
@@ -1,5 +1,6 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ declare const HelixHeadlessAppHeader_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
3
4
  /**
4
5
  * @summary Page-level banner holding brand, navigation, and actions.
5
6
  *
@@ -26,12 +27,21 @@ import { type PropertyValues, type TemplateResult } from 'lit';
26
27
  *
27
28
  * **Set `fold-below` to fold the middle region on a narrow screen.** Below that
28
29
  * many pixels of the bar's own width, the middle region is hidden behind a
29
- * toggle at the end of the bar, named by `menu-label`. The toggle says whether
30
- * the region is open with `aria-expanded`, and the region opens below the row.
31
- * Escape closes it and puts focus back on the toggle. The custom state
32
- * `folded` is on the host while the bar is that narrow, and `open` while the
33
- * folded region is open. A server serves the bar unfolded, and the browser
34
- * folds it once it has measured it.
30
+ * toggle at the end of the bar, named by `menu-label`, or at its start with
31
+ * `toggle-placement="start"`. The toggle says whether the region is open with
32
+ * `aria-expanded`, and the region opens below the row. The region follows the
33
+ * toggle's region in focus order, so Tab from the toggle at the end goes into
34
+ * the open region. The custom state `folded` is on the host while the bar is
35
+ * that narrow, and `open` while the folded region is open. A server serves the
36
+ * bar unfolded, and the browser folds it once it has measured it.
37
+ *
38
+ * **`open` says whether the folded region is open, and setting it opens or
39
+ * closes the region.** `requestOpen(true)` and `requestOpen(false)` do the same
40
+ * and send `helix-before-open` or `helix-before-close` first, which you can
41
+ * cancel, and `toggleOpen()` asks for the opposite. `toggle` follows every
42
+ * change. The region closes on Escape, which puts focus back on the toggle,
43
+ * when focus moves to something outside the header, and on a press outside
44
+ * it. A bar that widens past `fold-below` closes its region.
35
45
  *
36
46
  * The host is the page's `banner` landmark. Placed inside `main`, `article`,
37
47
  * `aside`, `nav` or `section`, it is a plain box instead, as a native
@@ -53,11 +63,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
53
63
  * @slot menu-icon - Replaces the glyph inside the toggle that opens a folded middle region.
54
64
  * @slot start - Leading content, usually a wordmark.
55
65
  *
66
+ * @event {HelixBeforeCloseEvent} helix-before-close - The folded region is about to close. Cancelable: cancel it to keep it open.
67
+ * @event {HelixBeforeOpenEvent} helix-before-open - The folded region is about to open. Cancelable: cancel it to keep it closed.
56
68
  * @event {HelixScrolledEvent} helix-scrolled - Fired when the page crosses the
57
69
  * scroll threshold, with the new state. A custom state cannot be observed the
58
70
  * way an attribute can, so listen for this event to follow it. The event uses
59
71
  * the header's own threshold and hysteresis, so you do not need a second scroll
60
72
  * listener.
73
+ * @event {ToggleEvent} toggle - The folded region opened or closed. The event carries `newState` and `oldState`.
61
74
  *
62
75
  * @csspart content - The middle region.
63
76
  * @csspart end - The trailing region.
@@ -71,7 +84,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
71
84
  * @cssstate open - While the folded middle region is open.
72
85
  * @cssstate scrolled - While the page is scrolled down from the top, which is when the bar lifts off it.
73
86
  */
74
- export declare class HelixHeadlessAppHeader extends HelixElement {
87
+ export declare class HelixHeadlessAppHeader extends HelixHeadlessAppHeader_base {
75
88
  /**
76
89
  * The key this family's words are registered under, its styled tag:
77
90
  * `registerWords('de', { 'helix-app-header': { menuLabel: 'Menü' } })`.
@@ -103,20 +116,37 @@ export declare class HelixHeadlessAppHeader extends HelixElement {
103
116
  * page scrolls up.
104
117
  */
105
118
  hideOnScroll: boolean;
119
+ /** The name of the toggle that opens a folded middle region. */
120
+ menuLabel: string;
106
121
  /** Whether the bar is narrower than `fold-below`, which only the observer sets. */
107
122
  private folded;
108
- /** Whether the folded middle region is open. */
109
- private unfolded;
123
+ /** Whether the folded middle region is open and showing. */
124
+ private get unfolded();
110
125
  /** Watches the bar's own width while `fold-below` is set. */
111
126
  private widths;
112
127
  /** Starts or stops watching the width, according to `fold-below`. */
113
128
  private watchWidth;
114
- /** Folds the middle region where the bar is narrower than `fold-below`. */
129
+ /**
130
+ * Folds the middle region where the bar is narrower than `fold-below`. A bar
131
+ * that widens closes its region, so it starts closed when it folds again;
132
+ * one that narrows keeps an `open` you set.
133
+ */
115
134
  private fold;
116
135
  /** Opens or closes the folded middle region. */
117
136
  private readonly handleToggle;
118
137
  /** Escape in the open region closes it and puts focus back on the toggle. */
119
138
  private readonly handleKeydown;
139
+ /**
140
+ * Closes the open region when focus moves to something outside the header.
141
+ * Focus leaving the window has no `relatedTarget`, and closes nothing.
142
+ */
143
+ private readonly handleFocusOut;
144
+ /** Closes the open region on a press outside the header. */
145
+ private readonly handleOutsidePress;
146
+ /** Whether the outside press is listened for, which is while the region is open. */
147
+ private pressing;
148
+ /** Listens for a press outside while the folded region is open, and only then. */
149
+ private watchPresses;
120
150
  /** Whether the bar is out of the way, under `hide-on-scroll`. */
121
151
  private away;
122
152
  /** Where the page was last scrolled to, to tell down from up. */
@@ -132,13 +162,19 @@ export declare class HelixHeadlessAppHeader extends HelixElement {
132
162
  * or its folded region is open, which a reader is using.
133
163
  */
134
164
  private follow;
135
- /** The name of the toggle that opens a folded middle region. */
136
- menuLabel: string;
137
165
  /**
138
166
  * The id of the element whose scrolling lifts the bar, for an app shell that
139
167
  * scrolls a container instead of the window. Unset is the window.
140
168
  */
141
169
  scroller?: string;
170
+ /**
171
+ * Where the toggle that opens a folded middle region sits: at the end of the
172
+ * bar, after the trailing content, or with `start`, before the leading
173
+ * content.
174
+ *
175
+ * Optional, so the resting bar writes no attribute onto the host.
176
+ */
177
+ togglePlacement?: 'start';
142
178
  private landmark;
143
179
  private scrolled;
144
180
  /** What scroll is listened to now, so a change of `scroller` moves it. */
@@ -187,6 +223,16 @@ export declare class HelixHeadlessAppHeader extends HelixElement {
187
223
  * tier above replaces it by overriding.
188
224
  */
189
225
  protected renderMenuIcon(): TemplateResult;
226
+ /**
227
+ * The row, with the middle region between the leading and the trailing
228
+ * content while the bar is wide, and after both while it is folded.
229
+ *
230
+ * **Folded, the region comes last in the markup**, because it opens below
231
+ * the row and focus order follows the markup. Between the two, as it is
232
+ * drawn on a wide bar, Tab from the toggle at the end left the header and
233
+ * the links it had just revealed were reached only by going back. The grid
234
+ * puts it on its own row either way.
235
+ */
190
236
  protected render(): TemplateResult;
191
237
  }
192
238
  declare global {
@@ -194,3 +240,4 @@ declare global {
194
240
  'helix-headless-app-header': HelixHeadlessAppHeader;
195
241
  }
196
242
  }
243
+ export {};
@@ -4,35 +4,47 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, deepActiveElement, deepContains, elementById, HelixElement, HelixScrolledEvent, hostAria, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, deepActiveElement, deepContains, elementById, HelixElement, HelixScrolledEvent, hostAria, MixinControllerOpen, words, } from '@fusion.dev/helix-core';
8
8
  import { list } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { inSectioningContent } from '../sectioning.js';
13
- let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
13
+ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends MixinControllerOpen(HelixElement) {
14
14
  constructor() {
15
15
  super(...arguments);
16
16
  this.hideOnScroll = false;
17
+ this.menuLabel = 'Menu';
17
18
  this.folded = false;
18
- this.unfolded = false;
19
19
  this.handleToggle = () => {
20
- this.unfolded = !this.unfolded;
20
+ this.toggleOpen();
21
21
  };
22
22
  this.handleKeydown = (event) => {
23
23
  if (event.key !== 'Escape' || !this.unfolded || event.defaultPrevented) {
24
24
  return;
25
25
  }
26
26
  event.preventDefault();
27
- this.unfolded = false;
28
- void this.updateComplete.then(() => this.partNamed('toggle')?.focus());
27
+ if (this.requestOpen(false)) {
28
+ void this.updateComplete.then(() => this.partNamed('toggle')?.focus());
29
+ }
30
+ };
31
+ this.handleFocusOut = (event) => {
32
+ const next = event.relatedTarget;
33
+ if (this.unfolded && next instanceof Node && !deepContains(this, next)) {
34
+ this.requestOpen(false);
35
+ }
29
36
  };
37
+ this.handleOutsidePress = (event) => {
38
+ if (this.unfolded && !event.composedPath().includes(this)) {
39
+ this.requestOpen(false);
40
+ }
41
+ };
42
+ this.pressing = false;
30
43
  this.away = false;
31
44
  this.lastY = 0;
32
45
  this.handleFocusIn = () => {
33
46
  this.hide(false);
34
47
  };
35
- this.menuLabel = 'Menu';
36
48
  this.landmark = 'banner';
37
49
  this.scrolled = false;
38
50
  this.handleScroll = () => {
@@ -49,6 +61,9 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
49
61
  };
50
62
  }
51
63
  static { this.words = 'helix-app-header'; }
64
+ get unfolded() {
65
+ return this.folded && this.open;
66
+ }
52
67
  watchWidth() {
53
68
  if (!this.foldBelow || typeof ResizeObserver === 'undefined') {
54
69
  this.widths?.disconnect();
@@ -71,7 +86,22 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
71
86
  const folded = this.foldBelow !== undefined && width < this.foldBelow;
72
87
  if (folded !== this.folded) {
73
88
  this.folded = folded;
74
- this.unfolded = false;
89
+ if (!folded && this.open) {
90
+ this.open = false;
91
+ }
92
+ }
93
+ }
94
+ watchPresses() {
95
+ const want = this.unfolded && this.isConnected;
96
+ if (want === this.pressing) {
97
+ return;
98
+ }
99
+ this.pressing = want;
100
+ if (want) {
101
+ document.addEventListener('pointerdown', this.handleOutsidePress, true);
102
+ }
103
+ else {
104
+ document.removeEventListener('pointerdown', this.handleOutsidePress, true);
75
105
  }
76
106
  }
77
107
  hide(away) {
@@ -119,7 +149,7 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
119
149
  super.willUpdate(changed);
120
150
  hostAria(this.internals, { role: this.landmark });
121
151
  this.toggleState('folded', this.folded);
122
- this.toggleState('open', this.folded && this.unfolded);
152
+ this.toggleState('open', this.unfolded);
123
153
  }
124
154
  connectedCallback() {
125
155
  super.connectedCallback();
@@ -131,9 +161,11 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
131
161
  });
132
162
  this.listen();
133
163
  this.addEventListener('focusin', this.handleFocusIn);
164
+ this.addEventListener('focusout', this.handleFocusOut);
134
165
  this.addEventListener('keydown', this.handleKeydown);
135
166
  if (this.hasUpdated) {
136
167
  this.watchWidth();
168
+ this.watchPresses();
137
169
  }
138
170
  }
139
171
  readLandmark() {
@@ -145,9 +177,11 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
145
177
  this.source?.removeEventListener('scroll', this.handleScroll);
146
178
  this.source = undefined;
147
179
  this.removeEventListener('focusin', this.handleFocusIn);
180
+ this.removeEventListener('focusout', this.handleFocusOut);
148
181
  this.removeEventListener('keydown', this.handleKeydown);
149
182
  this.widths?.disconnect();
150
183
  this.widths = undefined;
184
+ this.watchPresses();
151
185
  }
152
186
  firstUpdated(changed) {
153
187
  super.firstUpdated(changed);
@@ -170,6 +204,7 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
170
204
  if (changed.has('hideOnScroll') && !this.hideOnScroll) {
171
205
  this.hide(false);
172
206
  }
207
+ this.watchPresses();
173
208
  }
174
209
  renderMenuIcon() {
175
210
  return html `<slot name="menu-icon">
@@ -179,22 +214,26 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
179
214
  </slot>`;
180
215
  }
181
216
  render() {
217
+ const toggle = this.folded
218
+ ? html `<button
219
+ part="toggle"
220
+ type="button"
221
+ aria-controls="content"
222
+ aria-expanded=${this.unfolded ? 'true' : 'false'}
223
+ aria-label=${this.menuLabel}
224
+ @click=${this.handleToggle}
225
+ >
226
+ ${this.renderMenuIcon()}
227
+ </button>`
228
+ : nothing;
229
+ const first = this.togglePlacement === 'start';
230
+ const content = html `<div part="content" id="content" ?hidden=${this.folded && !this.unfolded}><slot></slot></div>`;
182
231
  return html `
183
232
  <div part="row">
184
- <div part="start"><slot name="start"></slot></div>
185
- <div part="content" id="content" ?hidden=${this.folded && !this.unfolded}><slot></slot></div>
186
- <div part="end"><slot name="end"></slot>${this.folded
187
- ? html `<button
188
- part="toggle"
189
- type="button"
190
- aria-controls="content"
191
- aria-expanded=${this.unfolded ? 'true' : 'false'}
192
- aria-label=${this.menuLabel}
193
- @click=${this.handleToggle}
194
- >
195
- ${this.renderMenuIcon()}
196
- </button>`
197
- : nothing}</div>
233
+ <div part="start">${first ? toggle : nothing}<slot name="start"></slot></div>
234
+ ${this.folded ? nothing : content}
235
+ <div part="end"><slot name="end"></slot>${first ? nothing : toggle}</div>
236
+ ${this.folded ? content : nothing}
198
237
  </div>
199
238
  `;
200
239
  }
@@ -208,19 +247,19 @@ __decorate([
208
247
  __decorate([
209
248
  property({ attribute: 'hide-on-scroll', type: Boolean })
210
249
  ], HelixHeadlessAppHeader.prototype, "hideOnScroll", void 0);
211
- __decorate([
212
- state()
213
- ], HelixHeadlessAppHeader.prototype, "folded", void 0);
214
- __decorate([
215
- state()
216
- ], HelixHeadlessAppHeader.prototype, "unfolded", void 0);
217
250
  __decorate([
218
251
  words,
219
252
  property({ attribute: 'menu-label' })
220
253
  ], HelixHeadlessAppHeader.prototype, "menuLabel", void 0);
254
+ __decorate([
255
+ state()
256
+ ], HelixHeadlessAppHeader.prototype, "folded", void 0);
221
257
  __decorate([
222
258
  property()
223
259
  ], HelixHeadlessAppHeader.prototype, "scroller", void 0);
260
+ __decorate([
261
+ property({ attribute: 'toggle-placement', reflect: true })
262
+ ], HelixHeadlessAppHeader.prototype, "togglePlacement", void 0);
224
263
  HelixHeadlessAppHeader = __decorate([
225
264
  customElement('helix-headless-app-header')
226
265
  ], HelixHeadlessAppHeader);
@@ -57,6 +57,7 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
57
57
  * @csspart text - The button's words, which the styled button trims to their capitals and baseline so the letters sit at the middle in any font.
58
58
  *
59
59
  * @cssstate pending - The button is pending: `pending` is set.
60
+ * @cssstate pressed - A Button Group in a selection mode has this button pressed.
60
61
  */
61
62
  export declare class HelixHeadlessButton extends HelixFormElement {
62
63
  /**
@@ -206,6 +207,13 @@ export declare class HelixHeadlessButton extends HelixFormElement {
206
207
  * reported collapsed and expanded and no popup.
207
208
  */
208
209
  popup?: 'dialog' | 'grid' | 'listbox' | 'menu' | 'tree';
210
+ /**
211
+ * @internal Set by a Button Group in a selection mode: whether the group has
212
+ * this button pressed. Recorded as `:state(pressed)`, so a page can style a
213
+ * pressed button from its own element. The group says it to screen readers
214
+ * on the control.
215
+ */
216
+ pressed?: boolean | undefined;
209
217
  /**
210
218
  * The status region, rendered from the first time the button is pending, so
211
219
  * a button that never waits carries no live region.
@@ -140,6 +140,7 @@ let HelixHeadlessButton = class HelixHeadlessButton extends HelixFormElement {
140
140
  willUpdate(changed) {
141
141
  super.willUpdate(changed);
142
142
  this.toggleState('pending', this.pending);
143
+ this.toggleState('pressed', this.pressed === true);
143
144
  }
144
145
  updated(changed) {
145
146
  super.updated(changed);
@@ -238,6 +239,9 @@ __decorate([
238
239
  __decorate([
239
240
  property({ attribute: false })
240
241
  ], HelixHeadlessButton.prototype, "popup", void 0);
242
+ __decorate([
243
+ property({ attribute: false })
244
+ ], HelixHeadlessButton.prototype, "pressed", void 0);
241
245
  __decorate([
242
246
  property()
243
247
  ], HelixHeadlessButton.prototype, "type", void 0);
@@ -16,11 +16,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
16
16
  * appearance and size.
17
17
  *
18
18
  * **Buttons that stay pressed.** With `selection-mode`, each button with a
19
- * `value` is a toggle button: a press presses or releases it, and it says so
20
- * with `aria-pressed`. `single` keeps one pressed at a time, and a press on the
21
- * pressed one releases it. `multiple` lets any number be pressed. `value` is
22
- * the pressed values, comma separated. A press sends a cancelable
23
- * `helix-before-change` with the value it would leave, then `change`.
19
+ * `value` is a toggle button: a press presses or releases it. `multiple` lets
20
+ * any number be pressed, and each button says so with `aria-pressed`. `single`
21
+ * keeps one pressed at a time: the group is a `radiogroup` and each button a
22
+ * `radio` that says so with `aria-checked`, as React Aria's toggle button
23
+ * group is. A press on the pressed one releases it, unless
24
+ * `disallow-empty-selection` keeps one pressed. `value` is the pressed values,
25
+ * comma separated. A press sends a cancelable `helix-before-change` with the
26
+ * value it would leave, then `change`. A pressed Helix button has
27
+ * `:state(pressed)` on its own element.
24
28
  *
25
29
  * The pressed state is written on the control inside each button's shadow
26
30
  * root, so the markup you rendered is never changed and a framework hydrating
@@ -48,10 +52,10 @@ import { type PropertyValues, type TemplateResult } from 'lit';
48
52
  * | Space, Enter | Presses or releases the focused button. |
49
53
  *
50
54
  * Left and Right swap on a right-to-left page, and a disabled button is
51
- * passed over. In a selection mode the group is a `toolbar`, with
55
+ * passed over. Under `multiple` the group is a `toolbar`, with
52
56
  * `aria-orientation` under `orientation="vertical"`. Inside a toolbar it stays
53
57
  * a named `group`, and the toolbar's own arrow keys walk its buttons, so a
54
- * reader meets one toolbar.
58
+ * reader meets one toolbar. Under `single` it is a `radiogroup` either way.
55
59
  *
56
60
  * @since 0.1.0
57
61
  * @status stable
@@ -69,6 +73,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
69
73
  * @cssstate pressed - While a group with `selection-mode` has a button pressed.
70
74
  */
71
75
  export declare class HelixHeadlessButtonGroup extends HelixElement {
76
+ /**
77
+ * Keeps at least one button pressed once one is, as React Aria's
78
+ * `disallowEmptySelection`: a press that would leave none pressed is
79
+ * refused.
80
+ */
81
+ disallowEmptySelection: boolean;
72
82
  /**
73
83
  * The group's name, read by screen readers.
74
84
  *
@@ -172,6 +182,9 @@ export declare class HelixHeadlessButtonGroup extends HelixElement {
172
182
  * Says on a button's control whether it is pressed, or that it is no toggle
173
183
  * at all where `pressed` is undefined. A styled tier draws the look here.
174
184
  *
185
+ * Under `single` the control is a `radio` that says so with `aria-checked`,
186
+ * and under `multiple` a toggle button that says so with `aria-pressed`.
187
+ *
175
188
  * @param control The focusable control inside the button's shadow root.
176
189
  * @param pressed Whether the button is pressed, or undefined where it is not a toggle.
177
190
  */
@@ -12,6 +12,7 @@ const isGroupButton = (child) => child instanceof HTMLElement && 'focusable' in
12
12
  let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixElement {
13
13
  constructor() {
14
14
  super(...arguments);
15
+ this.disallowEmptySelection = false;
15
16
  this.label = '';
16
17
  this.value = '';
17
18
  this.lastFocused = undefined;
@@ -21,8 +22,12 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
21
22
  const mode = this.selectionMode;
22
23
  const button = this.buttons.find((one) => event.composedPath().includes(one));
23
24
  if (mode && button && button.value !== '') {
24
- requestValue(this, pressedValue(mode, this.value, button.value), (next) => {
25
- this.value = next;
25
+ const next = pressedValue(mode, this.value, button.value);
26
+ if (this.disallowEmptySelection && commaList(next).length === 0) {
27
+ return;
28
+ }
29
+ requestValue(this, next, (value) => {
30
+ this.value = value;
26
31
  });
27
32
  }
28
33
  };
@@ -107,17 +112,30 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
107
112
  const control = button.shadowRoot?.querySelector('button');
108
113
  if (control && !button.hasAttribute('aria-pressed')) {
109
114
  const toggle = this.told.has(button) && button.value !== '';
110
- this.markPressed(control, toggle ? this.pressed(button) : undefined);
115
+ const pressed = toggle ? this.pressed(button) : undefined;
116
+ if ('pressed' in button && button.pressed !== pressed) {
117
+ button.pressed = pressed;
118
+ }
119
+ this.markPressed(control, pressed);
111
120
  }
112
121
  });
113
122
  }
114
123
  markPressed(control, pressed) {
115
- if (pressed === undefined) {
124
+ const radio = pressed !== undefined && this.selectionMode === 'single';
125
+ if (pressed === undefined || radio) {
116
126
  control.removeAttribute('aria-pressed');
117
127
  }
118
128
  else {
119
129
  control.setAttribute('aria-pressed', String(pressed));
120
130
  }
131
+ if (radio) {
132
+ control.setAttribute('role', 'radio');
133
+ control.setAttribute('aria-checked', String(pressed));
134
+ }
135
+ else {
136
+ control.removeAttribute('role');
137
+ control.removeAttribute('aria-checked');
138
+ }
121
139
  control.style.setProperty('--button-group-pressed', pressed === undefined ? '' : pressed ? '1' : '0');
122
140
  }
123
141
  connectedCallback() {
@@ -143,7 +161,11 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
143
161
  willUpdate(changed) {
144
162
  super.willUpdate(changed);
145
163
  const toolbar = this.roving;
146
- hostAria(this.internals, { label: this.label, role: toolbar ? 'toolbar' : 'group' });
164
+ const radios = this.selectionMode === 'single';
165
+ hostAria(this.internals, {
166
+ label: this.label,
167
+ role: radios ? 'radiogroup' : toolbar ? 'toolbar' : 'group',
168
+ });
147
169
  this.internals.ariaOrientation = toolbar ? (this.orientation ?? null) : null;
148
170
  this.toggleState('pressed', Boolean(this.selectionMode) && commaList(this.value).length > 0);
149
171
  }
@@ -159,6 +181,9 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
159
181
  return html `<slot></slot>`;
160
182
  }
161
183
  };
184
+ __decorate([
185
+ property({ attribute: 'disallow-empty-selection', type: Boolean })
186
+ ], HelixHeadlessButtonGroup.prototype, "disallowEmptySelection", void 0);
162
187
  __decorate([
163
188
  property()
164
189
  ], HelixHeadlessButtonGroup.prototype, "label", void 0);