@fusion.dev/helix-headless 0.150.0 → 0.152.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 (75) hide show
  1. package/custom-elements.json +1388 -93
  2. package/dist/app-header/root/app-header-root.d.ts +75 -0
  3. package/dist/app-header/root/app-header-root.js +140 -5
  4. package/dist/area-chart/root/area-chart-root.d.ts +36 -0
  5. package/dist/area-chart/root/area-chart-root.js +17 -2
  6. package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
  7. package/dist/bar-chart/root/bar-chart-root.js +17 -4
  8. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +11 -2
  9. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +11 -2
  10. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +14 -2
  11. package/dist/breadcrumbs/root/breadcrumbs-root.js +16 -3
  12. package/dist/callout/root/callout-root.d.ts +20 -0
  13. package/dist/callout/root/callout-root.js +19 -4
  14. package/dist/chat/composer/chat-composer.d.ts +30 -0
  15. package/dist/chat/composer/chat-composer.js +47 -2
  16. package/dist/chat/root/chat-root.d.ts +10 -0
  17. package/dist/chat/root/chat-root.js +14 -0
  18. package/dist/data-table/root/data-table-root.d.ts +34 -2
  19. package/dist/data-table/root/data-table-root.js +63 -2
  20. package/dist/dialog/description.d.ts +10 -0
  21. package/dist/dialog/description.js +22 -8
  22. package/dist/dialog/root/dialog-root.d.ts +17 -1
  23. package/dist/dialog/root/dialog-root.js +2 -2
  24. package/dist/divider/root/divider-root.d.ts +8 -6
  25. package/dist/divider/root/divider-root.js +10 -2
  26. package/dist/drawer/root/drawer-root.d.ts +38 -2
  27. package/dist/drawer/root/drawer-root.js +33 -6
  28. package/dist/element-events.generated.js +4 -0
  29. package/dist/kbd/root/kbd-root.d.ts +7 -1
  30. package/dist/kbd/root/kbd-root.js +8 -4
  31. package/dist/lightbox/root/lightbox-root.d.ts +44 -6
  32. package/dist/lightbox/root/lightbox-root.js +59 -11
  33. package/dist/lightbox/zoom-controller.d.ts +5 -0
  34. package/dist/lightbox/zoom-controller.js +8 -0
  35. package/dist/line-chart/root/line-chart-root.d.ts +36 -0
  36. package/dist/line-chart/root/line-chart-root.js +15 -2
  37. package/dist/menu/root/menu-root.js +2 -0
  38. package/dist/navigation/root/navigation-root.d.ts +11 -1
  39. package/dist/navigation/root/navigation-root.js +10 -5
  40. package/dist/notifications/root/notifications-root.d.ts +17 -0
  41. package/dist/notifications/root/notifications-root.js +21 -1
  42. package/dist/popover/root/popover-root.d.ts +18 -7
  43. package/dist/popover/root/popover-root.js +8 -1
  44. package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
  45. package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
  46. package/dist/splitter/root/splitter-root.d.ts +41 -0
  47. package/dist/splitter/root/splitter-root.js +77 -0
  48. package/dist/table/column/table-column.js +3 -2
  49. package/dist/table/grid-controller.d.ts +14 -2
  50. package/dist/table/grid-controller.js +27 -1
  51. package/dist/table/root/table-root.d.ts +30 -4
  52. package/dist/table/root/table-root.js +45 -7
  53. package/dist/table/row/table-row.d.ts +12 -2
  54. package/dist/table/row/table-row.js +8 -2
  55. package/dist/tabs/root/tabs-root.d.ts +25 -0
  56. package/dist/tabs/root/tabs-root.js +28 -1
  57. package/dist/tour/naming.d.ts +9 -3
  58. package/dist/tour/naming.js +9 -3
  59. package/dist/tour/root/tour-root.d.ts +29 -5
  60. package/dist/tour/root/tour-root.js +13 -1
  61. package/dist/tour/stop/tour-stop.d.ts +2 -1
  62. package/dist/tour/stop/tour-stop.js +4 -1
  63. package/dist/tree/root/tree-root.d.ts +13 -0
  64. package/dist/tree/root/tree-root.js +42 -2
  65. package/dist/video/root/video-root.d.ts +6 -0
  66. package/dist/video/root/video-root.js +5 -1
  67. package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
  68. package/dist/video-playlist/root/video-playlist-root.js +4 -1
  69. package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
  70. package/dist/virtual-list/root/virtual-list-root.js +21 -2
  71. package/lint.json +1 -1
  72. package/package.json +5 -5
  73. package/types/jsx.d.ts +1098 -128
  74. package/types/vue.d.ts +248 -59
  75. package/types/words.d.ts +12 -2
@@ -1,6 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
- import '../breadcrumb/breadcrumb.js';
4
3
  /** One step of a trail written as data, for `items`. */
5
4
  export interface BreadcrumbItem {
6
5
  /** The URL the step links to. The last step is the current page and takes none. */
@@ -26,7 +25,8 @@ export interface BreadcrumbItem {
26
25
  * markers.
27
26
  *
28
27
  * The items are slotted directly, so they are `<ol>` children in the flat tree,
29
- * and each item sets `role="listitem"` on itself.
28
+ * and each item sets `role="listitem"` on itself. Where no slotted crumb is
29
+ * `current`, the last one is said to be the current page.
30
30
  *
31
31
  * The trail does not measure itself, and you write its overflow in markup: put
32
32
  * the crumb that holds the folded items where the gap should appear.
@@ -41,6 +41,11 @@ export interface BreadcrumbItem {
41
41
  * to the first crumb it brought back. A new `items` folds again. Slotted crumbs
42
42
  * do not fold; write the folded crumb where the gap should be.
43
43
  *
44
+ * **Keyboard.** Each crumb with an `href` is a link and one tab stop, and
45
+ * Enter follows it. The current page and the separators are not in the tab
46
+ * order. In a folded trail, Enter or Space on the fold's button shows the
47
+ * whole trail and moves focus to the first crumb it brought back.
48
+ *
44
49
  * @since 0.1.0
45
50
  * @status stable
46
51
  * @category navigation
@@ -96,8 +101,15 @@ export declare class HelixHeadlessBreadcrumbs extends HelixElement {
96
101
  /** Shows the whole trail and moves focus to the first crumb it brought back. */
97
102
  private readonly expand;
98
103
  protected willUpdate(changed: PropertyValues): void;
104
+ protected updated(changed: PropertyValues): void;
99
105
  /** How many crumbs drawn from `items` are folded away, or 0. */
100
106
  private get folded();
107
+ /**
108
+ * Says the last slotted crumb is the current page where none is marked
109
+ * `current`, as React Aria's trail does. Through a property the crumb does
110
+ * not reflect, so nothing is written onto your markup.
111
+ */
112
+ private readonly markLast;
101
113
  /** The crumbs `items` describes, each a crumb of this tier. */
102
114
  protected renderItems(): TemplateResult | typeof nothing;
103
115
  protected render(): TemplateResult;
@@ -4,12 +4,12 @@ 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, formatWords, HelixElement, hostAria, words } from '@fusion.dev/helix-core';
7
+ import { customElement, formatWords, HelixElement, hostAria, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { repeat } from 'lit/directives/repeat.js';
11
11
  import { html } from 'lit/static-html.js';
12
- import '../breadcrumb/breadcrumb.js';
12
+ import { HelixHeadlessBreadcrumb } from '../breadcrumb/breadcrumb.js';
13
13
  let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixElement {
14
14
  constructor() {
15
15
  super(...arguments);
@@ -24,6 +24,15 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
24
24
  crumb?.shadowRoot?.querySelector('a, [part="link"]')?.focus();
25
25
  });
26
26
  };
27
+ this.markLast = () => {
28
+ const crumbs = (this.renderRoot.querySelector('slot:not([name])')?.assignedElements() ?? []).filter((one) => one instanceof HelixHeadlessBreadcrumb);
29
+ const marked = crumbs.some((one) => one.current);
30
+ crumbs.forEach((crumb, index) => {
31
+ whenHydrated(crumb, () => {
32
+ crumb.implied = !marked && this.items.length === 0 && index === crumbs.length - 1;
33
+ });
34
+ });
35
+ };
27
36
  }
28
37
  static { this.words = 'helix-breadcrumbs'; }
29
38
  willUpdate(changed) {
@@ -33,6 +42,10 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
33
42
  this.expanded = false;
34
43
  }
35
44
  }
45
+ updated(changed) {
46
+ super.updated(changed);
47
+ this.markLast();
48
+ }
36
49
  get folded() {
37
50
  const most = this.maxItems ?? 0;
38
51
  const count = this.items.length;
@@ -77,7 +90,7 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
77
90
  }
78
91
  render() {
79
92
  return html `<ol part="list" role="list">
80
- ${this.renderItems()}<slot></slot>
93
+ ${this.renderItems()}<slot @slotchange=${this.markLast}></slot>
81
94
  </ol>`;
82
95
  }
83
96
  };
@@ -48,6 +48,16 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
48
48
  */
49
49
  static words: string;
50
50
  protected willUpdate(changed: PropertyValues): void;
51
+ /**
52
+ * The dismiss button's name, read by screen readers, with `{label}` for
53
+ * what the callout is called: its heading, or the tone's name where it has
54
+ * none.
55
+ *
56
+ * "Dismiss Scheduled maintenance" says which of several callouts the button
57
+ * takes away, where every button was "Dismiss". A label without `{label}` is
58
+ * read as written.
59
+ */
60
+ dismissLabel: string;
51
61
  /**
52
62
  * What kind of attention the passage needs.
53
63
  *
@@ -96,6 +106,16 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
96
106
  * above.
97
107
  */
98
108
  protected renderDismissIcon(): TemplateResult;
109
+ /**
110
+ * The dismiss button's name: `dismissLabel` with the heading's words, or the
111
+ * tone's name.
112
+ *
113
+ * The heading is read from the children, so a server and a hydrating render
114
+ * name the button for the tone, and the update after names it for the
115
+ * heading.
116
+ */
117
+ private dismissName;
118
+ protected firstUpdated(changed: PropertyValues): void;
99
119
  protected render(): TemplateResult;
100
120
  }
101
121
  declare global {
@@ -4,13 +4,14 @@ 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, drawDismiss, drawDismissGlyph, HelixElement, hostAria, MixinDismissible, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
7
+ import { customElement, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
11
11
  let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(HelixElement) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.dismissLabel = 'Dismiss {label}';
14
15
  this.toneLabel = '';
15
16
  this.toneLabels = {
16
17
  danger: 'Error',
@@ -26,7 +27,7 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
26
27
  hostAria(this.internals, { role: 'note' });
27
28
  }
28
29
  renderIcon(icon, _label) {
29
- return html `<slot name="icon">
30
+ return html `<slot name="icon" aria-hidden="true">
30
31
  <svg part="icon-mark" fill="currentColor" viewBox=${icon.viewBox} aria-hidden="true">
31
32
  ${unsafeSVG(icon.content)}
32
33
  </svg>
@@ -37,23 +38,37 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
37
38
  ${drawDismissGlyph()}
38
39
  </slot>`;
39
40
  }
41
+ dismissName(tone) {
42
+ const heading = this.childrenKnown ? slottedWords(this, ['heading']) : '';
43
+ return this.dismissLabel.includes('{label}')
44
+ ? formatWords(this, 'dismissLabel', { label: heading || tone }).trim()
45
+ : this.dismissLabel;
46
+ }
47
+ firstUpdated(changed) {
48
+ super.firstUpdated(changed);
49
+ this.finishHydration();
50
+ }
40
51
  render() {
41
52
  const tone = this.tone === 'neutral' ? 'neutral' : toneMark(this.tone) ? (this.tone ?? 'info') : 'info';
42
53
  const { icon } = toneMark(tone) ?? TONE_MARKS.info;
43
54
  const named = this.toneLabel || (this.toneLabels[tone] ?? '');
44
55
  return html `<span part="icon" role="img" aria-label=${named}>${this.renderIcon(icon, named)}</span>
45
56
  <div part="content">
46
- <slot name="heading"></slot>
57
+ <slot name="heading" @slotchange=${() => this.requestUpdate()}></slot>
47
58
  <slot></slot>
48
59
  </div>${this.dismissible
49
60
  ? drawDismiss({
50
- label: this.dismissLabel,
61
+ label: this.dismissName(named),
51
62
  mark: this.renderDismissIcon(),
52
63
  onDismiss: () => this.dismiss(),
53
64
  })
54
65
  : nothing}`;
55
66
  }
56
67
  };
68
+ __decorate([
69
+ words,
70
+ property({ attribute: 'dismiss-label' })
71
+ ], HelixHeadlessCallout.prototype, "dismissLabel", void 0);
57
72
  __decorate([
58
73
  property({ reflect: true })
59
74
  ], HelixHeadlessCallout.prototype, "tone", void 0);
@@ -20,6 +20,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
20
20
  * disabled>` turns the composer off. Sending is still `helix-send`, and it
21
21
  * does not submit the form.
22
22
  *
23
+ * **`required`, `minlength` and `maxlength` are checked by the browser**, as
24
+ * on a `<textarea>`, and the message is the browser's. `maxlength` stops the
25
+ * field taking more. A draft that fails them does not send, and the composer
26
+ * takes `:state(invalid)` and `aria-invalid` once the reader has tried to send
27
+ * it, the form was submitted, or the reader edited the field and left it.
28
+ * Tabbing past an empty composer marks nothing.
29
+ *
23
30
  * **`typing` says whether the user is writing**, and `helix-typing` is sent
24
31
  * each time it changes: when they start, and when they send or stop for
25
32
  * `typing-timeout` milliseconds. Pass it on to show "is typing" to the other
@@ -45,6 +52,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
45
52
  * @csspart send - The send button.
46
53
  * @csspart start - The box around the `start` slot.
47
54
  * @csspart tools - The row under the field.
55
+ *
56
+ * @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it.
48
57
  */
49
58
  export declare class HelixHeadlessChatComposer extends HelixFormElement {
50
59
  /**
@@ -56,10 +65,24 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
56
65
  * The field's name, read by screen readers.
57
66
  */
58
67
  label: string;
68
+ /**
69
+ * The most characters a draft can have. The field stops taking more.
70
+ */
71
+ maxLength?: number;
72
+ /**
73
+ * The fewest characters a draft can have, once the reader has written in
74
+ * the field, as on a `<textarea>`.
75
+ */
76
+ minLength?: number;
59
77
  /**
60
78
  * Placeholder text shown in an empty field.
61
79
  */
62
80
  placeholder: string;
81
+ /**
82
+ * Whether the form needs a draft to submit. An empty draft never sends,
83
+ * required or not.
84
+ */
85
+ required: boolean;
63
86
  /**
64
87
  * How many rows the field opens at.
65
88
  *
@@ -97,6 +120,10 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
97
120
  readonly isChatComposer = true;
98
121
  /** Whether the user is writing, which `helix-typing` announces. */
99
122
  private writing;
123
+ /** Whether the reader has tried to send or submit the draft, or edited it and left. */
124
+ private touched;
125
+ /** Whether the composer shows that its draft is wrong. */
126
+ private shownInvalid;
100
127
  /** The pause after which the user has stopped writing. */
101
128
  private readonly pause;
102
129
  /**
@@ -121,6 +148,8 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
121
148
  private get declared();
122
149
  /** Puts back the draft the markup declared when the form is reset. */
123
150
  formResetCallback(): void;
151
+ /** The reader has had a go at the draft, so whether it is wrong can be shown. */
152
+ private readonly touch;
124
153
  /** Brings back the draft when the browser restores the form. */
125
154
  formStateRestoreCallback(state: string | File | FormData | null): void;
126
155
  /**
@@ -133,6 +162,7 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
133
162
  */
134
163
  private markEmpty;
135
164
  protected updated(changed: PropertyValues): void;
165
+ protected firstUpdated(changed: PropertyValues): void;
136
166
  protected willUpdate(changed: PropertyValues): void;
137
167
  /**
138
168
  * The control that offers the turn, behind a seam so a tier can draw its own.
@@ -6,8 +6,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, DelayController, HelixFormElement, HelixSendEvent, HelixTypingEvent, hostAria, markRegion, words, } from '@fusion.dev/helix-core';
8
8
  import { arrowElbowDownLeft } from '@fusion.dev/helix-icons';
9
- import { html } from 'lit';
10
- import { property } from 'lit/decorators.js';
9
+ import { html, nothing } from 'lit';
10
+ import { property, state } from 'lit/decorators.js';
11
11
  import { live } from 'lit/directives/live.js';
12
12
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
13
13
  let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
@@ -15,6 +15,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
15
15
  super(...arguments);
16
16
  this.label = 'Message';
17
17
  this.placeholder = '';
18
+ this.required = false;
18
19
  this.rows = 1;
19
20
  this.sendLabel = 'Send';
20
21
  this.submitOn = 'enter';
@@ -22,7 +23,13 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
22
23
  this.value = '';
23
24
  this.isChatComposer = true;
24
25
  this.writing = false;
26
+ this.touched = false;
27
+ this.shownInvalid = false;
25
28
  this.pause = new DelayController(this, { renders: false });
29
+ this.touch = () => {
30
+ this.touched = true;
31
+ this.requestUpdate();
32
+ };
26
33
  this.handleInput = (event) => {
27
34
  event.stopPropagation();
28
35
  this.value = event.target.value;
@@ -65,10 +72,15 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
65
72
  if (value === '' || this.isDisabled) {
66
73
  return false;
67
74
  }
75
+ if (!this.internals.validity.valid) {
76
+ this.touch();
77
+ return false;
78
+ }
68
79
  if (!this.dispatchEvent(new HelixSendEvent({ value }))) {
69
80
  return false;
70
81
  }
71
82
  this.value = '';
83
+ this.touched = false;
72
84
  this.markTyping(false);
73
85
  return true;
74
86
  }
@@ -78,6 +90,8 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
78
90
  formResetCallback() {
79
91
  super.formResetCallback();
80
92
  this.value = this.declared;
93
+ this.touched = false;
94
+ this.requestUpdate();
81
95
  }
82
96
  formStateRestoreCallback(state) {
83
97
  this.value = typeof state === 'string' ? state : this.declared;
@@ -93,6 +107,20 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
93
107
  if (changed.has('value')) {
94
108
  this.internals.setFormValue(this.value === '' ? null : this.value);
95
109
  }
110
+ const field = this.partNamed('field');
111
+ if (field?.validity) {
112
+ this.internals.setValidity(field.validity, field.validationMessage, field);
113
+ }
114
+ const invalid = this.touched && !this.internals.validity.valid;
115
+ this.toggleState('invalid', invalid);
116
+ this.internals.ariaInvalid = invalid ? 'true' : null;
117
+ if (invalid !== this.shownInvalid) {
118
+ this.shownInvalid = invalid;
119
+ }
120
+ }
121
+ firstUpdated(changed) {
122
+ super.firstUpdated(changed);
123
+ this.addEventListener('invalid', this.touch);
96
124
  }
97
125
  willUpdate(changed) {
98
126
  super.willUpdate(changed);
@@ -124,7 +152,12 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
124
152
  aria-label=${this.label}
125
153
  placeholder=${this.placeholder}
126
154
  rows=${this.rows}
155
+ ?required=${this.required}
156
+ minlength=${this.minLength ?? nothing}
157
+ maxlength=${this.maxLength ?? nothing}
158
+ aria-invalid=${this.shownInvalid ? 'true' : nothing}
127
159
  ?disabled=${this.isDisabled}
160
+ @change=${this.touch}
128
161
  @input=${this.handleInput}
129
162
  @keydown=${this.handleKey}
130
163
  ></textarea>
@@ -140,9 +173,18 @@ __decorate([
140
173
  words,
141
174
  property()
142
175
  ], HelixHeadlessChatComposer.prototype, "label", void 0);
176
+ __decorate([
177
+ property({ attribute: 'maxlength', type: Number })
178
+ ], HelixHeadlessChatComposer.prototype, "maxLength", void 0);
179
+ __decorate([
180
+ property({ attribute: 'minlength', type: Number })
181
+ ], HelixHeadlessChatComposer.prototype, "minLength", void 0);
143
182
  __decorate([
144
183
  property()
145
184
  ], HelixHeadlessChatComposer.prototype, "placeholder", void 0);
185
+ __decorate([
186
+ property({ type: Boolean })
187
+ ], HelixHeadlessChatComposer.prototype, "required", void 0);
146
188
  __decorate([
147
189
  property({ type: Number })
148
190
  ], HelixHeadlessChatComposer.prototype, "rows", void 0);
@@ -159,6 +201,9 @@ __decorate([
159
201
  __decorate([
160
202
  property()
161
203
  ], HelixHeadlessChatComposer.prototype, "value", void 0);
204
+ __decorate([
205
+ state()
206
+ ], HelixHeadlessChatComposer.prototype, "shownInvalid", void 0);
162
207
  HelixHeadlessChatComposer = __decorate([
163
208
  customElement('helix-headless-chat-composer')
164
209
  ], HelixHeadlessChatComposer);
@@ -331,6 +331,16 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
331
331
  get messages(): HelixHeadlessChatMessage[];
332
332
  /** The element that scrolls, once there is one. */
333
333
  private get log();
334
+ /**
335
+ * Makes the log a tab stop while it scrolls, and takes the stop away when it
336
+ * does not.
337
+ *
338
+ * **A reader on a keyboard scrolls the history from the log itself.**
339
+ * Chromium and Firefox make a box that scrolls focusable on their own, and
340
+ * Safari does not, so there the arrow keys could not reach a long history.
341
+ * A log that fits is no stop at all, so Tab goes straight to the composer.
342
+ */
343
+ private markScroller;
334
344
  /** Whether the reader is within `threshold` of the bottom right now. */
335
345
  private get atBottom();
336
346
  /** What identifies a turn fed as data: its key, or the object it is. */
@@ -78,6 +78,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
78
78
  });
79
79
  this.filled = undefined;
80
80
  this.handleResize = () => {
81
+ this.markScroller();
81
82
  if (this.pinned) {
82
83
  this.stick();
83
84
  }
@@ -141,6 +142,18 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
141
142
  get log() {
142
143
  return this.partNamed('log');
143
144
  }
145
+ markScroller() {
146
+ const log = this.log;
147
+ if (!log) {
148
+ return;
149
+ }
150
+ if (log.scrollHeight > log.clientHeight) {
151
+ log.tabIndex = 0;
152
+ }
153
+ else {
154
+ log.removeAttribute('tabindex');
155
+ }
156
+ }
144
157
  get atBottom() {
145
158
  const log = this.log;
146
159
  if (!log) {
@@ -421,6 +434,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
421
434
  this.virtualize.measure();
422
435
  }
423
436
  this.watchSizes();
437
+ this.markScroller();
424
438
  this.remember();
425
439
  this.reachHistory();
426
440
  this.announce();
@@ -40,6 +40,10 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
40
40
  * and shows a dash while only some are chosen.
41
41
  * - Shift with a press on a checkbox selects every row between it and the row
42
42
  * last chosen.
43
+ * - `selection-behavior="replace"` makes a press or a move select the row
44
+ * alone, as a file list does. Control or Command with a press adds or takes
45
+ * out one row, and Shift with a press selects every row from the one last
46
+ * chosen.
43
47
  *
44
48
  * To act on a single row, such as opening it, put a link or a button in a cell.
45
49
  *
@@ -56,7 +60,8 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
56
60
  * | Enter, F2 | Moves into the cell's controls. Escape moves back to the cell, and Tab moves between the controls of one cell. |
57
61
  * | Enter, Space on a sortable heading | Sorts by the column. |
58
62
  * | Enter, Space on the `load-more` row | Sends `helix-load-more`. |
59
- * | Space on a row | Selects the row, in a `selectable` table. |
63
+ * | Space on a row | Selects the row, in a `selectable` table. Under `selection-behavior="replace"`, selects it alone, and Control or Command with Space adds or takes out the row. |
64
+ * | Down, Up, Page Down, Page Up under `selection-behavior="replace"` | Selects the row it moves to alone. Control or Command with the key moves without selecting. |
60
65
  * | Shift with Down or Up | Selects the rows it moves across, in a `selectable` table. |
61
66
  * | Shift with Space | Selects every row from the one last chosen, in a `selectable` table. |
62
67
  * | Control or Command with A | Selects every row, in a `selectable` table. |
@@ -123,7 +128,9 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
123
128
  * @documentation https://fusion.dev/helix/components/data-table
124
129
  *
125
130
  * @slot - The columns, as `TableColumn` elements.
131
+ * @slot empty - Content shown when there are no rows to show and none are loading, such as a picture and a link. Falls back to `empty-text`, which the live region still says.
126
132
  * @slot footer - Content below the grid. It replaces the built-in pagination.
133
+ * @slot loading - Content shown while the rows load, such as a spinner. Falls back to `loading-text`, which the live region still says.
127
134
  * @slot toolbar - Content above the grid, such as a search field or actions.
128
135
  *
129
136
  * @event {HelixActivateEvent} helix-activate - Fired after the page changes, with the new page as a string in `detail.value`.
@@ -216,6 +223,8 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
216
223
  * `registerWords('de', { 'helix-data-table': { nextLabel: 'Nächste Seite' } })`.
217
224
  */
218
225
  static words: string;
226
+ /** The empty and loading lines are the `empty` and `loading` slots. */
227
+ protected statusSlots: boolean;
219
228
  /**
220
229
  * The word for an ascending sort, in a `sorted-label` that writes `{order}`.
221
230
  *
@@ -490,6 +499,15 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
490
499
  * {# wybranych} other {# wybranego}}`.
491
500
  */
492
501
  selectedLabel: string;
502
+ /**
503
+ * What a key or a press on a row does to the rows already selected, in a
504
+ * `selectable` table. `toggle`, the default, keeps them: Space adds or takes
505
+ * out the row. `replace` selects the row alone, as a file list does: Down and
506
+ * Up carry the selection with them, and a press on a row selects it. Control
507
+ * or Command with a press or Space adds or takes out one row, and with a move
508
+ * goes without selecting. The checkboxes add or take out one row either way.
509
+ */
510
+ selectionBehavior: 'replace' | 'toggle';
493
511
  /**
494
512
  * The name of a row's checkbox. `{row}` is replaced with the row's value in
495
513
  * the first column that has a field, or with its key.
@@ -954,7 +972,7 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
954
972
  */
955
973
  toggleSort(field: string): boolean;
956
974
  /**
957
- * The columns into the header row, everything else into the toolbar and footer.
975
+ * The columns into the header row, everything else into the slot it names.
958
976
  *
959
977
  * Manual assignment, so a consumer's own markup is never written to.
960
978
  */
@@ -1124,6 +1142,20 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
1124
1142
  * request carries only the state it asks for.
1125
1143
  */
1126
1144
  private readonly notePress;
1145
+ /** Whether a press or a move on a row selects it alone. */
1146
+ private get replaces();
1147
+ /**
1148
+ * Selects the row at `row` on the page alone, and makes it where a range
1149
+ * runs from. Asks nothing where it is already the only one selected.
1150
+ */
1151
+ private chooseOnly;
1152
+ /**
1153
+ * A press on a row under `selection-behavior="replace"`: it selects the row
1154
+ * alone, Control or Command adds or takes out the row, and Shift selects
1155
+ * every row from the one last chosen. A press on a control in a cell, or on
1156
+ * the checkbox or grip column, is the control's.
1157
+ */
1158
+ private readonly handleRowClick;
1127
1159
  /**
1128
1160
  * A click on a sortable heading, anywhere but its grip or its edge.
1129
1161
  *
@@ -30,6 +30,7 @@ const isControl = (element) => element.matches(CONTROL) || element.shadowRoot?.d
30
30
  let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollection(HelixElement) {
31
31
  constructor() {
32
32
  super(...arguments);
33
+ this.statusSlots = true;
33
34
  this.ascendingLabel = 'ascending';
34
35
  this.canceledLabel = 'Canceled';
35
36
  this.defaultExpanded = '';
@@ -64,6 +65,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
64
65
  this.selectable = false;
65
66
  this.selectAllLabel = 'Select every row';
66
67
  this.selectedLabel = '{count} selected';
68
+ this.selectionBehavior = 'toggle';
67
69
  this.selectRowLabel = 'Select {row}';
68
70
  this.sortedLabel = '{direction, select, ascending {Sorted by {column}, ascending} descending {Sorted by {column}, descending} other {Sorted by {column}}}';
69
71
  this.total = 0;
@@ -178,6 +180,30 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
178
180
  this.notePress = (event) => {
179
181
  this.shiftPressed = event.shiftKey;
180
182
  };
183
+ this.handleRowClick = (event) => {
184
+ if (!this.replaces || event.defaultPrevented) {
185
+ return;
186
+ }
187
+ const place = this.placeOf(event);
188
+ if (!place || place.row < 0 || place.row >= this.drawnPage.length || place.column < this.lead) {
189
+ return;
190
+ }
191
+ const origin = event.composedPath()[0];
192
+ if (origin instanceof Node && this.controlsIn(place).some((one) => deepContains(one, origin))) {
193
+ return;
194
+ }
195
+ if (event.shiftKey && this.anchorKey !== undefined) {
196
+ this.selectRange(place.row);
197
+ return;
198
+ }
199
+ if (event.ctrlKey || event.metaKey) {
200
+ const data = this.drawnPage[place.row];
201
+ const key = this.keyOf(data, this.rowIndex(place.row) - 2);
202
+ this.chooseRow(key, !this.chosen.has(key));
203
+ return;
204
+ }
205
+ this.chooseOnly(place.row);
206
+ };
181
207
  this.handleHeadClick = (event) => {
182
208
  if (this.partIn(event, 'grip') || this.partIn(event, 'resizer')) {
183
209
  return;
@@ -251,14 +277,22 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
251
277
  }
252
278
  const { column, row } = to;
253
279
  event.preventDefault();
280
+ const onRow = row >= 0 && row < this.drawnPage.length;
254
281
  if (event.shiftKey &&
255
282
  this.selectable &&
256
283
  (event.key === 'ArrowUp' || event.key === 'ArrowDown') &&
257
284
  place.row >= 0 &&
258
- row >= 0 &&
259
- row < this.drawnPage.length) {
285
+ onRow) {
260
286
  this.selectRange(row, place.row);
261
287
  }
288
+ else if (this.replaces &&
289
+ onRow &&
290
+ row !== place.row &&
291
+ !event.shiftKey &&
292
+ !event.ctrlKey &&
293
+ !event.metaKey) {
294
+ this.chooseOnly(row);
295
+ }
262
296
  if (row !== place.row || column !== place.column) {
263
297
  this.moveTo(row, column);
264
298
  }
@@ -878,6 +912,8 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
878
912
  for (const [name, wanted] of [
879
913
  ['toolbar', true],
880
914
  ['footer', true],
915
+ ['empty', true],
916
+ ['loading', true],
881
917
  ]) {
882
918
  const slot = root.querySelector(`slot[name="${name}"]`);
883
919
  if (slot?.assign && wanted) {
@@ -1201,6 +1237,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1201
1237
  connectedCallback() {
1202
1238
  super.connectedCallback();
1203
1239
  this.addEventListener('click', this.handleHeadClick);
1240
+ this.addEventListener('click', this.handleRowClick);
1204
1241
  this.addEventListener('pointerdown', this.notePress, true);
1205
1242
  this.addEventListener('keydown', this.notePress, true);
1206
1243
  this.addEventListener('focusin', this.handleFocusIn);
@@ -1212,6 +1249,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1212
1249
  disconnectedCallback() {
1213
1250
  super.disconnectedCallback();
1214
1251
  this.removeEventListener('click', this.handleHeadClick);
1252
+ this.removeEventListener('click', this.handleRowClick);
1215
1253
  this.removeEventListener('pointerdown', this.notePress, true);
1216
1254
  this.removeEventListener('keydown', this.notePress, true);
1217
1255
  this.removeEventListener('focusin', this.handleFocusIn);
@@ -1220,6 +1258,22 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1220
1258
  this.removeEventListener('keydown', this.handleGripKey);
1221
1259
  this.removeEventListener('helix-before-resize', this.handleResize);
1222
1260
  }
1261
+ get replaces() {
1262
+ return this.selectable && this.selectionBehavior === 'replace';
1263
+ }
1264
+ chooseOnly(row) {
1265
+ const data = this.drawnPage[row];
1266
+ if (!data) {
1267
+ return;
1268
+ }
1269
+ const key = this.keyOf(data, this.rowIndex(row) - 2);
1270
+ this.anchorKey = key;
1271
+ const now = this.chosen;
1272
+ if (now.size === 1 && now.has(key)) {
1273
+ return;
1274
+ }
1275
+ this.requestSelected(new Set([key]));
1276
+ }
1223
1277
  cellAt(row, column) {
1224
1278
  const offset = this.lead;
1225
1279
  if (row === -1) {
@@ -1402,6 +1456,10 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1402
1456
  event.preventDefault();
1403
1457
  this.selectRange(place.row);
1404
1458
  }
1459
+ else if (this.replaces && row && !event.ctrlKey && !event.metaKey) {
1460
+ event.preventDefault();
1461
+ this.chooseOnly(place.row);
1462
+ }
1405
1463
  else if (this.selectable && row) {
1406
1464
  event.preventDefault();
1407
1465
  const key = this.keyOf(row, this.rowIndex(place.row) - 2);
@@ -1595,6 +1653,9 @@ __decorate([
1595
1653
  words,
1596
1654
  property({ attribute: 'selected-label' })
1597
1655
  ], HelixHeadlessDataTable.prototype, "selectedLabel", void 0);
1656
+ __decorate([
1657
+ property({ attribute: 'selection-behavior' })
1658
+ ], HelixHeadlessDataTable.prototype, "selectionBehavior", void 0);
1598
1659
  __decorate([
1599
1660
  words,
1600
1661
  property({ attribute: 'select-row-label' })