@fusion.dev/helix-headless 0.154.0 → 0.156.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 (104) hide show
  1. package/custom-elements.json +3213 -179
  2. package/dist/accordion/item/accordion-item.d.ts +15 -0
  3. package/dist/accordion/item/accordion-item.js +12 -1
  4. package/dist/accordion/root/accordion-root.d.ts +2 -1
  5. package/dist/accordion/root/accordion-root.js +1 -0
  6. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
  7. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
  8. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
  9. package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
  10. package/dist/button-group/root/button-group-root.d.ts +2 -0
  11. package/dist/button-group/root/button-group-root.js +1 -0
  12. package/dist/canvas/node/canvas-node.js +1 -1
  13. package/dist/canvas/root/canvas-root.d.ts +17 -2
  14. package/dist/canvas/root/canvas-root.js +23 -6
  15. package/dist/chat/composer/chat-composer.js +1 -1
  16. package/dist/chat/root/chat-root.d.ts +14 -1
  17. package/dist/chat/root/chat-root.js +14 -4
  18. package/dist/color-input/root/color-input-root.d.ts +9 -3
  19. package/dist/color-input/root/color-input-root.js +3 -1
  20. package/dist/color-picker/channels.d.ts +5 -3
  21. package/dist/color-picker/channels.js +79 -21
  22. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  23. package/dist/color-picker/root/color-picker-root.js +75 -13
  24. package/dist/combobox/root/combobox-root.d.ts +10 -0
  25. package/dist/combobox/root/combobox-root.js +18 -3
  26. package/dist/command/root/command-root.d.ts +116 -1
  27. package/dist/command/root/command-root.js +124 -15
  28. package/dist/data-table/root/data-table-root.d.ts +25 -0
  29. package/dist/data-table/root/data-table-root.js +13 -3
  30. package/dist/date-input/root/date-input-root.d.ts +33 -1
  31. package/dist/date-input/root/date-input-root.js +9 -2
  32. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  33. package/dist/date-picker/root/date-picker-root.js +54 -31
  34. package/dist/element-events.generated.js +3 -1
  35. package/dist/graph/root/graph-root.d.ts +13 -0
  36. package/dist/graph/root/graph-root.js +16 -0
  37. package/dist/list/root/list-root.d.ts +74 -1
  38. package/dist/list/root/list-root.js +92 -4
  39. package/dist/list/selection.js +3 -3
  40. package/dist/map/root/map-root.d.ts +6 -0
  41. package/dist/map/root/map-root.js +8 -1
  42. package/dist/navigation/root/navigation-root.d.ts +26 -0
  43. package/dist/navigation/root/navigation-root.js +58 -1
  44. package/dist/number-input/root/number-input-root.d.ts +63 -6
  45. package/dist/number-input/root/number-input-root.js +96 -11
  46. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  47. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  48. package/dist/option/root/option-root.d.ts +32 -0
  49. package/dist/option/root/option-root.js +44 -5
  50. package/dist/outline/root/outline-root.d.ts +6 -2
  51. package/dist/outline/root/outline-root.js +7 -3
  52. package/dist/select/root/select-root.d.ts +29 -3
  53. package/dist/select/root/select-root.js +34 -9
  54. package/dist/slider/root/slider-root.d.ts +71 -9
  55. package/dist/slider/root/slider-root.js +129 -42
  56. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  57. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  58. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  59. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  60. package/dist/splitter/root/splitter-root.d.ts +22 -0
  61. package/dist/splitter/root/splitter-root.js +36 -7
  62. package/dist/steps/reach.d.ts +5 -1
  63. package/dist/steps/reach.js +2 -2
  64. package/dist/steps/root/steps-root.d.ts +37 -2
  65. package/dist/steps/root/steps-root.js +49 -7
  66. package/dist/steps/step/step.d.ts +6 -0
  67. package/dist/steps/step/step.js +4 -0
  68. package/dist/table/root/table-root.d.ts +20 -0
  69. package/dist/table/root/table-root.js +13 -3
  70. package/dist/tabs/root/tabs-root.d.ts +37 -0
  71. package/dist/tabs/root/tabs-root.js +92 -2
  72. package/dist/tabs/tab/tab.d.ts +38 -2
  73. package/dist/tabs/tab/tab.js +45 -4
  74. package/dist/tag/group/tag-group.d.ts +29 -3
  75. package/dist/tag/group/tag-group.js +42 -11
  76. package/dist/time-input/root/time-input-root.d.ts +32 -1
  77. package/dist/time-input/root/time-input-root.js +9 -2
  78. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  79. package/dist/time-picker/root/time-picker-root.js +58 -7
  80. package/dist/timeline/item/timeline-item.d.ts +25 -0
  81. package/dist/timeline/item/timeline-item.js +27 -2
  82. package/dist/toggle/root/toggle-root.d.ts +2 -0
  83. package/dist/toggle/root/toggle-root.js +1 -1
  84. package/dist/tree/item/tree-item.d.ts +10 -0
  85. package/dist/tree/item/tree-item.js +12 -0
  86. package/dist/tree/root/tree-root.d.ts +40 -1
  87. package/dist/tree/root/tree-root.js +120 -4
  88. package/dist/tree/selection.d.ts +6 -0
  89. package/dist/tree/selection.js +11 -0
  90. package/dist/video/root/keys.d.ts +6 -2
  91. package/dist/video/root/keys.js +18 -1
  92. package/dist/video/root/video-root.d.ts +60 -1
  93. package/dist/video/root/video-root.js +70 -2
  94. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  95. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  96. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  97. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  98. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  99. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  100. package/lint.json +1 -1
  101. package/package.json +5 -5
  102. package/types/jsx.d.ts +3885 -383
  103. package/types/vue.d.ts +1045 -107
  104. package/types/words.d.ts +146 -1
@@ -26,6 +26,9 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
26
26
  this.status = new StatusController(this);
27
27
  }
28
28
  static { this.words = 'helix-step'; }
29
+ focus(options) {
30
+ this.partNamed('label')?.focus(options);
31
+ }
29
32
  select() {
30
33
  if (this.disabled || this.locked) {
31
34
  return;
@@ -44,6 +47,7 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
44
47
  super.willUpdate(changed);
45
48
  hostAria(this.internals, { role: 'listitem' });
46
49
  this.toggleState('invalid', this.invalid);
50
+ this.toggleState('locked', this.locked);
47
51
  }
48
52
  updated(changed) {
49
53
  super.updated(changed);
@@ -96,6 +96,11 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
96
96
  * | A letter or a word | Moves to the next row whose first cell starts with what you typed. |
97
97
  * | Tab | Leaves the table. |
98
98
  *
99
+ * When the rows did not load, set `error-text`, such as "The orders did not
100
+ * load", and put a button that tries again in the `error` slot. The line shows
101
+ * after the rows in place of the empty and loading lines, the live region says
102
+ * it, and the table takes the `error` state. Clear it once you try again.
103
+ *
99
104
  * The CSS custom properties listed for this component are written by it and
100
105
  * read by the stylesheet that draws it. Do not set them. Each holds a number
101
106
  * the component computes and CSS cannot, so the stylesheet can draw the table
@@ -109,6 +114,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
109
114
  *
110
115
  * @slot - The table's rows.
111
116
  * @slot empty - Content shown when there are no rows and none are loading, such as a picture and a link. Falls back to `empty-text`, which the live region still says.
117
+ * @slot error - Content shown while `error-text` is set, such as a button that tries again. Falls back to `error-text`, which the live region still says.
112
118
  * @slot heading - Words that name the table, shown above it, such as a heading.
113
119
  * @slot loading - Content shown while the rows load, such as a spinner. Falls back to `loading-text`, which the live region still says.
114
120
  *
@@ -132,6 +138,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
132
138
  * @csspart checkbox-mark-indeterminate - Added to `checkbox-mark` while only some rows are selected.
133
139
  * @csspart drag-preview - A copy of the row a pointer drags by its grip, which follows the pointer in the top layer until the row is let go, in a `reorderable` table. Hidden from assistive technology.
134
140
  * @csspart empty - The text shown when there are no rows.
141
+ * @csspart error - The line shown after the rows while `error-text` is set, in place of the empty and loading text.
135
142
  * @csspart grid-end - An empty box after the rows, in a `grid` table. Tab focuses it on its way out of the table, so the key leaves from the end.
136
143
  * @csspart grid-start - An empty box before the rows, in a `grid` table. Shift with Tab focuses it on its way out of the table.
137
144
  * @csspart grip-head - The heading over the row grips, in a `reorderable` table.
@@ -142,6 +149,9 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
142
149
  * @csspart table - The grid.
143
150
  *
144
151
  * @cssproperty --table-template - The column track list the table works out from its columns. The component writes it inline on the element that reads it, so setting it has no effect. Set `columns` to change the tracks.
152
+ *
153
+ * @cssstate empty - While there are no rows and none are loading.
154
+ * @cssstate error - While `error-text` is set.
145
155
  */
146
156
  export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
147
157
  /**
@@ -214,6 +224,16 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
214
224
  descendingLabel: string;
215
225
  /** What screen readers hear when a held row is dropped, in a `reorderable` table. */
216
226
  droppedLabel: string;
227
+ /**
228
+ * The text shown and announced when the rows could not load. Set it when a
229
+ * fetch fails, and clear it once you try again. Empty, the default, shows
230
+ * nothing.
231
+ *
232
+ * While it is set, a line after the rows shows it, or your own content in the
233
+ * `error` slot, in place of the empty and loading lines, and the live region
234
+ * says it.
235
+ */
236
+ errorText: string;
217
237
  /**
218
238
  * The table's name where neither `label` nor an `aria-label` or
219
239
  * `aria-labelledby` on the host gives it one.
@@ -44,7 +44,7 @@ const SAMPLE = 200;
44
44
  const textOf = (element) => (element?.textContent ?? '').replace(/\s+/g, ' ').trim();
45
45
  const lengthOf = (element) => textOf(element).length;
46
46
  const numberOf = (value) => Number(value.replaceAll(',', ''));
47
- const STATUS = new Set(['empty', 'loading']);
47
+ const STATUS = new Set(['empty', 'error', 'loading']);
48
48
  const NAMED = new Set([...STATUS, 'heading']);
49
49
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
50
50
  constructor() {
@@ -57,6 +57,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
57
57
  this.defaultSort = '';
58
58
  this.descendingLabel = 'descending';
59
59
  this.droppedLabel = 'Dropped';
60
+ this.errorText = '';
60
61
  this.fallbackLabel = 'Table';
61
62
  this.grid = false;
62
63
  this.label = '';
@@ -496,6 +497,8 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
496
497
  }
497
498
  updated(changed) {
498
499
  super.updated(changed);
500
+ this.toggleState('error', this.errorText !== '');
501
+ this.toggleState('empty', !this.loading && !this.hasRows);
499
502
  this.assign();
500
503
  this.alignCells();
501
504
  this.markSorted();
@@ -561,8 +564,12 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
561
564
  this.handleSlotChange();
562
565
  }}
563
566
  ></slot>
564
- ${this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')}
565
- </div>${end}${this.renderLive(this.hasRows)}${this.reorder.drawDragPreview()}`;
567
+ ${this.errorText === ''
568
+ ? this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')
569
+ : html `<p part="error" role="row"><span role=${grid ? 'gridcell' : 'cell'}><slot name="error">${this.errorText}</slot></span></p>`}
570
+ </div>${end}${this.errorText === ''
571
+ ? this.renderLive(this.hasRows)
572
+ : html `<span part="live" role="status">${this.errorText}</span>`}${this.reorder.drawDragPreview()}`;
566
573
  }
567
574
  };
568
575
  __decorate([
@@ -590,6 +597,9 @@ __decorate([
590
597
  words,
591
598
  property({ attribute: 'dropped-label' })
592
599
  ], HelixHeadlessTable.prototype, "droppedLabel", void 0);
600
+ __decorate([
601
+ property({ attribute: 'error-text' })
602
+ ], HelixHeadlessTable.prototype, "errorText", void 0);
593
603
  __decorate([
594
604
  words,
595
605
  property({ attribute: 'fallback-label' })
@@ -50,6 +50,16 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
50
50
  * `active` says so, with `helix-activate`. Where the removed tab had focus,
51
51
  * focus moves to the tab now selected.
52
52
  *
53
+ * **More tabs than fit.** With `scroll-controls`, a strip that scrolls shows a
54
+ * button at each end it can scroll toward, as Web Awesome's tab group does,
55
+ * and a press scrolls it most of its own length. The buttons are for the
56
+ * pointer, so they are out of the tab order and hidden from screen readers;
57
+ * the arrow keys already reach every tab, and the strip scrolls the selected
58
+ * tab into view. A strip scrolls once a stylesheet gives the `tabs` part an
59
+ * `overflow`, as the styled tier does.
60
+ *
61
+ * A tab with `dismissible` can be closed: see `Tabs.Tab`.
62
+ *
53
63
  * The `indicator` part moves to the selected tab and carries its place and
54
64
  * size, so a mark of your own can be drawn there: style the part, or slot an
55
65
  * element into `indicator`, which sits inside the part and reads the same
@@ -74,6 +84,8 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
74
84
  * `--indicator-block-size`, and `data-placed` once it has been placed, for a mark you draw
75
85
  * and animate yourself.
76
86
  * @csspart panels - The panel container.
87
+ * @csspart scroll-end - The button that scrolls the strip toward its end, shown under `scroll-controls` while there is more that way.
88
+ * @csspart scroll-start - The button that scrolls the strip toward its start, shown under `scroll-controls` while there is more that way.
77
89
  * @csspart tabs - The tab list.
78
90
  */
79
91
  export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
@@ -109,6 +121,11 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
109
121
  * Optional, so the resting row writes no attribute onto the host.
110
122
  */
111
123
  orientation?: 'horizontal' | 'vertical';
124
+ /**
125
+ * Whether a strip with more tabs than fit shows a button at each end it can
126
+ * scroll toward.
127
+ */
128
+ scrollControls: boolean;
112
129
  /**
113
130
  * How the row of tabs is drawn.
114
131
  *
@@ -139,6 +156,17 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
139
156
  private get linked();
140
157
  private get panels();
141
158
  private readonly handleSelection;
159
+ /** Which ends the strip can scroll toward, under `scroll-controls`. */
160
+ private more;
161
+ /** Watches the strip's size for `scroll-controls`. */
162
+ private resizing;
163
+ /** Reads which ends of the strip have more tabs past them. */
164
+ private readonly measure;
165
+ /**
166
+ * Scrolls the strip most of its own length toward one end, smoothly unless
167
+ * the reader asked for less motion.
168
+ */
169
+ private scrollStrip;
142
170
  /** The values of the tabs the last pairing read, to tell a removed tab from one not yet here. */
143
171
  private held;
144
172
  /** Pushes the current value down to whichever tab and panel own it. */
@@ -155,6 +183,10 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
155
183
  connectedCallback(): void;
156
184
  disconnectedCallback(): void;
157
185
  protected firstUpdated(changed: PropertyValues): void;
186
+ /** Watches the strip's size while `scroll-controls` asks, and reads it. */
187
+ private watchStrip;
188
+ /** Brings the selected tab into the strip's view, and moves nothing else. */
189
+ private reveal;
158
190
  /**
159
191
  * The set changed: pair it up and measure it.
160
192
  *
@@ -172,6 +204,11 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
172
204
  */
173
205
  private readonly handleSlotChange;
174
206
  protected updated(changed: PropertyValues): void;
207
+ /**
208
+ * One of the two scroll buttons, pointing the way it scrolls. A caret along
209
+ * the line is mirrored on a right-to-left page, and one down a column is not.
210
+ */
211
+ private renderScroll;
175
212
  protected render(): TemplateResult;
176
213
  }
177
214
  declare global {
@@ -4,9 +4,11 @@ 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 { AriaForwardController, customElement, deepActiveElement, HelixActivateEvent, HelixElement, IndicatorController, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, deepActiveElement, HelixActivateEvent, HelixElement, IndicatorController, isRtl, KeyboardNavigationController, MixinControllerActive, scrollItemIntoView, whenHydrated, words, } from '@fusion.dev/helix-core';
8
+ import { caretDown, caretLeft, caretRight, caretUp } from '@fusion.dev/helix-icons';
8
9
  import { html, nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
10
+ import { property, state } from 'lit/decorators.js';
11
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
10
12
  import { HelixHeadlessTabPanel } from '../panel/tab-panel.js';
11
13
  import { HelixHeadlessTab } from '../tab/tab.js';
12
14
  let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(HelixElement) {
@@ -29,12 +31,32 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
29
31
  });
30
32
  this.label = 'Tabs';
31
33
  this.manual = false;
34
+ this.scrollControls = false;
32
35
  this.handleSelection = (event) => {
33
36
  if (event.defaultPrevented || !this.tabs.includes(event.target)) {
34
37
  return;
35
38
  }
36
39
  this.requestActive(event.detail.value);
37
40
  };
41
+ this.more = { end: false, start: false };
42
+ this.measure = () => {
43
+ const strip = this.partNamed('tabs');
44
+ if (!strip || !this.scrollControls) {
45
+ if (this.more.start || this.more.end) {
46
+ this.more = { end: false, start: false };
47
+ }
48
+ return;
49
+ }
50
+ const down = this.orientation === 'vertical';
51
+ const along = Math.abs(down ? strip.scrollTop : strip.scrollLeft);
52
+ const length = down ? strip.clientHeight : strip.clientWidth;
53
+ const whole = down ? strip.scrollHeight : strip.scrollWidth;
54
+ const start = along > 1;
55
+ const end = along + length < whole - 1;
56
+ if (start !== this.more.start || end !== this.more.end) {
57
+ this.more = { end, start };
58
+ }
59
+ };
38
60
  this.held = [];
39
61
  this.handleFocusIn = (event) => {
40
62
  const path = event.composedPath();
@@ -44,6 +66,7 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
44
66
  this.sync();
45
67
  this.indicator.place();
46
68
  this.refocus();
69
+ this.measure();
47
70
  };
48
71
  }
49
72
  static { this.words = 'helix-tabs'; }
@@ -56,6 +79,16 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
56
79
  get panels() {
57
80
  return [...this.children].filter((child) => child instanceof HelixHeadlessTabPanel);
58
81
  }
82
+ scrollStrip(toward) {
83
+ const strip = this.partNamed('tabs');
84
+ if (!strip) {
85
+ return;
86
+ }
87
+ const down = this.orientation === 'vertical';
88
+ const by = (down ? strip.clientHeight : strip.clientWidth) * 0.75 * toward;
89
+ const behavior = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
90
+ strip.scrollBy(down ? { behavior, top: by } : { behavior, left: isRtl(this) ? -by : by });
91
+ }
59
92
  sync() {
60
93
  const tabs = this.tabs;
61
94
  const declared = this.active === '' ? tabs.find((tab) => tab.selected) : undefined;
@@ -106,11 +139,16 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
106
139
  super.connectedCallback();
107
140
  this.addEventListener('helix-select', this.handleSelection);
108
141
  this.addEventListener('focusin', this.handleFocusIn);
142
+ if (this.hasUpdated) {
143
+ this.watchStrip();
144
+ }
109
145
  }
110
146
  disconnectedCallback() {
111
147
  super.disconnectedCallback();
112
148
  this.removeEventListener('helix-select', this.handleSelection);
113
149
  this.removeEventListener('focusin', this.handleFocusIn);
150
+ this.resizing?.disconnect();
151
+ this.resizing = undefined;
114
152
  }
115
153
  firstUpdated(changed) {
116
154
  super.firstUpdated(changed);
@@ -118,22 +156,68 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
118
156
  this.indicator.place();
119
157
  requestAnimationFrame(() => this.indicator.place());
120
158
  }
159
+ watchStrip() {
160
+ const strip = this.partNamed('tabs');
161
+ if (this.scrollControls && strip && !this.resizing) {
162
+ this.resizing = new ResizeObserver(this.measure);
163
+ this.resizing.observe(strip);
164
+ }
165
+ if (!this.scrollControls && this.resizing) {
166
+ this.resizing.disconnect();
167
+ this.resizing = undefined;
168
+ }
169
+ this.measure();
170
+ }
171
+ reveal() {
172
+ const strip = this.partNamed('tabs');
173
+ const tab = this.tabs.find((one) => one.selected);
174
+ if (strip && tab) {
175
+ scrollItemIntoView(tab, { padding: this.scrollControls ? 40 : 0, scroller: strip });
176
+ }
177
+ }
121
178
  updated(changed) {
122
179
  super.updated(changed);
123
180
  if (changed.has('active')) {
124
181
  this.sync();
182
+ if (changed.get('active') !== undefined) {
183
+ void this.updateComplete.then(() => this.reveal());
184
+ }
185
+ }
186
+ if (changed.has('scrollControls') || changed.has('orientation')) {
187
+ this.watchStrip();
125
188
  }
126
189
  this.indicator.place();
127
190
  }
191
+ renderScroll(toward) {
192
+ if (!this.scrollControls) {
193
+ return nothing;
194
+ }
195
+ const down = this.orientation === 'vertical';
196
+ const icon = down ? (toward > 0 ? caretDown : caretUp) : toward > 0 ? caretRight : caretLeft;
197
+ const glyph = html `<svg
198
+ data-mirror=${down ? nothing : ''}
199
+ aria-hidden="true"
200
+ fill="currentColor"
201
+ viewBox=${icon.viewBox}
202
+ >${unsafeSVG(icon.content)}</svg>`;
203
+ const shown = toward > 0 ? this.more.end : this.more.start;
204
+ const keepFocus = (event) => event.preventDefault();
205
+ return toward > 0
206
+ ? html `<button part="scroll-end" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(1)}>${glyph}</button>`
207
+ : html `<button part="scroll-start" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(-1)}>${glyph}</button>`;
208
+ }
128
209
  render() {
129
210
  return html `<div
130
211
  part="tabs"
131
212
  role="tablist"
132
213
  aria-label=${this.label}
133
214
  aria-orientation=${this.orientation === 'vertical' ? 'vertical' : nothing}
215
+ @scroll=${this.scrollControls ? this.measure : nothing}
134
216
  >
217
+ ${this.renderScroll(-1)}
135
218
  <slot @slotchange=${this.handleSlotChange}></slot>
136
219
  <span part="indicator" aria-hidden="true"><slot name="indicator"></slot></span>
220
+ ${this.renderScroll(1)}
137
221
  </div>
138
222
  <div part="panels"><slot name="panels" @slotchange=${this.handleSlotChange}></slot></div>`;
139
223
  }
@@ -148,9 +232,15 @@ __decorate([
148
232
  __decorate([
149
233
  property({ reflect: true })
150
234
  ], HelixHeadlessTabs.prototype, "orientation", void 0);
235
+ __decorate([
236
+ property({ attribute: 'scroll-controls', reflect: true, type: Boolean })
237
+ ], HelixHeadlessTabs.prototype, "scrollControls", void 0);
151
238
  __decorate([
152
239
  property({ reflect: true })
153
240
  ], HelixHeadlessTabs.prototype, "variant", void 0);
241
+ __decorate([
242
+ state()
243
+ ], HelixHeadlessTabs.prototype, "more", void 0);
154
244
  HelixHeadlessTabs = __decorate([
155
245
  customElement('helix-headless-tabs')
156
246
  ], HelixHeadlessTabs);
@@ -1,6 +1,6 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
3
- declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
2
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
+ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.dev/helix-core").DismissibleElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
5
5
  * @summary One tab in a tab list.
6
6
  *
@@ -12,6 +12,15 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
12
12
  * first, and canceling it keeps the browser from following, so a client-side
13
13
  * router follows it instead.
14
14
  *
15
+ * With `dismissible`, the tab can be closed, as Web Awesome's closable tab
16
+ * can: a press on its `dismiss` mark, or Delete or Backspace while the tab
17
+ * has focus, sends `helix-dismiss`. The tab does not remove itself, because
18
+ * your code owns the set, so remove the tab and its panel in your handler.
19
+ * Focus moves to the tab beside it, and the tab set selects a tab that is
20
+ * left.
21
+ * The tab says the key through `aria-keyshortcuts`, and the mark is for the
22
+ * pointer, hidden from screen readers.
23
+ *
15
24
  * @since 0.1.0
16
25
  * @status stable
17
26
  * @aliases Tab
@@ -19,13 +28,20 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
19
28
  *
20
29
  * @slot - The tab's label.
21
30
  *
31
+ * @event {HelixDismissEvent} helix-dismiss - A `dismissible` tab was asked to close. The tab does not remove itself, so remove it and its panel in response. Cancelable.
22
32
  * @event {HelixSelectEvent} helix-select - Fired when the tab is chosen. Cancelable: canceling it stops the change and leaves the decision to you.
23
33
  *
34
+ * @csspart dismiss - The mark a pointer presses to close a `dismissible` tab.
24
35
  * @csspart tab - The tab itself.
25
36
  *
26
37
  * @cssstate selected - While it is the tab selected.
27
38
  */
28
39
  export declare class HelixHeadlessTab extends HelixHeadlessTab_base {
40
+ /**
41
+ * The key this family's words are registered under, its styled tag:
42
+ * `registerWords('de', { 'helix-tab': { dismissLabel: 'Schließen' } })`.
43
+ */
44
+ static words: string;
29
45
  protected itemPart: string;
30
46
  /**
31
47
  * @internal The panel this tab shows, written by the tab set. A property,
@@ -34,6 +50,12 @@ export declare class HelixHeadlessTab extends HelixHeadlessTab_base {
34
50
  * inside this shadow root can reach.
35
51
  */
36
52
  controls: Element | undefined;
53
+ /**
54
+ * The tooltip on the mark that closes a `dismissible` tab. The mark is for
55
+ * the pointer and hidden from screen readers, which hear the Delete key
56
+ * through `aria-keyshortcuts`.
57
+ */
58
+ dismissLabel: string;
37
59
  /**
38
60
  * The URL the tab links to. Setting it renders the tab as a link, for a set
39
61
  * of tabs that are pages of a site.
@@ -52,6 +74,20 @@ export declare class HelixHeadlessTab extends HelixHeadlessTab_base {
52
74
  * script can focus the tab the way it focuses a native one.
53
75
  */
54
76
  focus(options?: FocusOptions): void;
77
+ /**
78
+ * Sends `helix-dismiss` for a `dismissible` tab, and returns `false` where a
79
+ * listener refused it, or where the tab is not dismissible or is disabled.
80
+ * The tab stays where it is either way: removing it is yours. Where the tab
81
+ * had focus, focus moves to the tab beside it once you remove it.
82
+ */
83
+ dismiss(): boolean;
84
+ /** Where focus goes when a tab beside this one that held it is removed: this tab. */
85
+ protected get returnFocusTo(): HTMLElement | null;
86
+ /** A press on the mark closes the tab, and neither selects it nor follows its link. */
87
+ private readonly handleDismissPress;
88
+ private readonly handleKeydown;
89
+ /** The mark that closes a `dismissible` tab, inside the control so it shares its box. */
90
+ protected renderDismiss(): TemplateResult | typeof nothing;
55
91
  /**
56
92
  * Chooses the tab before the browser follows its link, and keeps the browser
57
93
  * from following where the choice was refused. A press with a modifier, or
@@ -4,14 +4,28 @@ 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, HelixElement, MixinSelectableItem, plainClick, reflectElements, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, MixinDismissible, MixinSelectableItem, plainClick, reflectElements, words, } from '@fusion.dev/helix-core';
8
+ import { x } from '@fusion.dev/helix-icons';
8
9
  import { html, nothing } from 'lit';
9
10
  import { property } from 'lit/decorators.js';
10
- let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixElement) {
11
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
+ let HelixHeadlessTab = class HelixHeadlessTab extends MixinDismissible(MixinSelectableItem(HelixElement)) {
11
13
  constructor() {
12
14
  super(...arguments);
13
15
  this.itemPart = 'tab';
14
16
  this.controls = undefined;
17
+ this.dismissLabel = 'Close';
18
+ this.handleDismissPress = (event) => {
19
+ event.preventDefault();
20
+ event.stopPropagation();
21
+ this.dismiss();
22
+ };
23
+ this.handleKeydown = (event) => {
24
+ if (this.dismissible && (event.key === 'Delete' || event.key === 'Backspace')) {
25
+ event.preventDefault();
26
+ this.dismiss();
27
+ }
28
+ };
15
29
  this.handleLinkClick = (event) => {
16
30
  if (!plainClick(event)) {
17
31
  return;
@@ -21,9 +35,27 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixE
21
35
  }
22
36
  };
23
37
  }
38
+ static { this.words = 'helix-tab'; }
24
39
  focus(options) {
25
40
  this.partNamed('tab')?.focus(options);
26
41
  }
42
+ dismiss() {
43
+ if (!this.dismissible || this.disabled) {
44
+ return false;
45
+ }
46
+ return super.dismiss();
47
+ }
48
+ get returnFocusTo() {
49
+ return this.partNamed('tab');
50
+ }
51
+ renderDismiss() {
52
+ if (!this.dismissible) {
53
+ return nothing;
54
+ }
55
+ return html `<span part="dismiss" aria-hidden="true" title=${this.dismissLabel} @click=${this.handleDismissPress}
56
+ ><svg fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">${unsafeSVG(x.content)}</svg></span
57
+ >`;
58
+ }
27
59
  updated(changed) {
28
60
  super.updated(changed);
29
61
  const control = this.renderRoot.querySelector('[part="tab"]');
@@ -35,6 +67,7 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixE
35
67
  }
36
68
  render() {
37
69
  const tabindex = this.selected || !this.childrenKnown ? '0' : '-1';
70
+ const shortcut = this.dismissible && !this.disabled ? 'Delete' : nothing;
38
71
  if ((this.href ?? '') !== '') {
39
72
  return html `<a
40
73
  part="tab"
@@ -44,10 +77,12 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixE
44
77
  rel=${this.rel || nothing}
45
78
  aria-selected=${this.selected ? 'true' : 'false'}
46
79
  aria-disabled=${this.disabled ? 'true' : nothing}
80
+ aria-keyshortcuts=${shortcut}
47
81
  tabindex=${this.disabled ? nothing : tabindex}
48
82
  @click=${this.handleLinkClick}
83
+ @keydown=${this.handleKeydown}
49
84
  >
50
- <slot></slot>
85
+ <slot></slot>${this.renderDismiss()}
51
86
  </a>`;
52
87
  }
53
88
  return html `<button
@@ -55,17 +90,23 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixE
55
90
  role="tab"
56
91
  type="button"
57
92
  aria-selected=${this.selected ? 'true' : 'false'}
93
+ aria-keyshortcuts=${shortcut}
58
94
  ?disabled=${this.disabled}
59
95
  tabindex=${tabindex}
60
96
  @click=${() => this.select()}
97
+ @keydown=${this.handleKeydown}
61
98
  >
62
- <slot></slot>
99
+ <slot></slot>${this.renderDismiss()}
63
100
  </button>`;
64
101
  }
65
102
  };
66
103
  __decorate([
67
104
  property({ attribute: false })
68
105
  ], HelixHeadlessTab.prototype, "controls", void 0);
106
+ __decorate([
107
+ words,
108
+ property({ attribute: 'dismiss-label' })
109
+ ], HelixHeadlessTab.prototype, "dismissLabel", void 0);
69
110
  __decorate([
70
111
  property()
71
112
  ], HelixHeadlessTab.prototype, "href", void 0);
@@ -17,8 +17,12 @@ export type TagSelectionMode = SelectionMode;
17
17
  * `selection-mode` lets a reader select tags, with Space, Enter or a press:
18
18
  * `single` selects one at a time and `multiple` any number. In `multiple`,
19
19
  * Shift with an arrow, Home or End selects the range from the tag last
20
- * pressed to the tag focus moves to, and Control or Command with A selects
21
- * every tag that is not disabled. `value` holds the
20
+ * pressed to the tag focus moves to, Shift with a press or Space selects the
21
+ * range to that tag, and Control or Command with A selects every tag that is
22
+ * not disabled. `selection-behavior="replace"` selects the tag a reader
23
+ * presses or moves to alone, as a file list does, and Control or Command
24
+ * with a press or Space adds or takes out one tag. `disallow-empty-selection`
25
+ * keeps one tag selected once one is. `value` holds the
22
26
  * selected tags' values, separated by commas, and a change asks with
23
27
  * `helix-before-change` and tells with `change`. Enter on a tag with an
24
28
  * `href` follows its link. A `disabled` tag is passed over. A selected tag
@@ -56,6 +60,12 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
56
60
  * `registerWords('de', { 'helix-tag-group': { emptyLabel: 'Keine Schlagwörter' } })`.
57
61
  */
58
62
  static words: string;
63
+ /**
64
+ * Keeps at least one tag selected once one is, as React Aria's
65
+ * `disallowEmptySelection`: a press or a key that would leave none selected
66
+ * is refused. Removing the last selected tag still takes it out of `value`.
67
+ */
68
+ disallowEmptySelection: boolean;
59
69
  /**
60
70
  * The words the group shows and says once its last tag is removed, where
61
71
  * nothing is in the `empty` slot.
@@ -66,6 +76,15 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
66
76
  * before its tags.
67
77
  */
68
78
  label: string;
79
+ /**
80
+ * What a press, Space, or a move does to the tags already selected.
81
+ * `toggle`, the default, keeps them: a press adds or takes out the tag, and
82
+ * the arrow keys move focus alone. `replace` selects the tag alone, as a
83
+ * file list does, and the arrow keys carry the selection with focus.
84
+ * Control or Command with a press or Space adds or takes out one tag in
85
+ * `multiple`, and with an arrow moves focus without selecting.
86
+ */
87
+ selectionBehavior: 'replace' | 'toggle';
69
88
  /**
70
89
  * How many tags a reader can select: `single`, `multiple`, or none.
71
90
  *
@@ -129,7 +148,14 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
129
148
  * listener that refuses it keeps the selection. `change` follows once it
130
149
  * has moved.
131
150
  */
132
- protected press(tag: HelixHeadlessTag): void;
151
+ protected press(tag: HelixHeadlessTag, cause?: KeyboardEvent | MouseEvent): void;
152
+ /** Whether a press or a move selects the tag alone. */
153
+ private get replaces();
154
+ /**
155
+ * Asks for `value` to become `next`, where it changes and where it leaves a
156
+ * tag selected under `disallow-empty-selection`.
157
+ */
158
+ private ask;
133
159
  /** The reachable tags from one to the other, either way round. */
134
160
  private span;
135
161
  /**