@fusion.dev/helix-headless 0.149.1 → 0.150.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 (35) hide show
  1. package/custom-elements.json +471 -45
  2. package/dist/button/root/button-root.d.ts +5 -0
  3. package/dist/button/root/button-root.js +3 -0
  4. package/dist/color-input/root/color-input-root.d.ts +1 -0
  5. package/dist/color-picker/root/color-picker-root.js +1 -0
  6. package/dist/combobox/root/combobox-root.d.ts +12 -1
  7. package/dist/combobox/root/combobox-root.js +8 -0
  8. package/dist/copy-button/root/copy-button-root.d.ts +4 -3
  9. package/dist/copy-button/root/copy-button-root.js +5 -1
  10. package/dist/date-input/root/date-input-root.d.ts +1 -0
  11. package/dist/element-events.generated.js +1 -1
  12. package/dist/filter-input/chip/filter-chip.d.ts +2 -1
  13. package/dist/filter-input/root/filter-input-root.d.ts +5 -0
  14. package/dist/filter-input/root/filter-input-root.js +7 -0
  15. package/dist/number-input/root/number-input-root.d.ts +6 -0
  16. package/dist/number-input/root/number-input-root.js +14 -3
  17. package/dist/option/root/option-root.d.ts +17 -8
  18. package/dist/option/root/option-root.js +18 -4
  19. package/dist/radio/group/radio-group.d.ts +8 -1
  20. package/dist/radio/group/radio-group.js +15 -1
  21. package/dist/slider/root/slider-root.d.ts +8 -0
  22. package/dist/slider/root/slider-root.js +6 -3
  23. package/dist/tag/group/tag-group.d.ts +22 -1
  24. package/dist/tag/group/tag-group.js +63 -3
  25. package/dist/tag/root/tag-root.d.ts +9 -0
  26. package/dist/tag/root/tag-root.js +5 -1
  27. package/dist/time-input/root/time-input-root.d.ts +1 -0
  28. package/dist/toggle/group/toggle-group.d.ts +6 -0
  29. package/dist/toggle/group/toggle-group.js +16 -1
  30. package/dist/toggle/root/toggle-root.d.ts +21 -3
  31. package/dist/toggle/root/toggle-root.js +22 -5
  32. package/lint.json +1 -1
  33. package/package.json +5 -5
  34. package/types/jsx.d.ts +160 -58
  35. package/types/vue.d.ts +28 -16
@@ -70,6 +70,15 @@ export declare class HelixHeadlessTag extends HelixHeadlessTag_base {
70
70
  * tag's words in it, so a translation puts them where its language does.
71
71
  */
72
72
  dismissLabel: string;
73
+ /**
74
+ * Whether the remove button is a tab stop of its own, outside a `Tag.Group`.
75
+ *
76
+ * A field that holds tags as its tokens, such as a `multiple` Combobox,
77
+ * writes `false`: the field's text is its one tab stop, and the arrow keys
78
+ * move from the text to the tags. No attribute: it is coordination between
79
+ * elements, as `focusable` is.
80
+ */
81
+ dismissTabStop: boolean;
73
82
  /**
74
83
  * Whether the tag holds its group's tab stop.
75
84
  *
@@ -12,6 +12,7 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
12
12
  super(...arguments);
13
13
  this.disabled = false;
14
14
  this.dismissLabel = 'Remove {label}';
15
+ this.dismissTabStop = true;
15
16
  this.href = '';
16
17
  this.value = '';
17
18
  }
@@ -87,7 +88,7 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
87
88
  label: this.removeName,
88
89
  mark: html `<slot name="dismiss-icon">${drawDismissGlyph()}</slot>`,
89
90
  onDismiss: () => this.dismiss(),
90
- tabindex: this.grouped ? '-1' : undefined,
91
+ tabindex: this.grouped || !this.dismissTabStop ? '-1' : undefined,
91
92
  });
92
93
  }
93
94
  };
@@ -98,6 +99,9 @@ __decorate([
98
99
  words,
99
100
  property({ attribute: 'dismiss-label' })
100
101
  ], HelixHeadlessTag.prototype, "dismissLabel", void 0);
102
+ __decorate([
103
+ property({ attribute: false })
104
+ ], HelixHeadlessTag.prototype, "dismissTabStop", void 0);
101
105
  __decorate([
102
106
  property({ attribute: false })
103
107
  ], HelixHeadlessTag.prototype, "focusable", void 0);
@@ -42,6 +42,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
42
42
  *
43
43
  * @slot feedback - The message that says what is wrong with the value.
44
44
  * @slot hint - Help under the field.
45
+ * @slot label - The label, for content richer than the `label` text.
45
46
  * @slot panel-footer - Content in the panel under the times, such as a note or an action.
46
47
  * @slot panel-header - Content in the panel above the times, such as a heading or a shortcut.
47
48
  * @slot trigger - The icon on the field that shows a list opens.
@@ -191,7 +191,13 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
191
191
  private readonly indicator;
192
192
  protected readonly keys: KeyboardNavigationController;
193
193
  protected readonly cascade: DisabledCascadeController;
194
+ /**
195
+ * The toggles, in light-DOM order, at any depth, so a toggle inside a layout
196
+ * wrapper of yours is one of the group's. A toggle held by a group nested
197
+ * inside this one is that group's.
198
+ */
194
199
  private get items();
200
+ private observer;
195
201
  private readonly handleSelection;
196
202
  /**
197
203
  * Whether the group holds an answer the form can submit: a value an item
@@ -101,7 +101,10 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
101
101
  null);
102
102
  }
103
103
  get items() {
104
- return [...(this.children ?? [])].filter((child) => child instanceof HelixHeadlessToggle);
104
+ if (this.children === undefined) {
105
+ return [];
106
+ }
107
+ return [...this.querySelectorAll('*')].filter((element) => element instanceof HelixHeadlessToggle && nearestGroup(element) === this);
105
108
  }
106
109
  get answered() {
107
110
  if (this.value === '') {
@@ -118,11 +121,15 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
118
121
  super.connectedCallback();
119
122
  this.addEventListener('helix-select', this.handleSelection);
120
123
  this.addEventListener('focusin', this.noteFocus);
124
+ this.observer = new MutationObserver(this.handleItems);
125
+ this.observer.observe(this, { childList: true, subtree: true });
121
126
  }
122
127
  disconnectedCallback() {
123
128
  super.disconnectedCallback();
124
129
  this.removeEventListener('helix-select', this.handleSelection);
125
130
  this.removeEventListener('focusin', this.noteFocus);
131
+ this.observer?.disconnect();
132
+ this.observer = undefined;
126
133
  }
127
134
  firstUpdated(changed) {
128
135
  super.firstUpdated(changed);
@@ -146,6 +153,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
146
153
  this.adopt();
147
154
  hostAria(this.internals, { label: this.label, role: this.groupRole });
148
155
  this.internals.ariaOrientation = this.orientation ?? null;
156
+ this.internals.ariaRequired = this.required && !this.multiple ? 'true' : null;
149
157
  if (!this.answered) {
150
158
  this.internals.setFormValue(null);
151
159
  }
@@ -203,3 +211,10 @@ HelixHeadlessToggleGroup = __decorate([
203
211
  customElement('helix-headless-toggle-group')
204
212
  ], HelixHeadlessToggleGroup);
205
213
  export { HelixHeadlessToggleGroup };
214
+ function nearestGroup(toggle) {
215
+ let parent = toggle.parentElement;
216
+ while (parent && !(parent instanceof HelixHeadlessToggleGroup)) {
217
+ parent = parent.parentElement;
218
+ }
219
+ return parent;
220
+ }
@@ -2,13 +2,18 @@ import { HelixElement, HostClickController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
5
- * @summary One option in a toggle group.
5
+ * @summary One option in a toggle group, or a toggle button on its own.
6
6
  *
7
7
  * @description
8
8
  * Renders a `<button role="radio">` for use as a form control. In a group with
9
9
  * `selection-mode="multiple"` it is a `<button>` that says whether it is
10
10
  * pressed with `aria-pressed`.
11
11
  *
12
+ * **On its own, outside a `Toggle.Group`, it is a toggle button**, as React
13
+ * Aria's `ToggleButton`: a `<button>` with `aria-pressed`. A press, Enter or
14
+ * Space sends `helix-select`, which you can cancel, then flips `selected` and
15
+ * sends `change`. Write `selected` in the markup to start it pressed.
16
+ *
12
17
  * Do not use it for navigation. For navigation, use links marked with
13
18
  * `aria-current` and styled by the `data-navigation-item` recipe in the base
14
19
  * layer. The recipe has the same look, and links are not announced as choices.
@@ -21,6 +26,7 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
21
26
  *
22
27
  * @slot - The item's label.
23
28
  *
29
+ * @event {Event} change - Fired after a toggle button on its own is pressed or released.
24
30
  * @event {HelixSelectEvent} helix-select - Fired when the item is chosen. Cancelable: call `preventDefault()` to keep the current choice.
25
31
  *
26
32
  * @csspart item - The radio button.
@@ -61,8 +67,8 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
61
67
  get defaultSelected(): boolean;
62
68
  set defaultSelected(next: boolean);
63
69
  /**
64
- * Whether the item holds the group's tab stop. Unset, the chosen item holds
65
- * it.
70
+ * Whether the item holds the group's tab stop. Unset, no group holds the
71
+ * item, and it is a toggle button with a tab stop of its own.
66
72
  *
67
73
  * Written by the group on every item: `true` for the chosen item, or for the
68
74
  * first item that can be chosen while there is no selection to carry the
@@ -79,6 +85,18 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
79
85
  * Written by the group, like `selected`, so no attribute.
80
86
  */
81
87
  pressed?: boolean | undefined;
88
+ /**
89
+ * Whether no `Toggle.Group` holds the item, which makes it a toggle button
90
+ * of its own. A group writes `focusable` on every item it holds. A server,
91
+ * and the render that hydrates its markup, cannot tell, and draw the item a
92
+ * group holds.
93
+ */
94
+ private get alone();
95
+ /**
96
+ * Chooses the item in its group, or, on its own, flips `selected` once
97
+ * `helix-select` stands and sends `change`.
98
+ */
99
+ select(): boolean;
82
100
  protected willUpdate(changed: PropertyValues): void;
83
101
  /**
84
102
  * Asks for the render that follows a hydration, so the stop stops being every
@@ -18,23 +18,40 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
18
18
  set defaultSelected(next) {
19
19
  this.toggleAttribute('selected', next);
20
20
  }
21
+ get alone() {
22
+ return this.focusable === undefined && this.childrenKnown;
23
+ }
24
+ select() {
25
+ if (!this.alone) {
26
+ return super.select();
27
+ }
28
+ if (!super.select()) {
29
+ return false;
30
+ }
31
+ this.selected = !this.selected;
32
+ this.dispatchEvent(new Event('change', { bubbles: true }));
33
+ return true;
34
+ }
21
35
  willUpdate(changed) {
22
36
  super.willUpdate(changed);
23
- this.toggleState('pressed', this.pressed === true);
37
+ this.toggleState('pressed', this.alone ? this.selected : this.pressed === true);
24
38
  }
25
39
  firstUpdated(changed) {
26
40
  super.firstUpdated(changed);
27
41
  this.finishHydration();
28
42
  }
29
43
  render() {
44
+ const alone = this.alone;
45
+ const radio = !alone && this.pressed === undefined;
46
+ const pressed = alone ? this.selected : this.pressed;
30
47
  return html `<button
31
48
  part="item"
32
49
  type="button"
33
- role=${this.pressed === undefined ? 'radio' : nothing}
34
- aria-checked=${this.pressed === undefined ? String(this.selected) : nothing}
35
- aria-pressed=${this.pressed === undefined ? nothing : String(this.pressed)}
50
+ role=${radio ? 'radio' : nothing}
51
+ aria-checked=${radio ? String(this.selected) : nothing}
52
+ aria-pressed=${radio || pressed === undefined ? nothing : String(pressed)}
36
53
  ?disabled=${this.disabled}
37
- tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
54
+ tabindex=${alone || (this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
38
55
  @click=${() => this.select()}
39
56
  ><slot></slot></button>`;
40
57
  }