@fusion.dev/helix-headless 0.149.1 → 0.151.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 (69) hide show
  1. package/custom-elements.json +1211 -110
  2. package/dist/area-chart/root/area-chart-root.d.ts +36 -0
  3. package/dist/area-chart/root/area-chart-root.js +17 -2
  4. package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
  5. package/dist/bar-chart/root/bar-chart-root.js +17 -4
  6. package/dist/button/root/button-root.d.ts +5 -0
  7. package/dist/button/root/button-root.js +3 -0
  8. package/dist/callout/root/callout-root.d.ts +20 -0
  9. package/dist/callout/root/callout-root.js +19 -4
  10. package/dist/chat/composer/chat-composer.d.ts +30 -0
  11. package/dist/chat/composer/chat-composer.js +47 -2
  12. package/dist/chat/root/chat-root.d.ts +10 -0
  13. package/dist/chat/root/chat-root.js +14 -0
  14. package/dist/color-input/root/color-input-root.d.ts +1 -0
  15. package/dist/color-picker/root/color-picker-root.js +1 -0
  16. package/dist/combobox/root/combobox-root.d.ts +12 -1
  17. package/dist/combobox/root/combobox-root.js +8 -0
  18. package/dist/copy-button/root/copy-button-root.d.ts +4 -3
  19. package/dist/copy-button/root/copy-button-root.js +5 -1
  20. package/dist/data-table/root/data-table-root.d.ts +34 -2
  21. package/dist/data-table/root/data-table-root.js +63 -2
  22. package/dist/date-input/root/date-input-root.d.ts +1 -0
  23. package/dist/divider/root/divider-root.d.ts +8 -6
  24. package/dist/divider/root/divider-root.js +10 -2
  25. package/dist/element-events.generated.js +5 -1
  26. package/dist/filter-input/chip/filter-chip.d.ts +2 -1
  27. package/dist/filter-input/root/filter-input-root.d.ts +5 -0
  28. package/dist/filter-input/root/filter-input-root.js +7 -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/line-chart/root/line-chart-root.d.ts +36 -0
  32. package/dist/line-chart/root/line-chart-root.js +15 -2
  33. package/dist/number-input/root/number-input-root.d.ts +6 -0
  34. package/dist/number-input/root/number-input-root.js +14 -3
  35. package/dist/option/root/option-root.d.ts +17 -8
  36. package/dist/option/root/option-root.js +18 -4
  37. package/dist/radio/group/radio-group.d.ts +8 -1
  38. package/dist/radio/group/radio-group.js +15 -1
  39. package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
  40. package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
  41. package/dist/slider/root/slider-root.d.ts +8 -0
  42. package/dist/slider/root/slider-root.js +6 -3
  43. package/dist/table/column/table-column.js +3 -2
  44. package/dist/table/grid-controller.d.ts +14 -2
  45. package/dist/table/grid-controller.js +27 -1
  46. package/dist/table/root/table-root.d.ts +30 -4
  47. package/dist/table/root/table-root.js +45 -7
  48. package/dist/table/row/table-row.d.ts +12 -2
  49. package/dist/table/row/table-row.js +8 -2
  50. package/dist/tag/group/tag-group.d.ts +22 -1
  51. package/dist/tag/group/tag-group.js +63 -3
  52. package/dist/tag/root/tag-root.d.ts +9 -0
  53. package/dist/tag/root/tag-root.js +5 -1
  54. package/dist/time-input/root/time-input-root.d.ts +1 -0
  55. package/dist/toggle/group/toggle-group.d.ts +6 -0
  56. package/dist/toggle/group/toggle-group.js +16 -1
  57. package/dist/toggle/root/toggle-root.d.ts +21 -3
  58. package/dist/toggle/root/toggle-root.js +22 -5
  59. package/dist/video/root/video-root.d.ts +6 -0
  60. package/dist/video/root/video-root.js +5 -1
  61. package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
  62. package/dist/video-playlist/root/video-playlist-root.js +4 -1
  63. package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
  64. package/dist/virtual-list/root/virtual-list-root.js +21 -2
  65. package/lint.json +1 -1
  66. package/package.json +5 -5
  67. package/types/jsx.d.ts +808 -156
  68. package/types/vue.d.ts +223 -59
  69. package/types/words.d.ts +4 -2
@@ -4,7 +4,7 @@ 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 { byRow, CHART_PLOT, commaList, customElement, drawTooltip, extent, HelixElement, linear, linePath, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesKey, ticks, unbroken, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { byRow, CHART_PLOT, commaList, customElement, drawTooltip, drawValueAxis, extent, formatNumbers, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, linear, linePath, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesKey, ticks, unbroken, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
@@ -14,9 +14,16 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
14
14
  this.area = false;
15
15
  this.dashed = '';
16
16
  this.markers = false;
17
+ this.valueAxis = false;
17
18
  this.zero = false;
18
19
  }
19
20
  static { this.words = 'helix-line-chart'; }
21
+ connectedCallback() {
22
+ super.connectedCallback();
23
+ this.pointer.onReading = (heard, detail) => this.dispatchEvent(heard === 'press'
24
+ ? new HelixReadingPressEvent(detail)
25
+ : new HelixReadingChangeEvent(detail));
26
+ }
20
27
  get rulesValue() {
21
28
  return this.gridlines !== undefined && this.gridlines !== 'category';
22
29
  }
@@ -58,7 +65,10 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
58
65
  const y = linear(domain, [PLOT_HEIGHT, 0]);
59
66
  const slot = PLOT_WIDTH / readings.length;
60
67
  const x = linear([0, Math.max(readings.length - 1, 1)], [slot / 2, PLOT_WIDTH - slot / 2]);
61
- return html `${this.renderPlot(series, x, y, domain)} ${this.renderLabels(readings)}${this.renderSummary(series)}`;
68
+ return html `${this.valueAxis ? this.renderAxis(y, domain) : nothing}${this.renderPlot(series, x, y, domain)} ${this.renderLabels(readings)}${this.renderSummary(series)}`;
69
+ }
70
+ renderAxis(y, domain) {
71
+ return drawValueAxis(domain, (value) => (y(value) / PLOT_HEIGHT) * 100, formatNumbers(wordsLocale(this)), (widest) => `--line-chart-axis: ${widest}ch`);
62
72
  }
63
73
  renderSummary(series) {
64
74
  const rows = this.sourceRows;
@@ -254,6 +264,9 @@ __decorate([
254
264
  __decorate([
255
265
  property({ reflect: true })
256
266
  ], HelixHeadlessLineChart.prototype, "terminus", void 0);
267
+ __decorate([
268
+ property({ attribute: 'value-axis', reflect: true, type: Boolean })
269
+ ], HelixHeadlessLineChart.prototype, "valueAxis", void 0);
257
270
  __decorate([
258
271
  property({ reflect: true, type: Boolean })
259
272
  ], HelixHeadlessLineChart.prototype, "zero", void 0);
@@ -381,6 +381,8 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
381
381
  * saving on `change` kept 150 while the form submitted 100.
382
382
  */
383
383
  private readonly handleChange;
384
+ /** The value Enter settled and announced, until the field's `change` or the next keystroke. */
385
+ private enterCommit;
384
386
  /**
385
387
  * The four keys a spinner answers, which `Input` does not bind.
386
388
  *
@@ -423,6 +425,10 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
423
425
  *
424
426
  * On `keypress`, where the platform runs its own implicit submission, so a
425
427
  * consumer who prevents the `keydown` for Enter stops this too.
428
+ *
429
+ * **Settled first**, as blur settles it. The typed text is read as a number,
430
+ * `change` announces it, and the form value is written before the form is
431
+ * submitted, so a field showing `$2,500.50` submits 2500.5.
426
432
  */
427
433
  private readonly handleKeypress;
428
434
  /** @internal A single line of text counts against implicit submission. */
@@ -96,6 +96,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
96
96
  };
97
97
  this.handleInput = (event) => {
98
98
  event.stopPropagation();
99
+ this.enterCommit = undefined;
99
100
  const control = event.target;
100
101
  this.requestValue(control.value);
101
102
  restoreValue(control, this.shown);
@@ -109,7 +110,11 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
109
110
  this.handleChange = (event) => {
110
111
  event.stopPropagation();
111
112
  this.settle();
112
- this.announceChange();
113
+ const announced = this.enterCommit === this.value;
114
+ this.enterCommit = undefined;
115
+ if (!announced) {
116
+ this.announceChange();
117
+ }
113
118
  };
114
119
  this.handleKeydown = (event) => {
115
120
  if (event.key === 'ArrowUp') {
@@ -142,9 +147,15 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
142
147
  event.preventDefault();
143
148
  };
144
149
  this.handleKeypress = (event) => {
145
- if (event.key === 'Enter' && !event.defaultPrevented) {
146
- submitImplicitly(this.form);
150
+ if (event.key !== 'Enter' || event.defaultPrevented) {
151
+ return;
152
+ }
153
+ if (this.settle()) {
154
+ this.sync();
155
+ this.enterCommit = this.value;
156
+ this.announceChange();
147
157
  }
158
+ submitImplicitly(this.form);
148
159
  };
149
160
  }
150
161
  static { this.words = 'helix-number-input'; }
@@ -44,7 +44,9 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
44
44
  * @documentation https://fusion.dev/helix/components/option
45
45
  *
46
46
  * @slot - The option's label.
47
+ * @slot description - A second line under the label, for content richer than the `description` text.
47
48
  * @slot end - Content after the label, such as a shortcut or a count.
49
+ * @slot indicator - An icon shown while the option is chosen, in place of the check the styled option draws.
48
50
  * @slot start - Content before the label, such as an icon or an avatar.
49
51
  *
50
52
  * @event {HelixSelectEvent} helix-select - The option was chosen. Cancelable: canceling it leaves the selection as it was, and keeps the browser from following an option's `href`.
@@ -57,6 +59,8 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
57
59
  * @csspart text - The label and the description under it, as one column.
58
60
  *
59
61
  * @cssstate active - While the keyboard is on the option, which is the option `aria-activedescendant` names.
62
+ * @cssstate disabled - While the option cannot be chosen.
63
+ * @cssstate has-indicator - An icon is slotted into `indicator`.
60
64
  * @cssstate selected - While the option is chosen.
61
65
  */
62
66
  export declare class HelixHeadlessOption extends HelixElement {
@@ -136,14 +140,10 @@ export declare class HelixHeadlessOption extends HelixElement {
136
140
  * ends. Matching still reads the label, so what somebody types is matched
137
141
  * against the name rather than against prose about it.
138
142
  *
139
- * **A property and not a slot, which is the departure worth explaining.** The
140
- * area rules say a content property is its slot's fallback, and that holds for
141
- * a region always drawn: `Avatar`'s initials, `Tooltip`'s text. This one is
142
- * usually absent, and a wrapped slot generates a box whether or not it is
143
- * filled, so every option without a caption would carry an empty line. The
144
- * detection that would avoid it does not exist, `:host(:has())` being invalid
145
- * and the server having no children to inspect. A property is known at render
146
- * time, so the component can decline to draw the box at all.
143
+ * For a caption richer than a string, slot it into `description`. The
144
+ * property is that slot's fallback inside the `description` part. With no
145
+ * property the slot is drawn bare, which takes no box while it is empty, so
146
+ * an option without a caption carries no empty line.
147
147
  *
148
148
  * It does not reflect, per the rule that payload state stays off the host: a
149
149
  * string is not a fact a selector needs.
@@ -198,6 +198,15 @@ export declare class HelixHeadlessOption extends HelixElement {
198
198
  * Written by the listbox.
199
199
  */
200
200
  selected: boolean;
201
+ /**
202
+ * Whether the option says it is selected while the keyboard is on it.
203
+ *
204
+ * Written by a control whose list holds suggestions, such as `FilterInput`:
205
+ * nothing there stays chosen, so the row the keyboard is on is the
206
+ * selection, as the APG combobox's list with selection that follows focus
207
+ * says. No attribute: it is coordination between elements.
208
+ */
209
+ selectionFollowsActive: boolean;
201
210
  /**
202
211
  * Where the link opens, such as `_blank`, for an option with an `href`.
203
212
  */
@@ -4,8 +4,8 @@ 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, followLink, HelixElement, HelixSelectEvent, } from '@fusion.dev/helix-core';
8
- import { html, nothing } from 'lit';
7
+ import { customElement, followLink, HelixElement, HelixSelectEvent, slotFilled, } from '@fusion.dev/helix-core';
8
+ import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  export const optionsAmong = (elements) => {
11
11
  const isOption = (element) => 'text' in element && 'value' in element && 'selected' in element;
@@ -23,6 +23,7 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
23
23
  this.label = '';
24
24
  this.rel = '';
25
25
  this.selected = false;
26
+ this.selectionFollowsActive = false;
26
27
  this.target = '';
27
28
  this.textValue = '';
28
29
  this.value = '';
@@ -59,10 +60,14 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
59
60
  super.willUpdate(changed);
60
61
  this.toggleState('selected', this.selected);
61
62
  this.toggleState('active', this.active);
63
+ this.toggleState('disabled', this.disabled);
62
64
  }
63
65
  render() {
64
66
  this.internals.role = 'option';
65
- this.internals.ariaSelected = this.href === '' ? String(this.selected) : null;
67
+ this.internals.ariaSelected =
68
+ this.href === ''
69
+ ? String(this.selected || (this.selectionFollowsActive && this.active))
70
+ : null;
66
71
  this.internals.ariaDisabled = this.disabled ? 'true' : null;
67
72
  this.internals.ariaDescription =
68
73
  this.alternate !== '' && this.alternateLabel !== '' ? this.alternateLabel : null;
@@ -73,9 +78,15 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
73
78
  <slot name="start"></slot>
74
79
  <span part="text">
75
80
  <span part="label"><slot>${this.label}</slot></span>
76
- ${this.description ? html `<span part="description">${this.description}</span>` : nothing}
81
+ ${this.description
82
+ ? html `<span part="description"><slot name="description">${this.description}</slot></span>`
83
+ : html `<slot name="description"></slot>`}
77
84
  </span>
78
85
  <slot name="end"></slot>
86
+ <slot
87
+ name="indicator"
88
+ @slotchange=${(event) => this.toggleState('has-indicator', slotFilled(event.target))}
89
+ ></slot>
79
90
  </div>`;
80
91
  }
81
92
  };
@@ -109,6 +120,9 @@ __decorate([
109
120
  __decorate([
110
121
  property({ type: Boolean })
111
122
  ], HelixHeadlessOption.prototype, "selected", void 0);
123
+ __decorate([
124
+ property({ attribute: false })
125
+ ], HelixHeadlessOption.prototype, "selectionFollowsActive", void 0);
112
126
  __decorate([
113
127
  property()
114
128
  ], HelixHeadlessOption.prototype, "target", void 0);
@@ -114,8 +114,15 @@ export declare class HelixHeadlessRadioGroup extends HelixHeadlessRadioGroup_bas
114
114
  valueMissingMessage: string;
115
115
  protected readonly keys: KeyboardNavigationController;
116
116
  protected readonly cascade: DisabledCascadeController;
117
- /** The radios, in light-DOM order. Client only: the server has no tree. */
117
+ /**
118
+ * The radios, in light-DOM order. Client only: the server has no tree.
119
+ *
120
+ * Descendants, as `Checkbox.Group` reads its boxes, so a radio inside a
121
+ * layout wrapper of yours is one of the group's. A radio held by a group
122
+ * nested inside this one is that group's.
123
+ */
118
124
  get items(): HelixHeadlessRadio[];
125
+ private observer;
119
126
  /**
120
127
  * Announces before it applies, so a listener can refuse the change.
121
128
  *
@@ -42,7 +42,10 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
42
42
  }
43
43
  static { this.words = 'helix-radio-group'; }
44
44
  get items() {
45
- return [...(this.children ?? [])].filter((child) => child instanceof HelixHeadlessRadio);
45
+ if (this.children === undefined) {
46
+ return [];
47
+ }
48
+ return [...this.querySelectorAll('*')].filter((element) => element instanceof HelixHeadlessRadio && nearestGroup(element) === this);
46
49
  }
47
50
  get answer() {
48
51
  return this.items.find((item) => item.value === this.value);
@@ -91,10 +94,14 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
91
94
  connectedCallback() {
92
95
  super.connectedCallback();
93
96
  this.addEventListener('helix-select', this.handleSelection);
97
+ this.observer = new MutationObserver(() => this.requestUpdate());
98
+ this.observer.observe(this, { childList: true, subtree: true });
94
99
  }
95
100
  disconnectedCallback() {
96
101
  super.disconnectedCallback();
97
102
  this.removeEventListener('helix-select', this.handleSelection);
103
+ this.observer?.disconnect();
104
+ this.observer = undefined;
98
105
  }
99
106
  willUpdate(changed) {
100
107
  super.willUpdate(changed);
@@ -134,3 +141,10 @@ HelixHeadlessRadioGroup = __decorate([
134
141
  customElement('helix-headless-radio-group')
135
142
  ], HelixHeadlessRadioGroup);
136
143
  export { HelixHeadlessRadioGroup };
144
+ function nearestGroup(radio) {
145
+ let parent = radio.parentElement;
146
+ while (parent && !(parent instanceof HelixHeadlessRadioGroup)) {
147
+ parent = parent.parentElement;
148
+ }
149
+ return parent;
150
+ }
@@ -66,12 +66,20 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
66
66
  * reads in its own direction. The summary and the tooltip bubble follow the
67
67
  * page's direction.
68
68
  *
69
+ * **A tap shows the reading under it**, and keeps it until a tap lands
70
+ * elsewhere. `helix-reading-change` says each move of the reading, by pointer,
71
+ * tap or key, and `helix-reading-press` says a reading was clicked, tapped, or
72
+ * pressed with Enter or Space, for a page that opens what a reading stands for.
73
+ *
69
74
  * @since 0.1.0
70
75
  * @status stable
71
76
  * @category charts
72
77
  * @aliases Scatter Plot, XY Chart, Correlation Chart, Dot Plot
73
78
  * @documentation https://fusion.dev/helix/components/scatter-chart
74
79
  *
80
+ * @event {HelixReadingChangeEvent} helix-reading-change - The reading shown moved, or was put away: `index` is the row, or `-1` for none, with its `label`, the `series` the keys rest on, and the `pointerType` that moved it.
81
+ * @event {HelixReadingPressEvent} helix-reading-press - A reading was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
82
+ *
75
83
  * @csspart gridline - One line across the plot, drawn when `gridlines` is set.
76
84
  * @csspart live - The readout: a polite live region that says the row the keys moved to. Hide it visually.
77
85
  * @csspart mark - The invisible point the tooltip is anchored to.
@@ -238,6 +246,16 @@ export declare class HelixHeadlessScatterChart extends HelixHeadlessScatterChart
238
246
  * rest on a point with.
239
247
  */
240
248
  private readonly handleMove;
249
+ /**
250
+ * The point a tap lands nearest, within twice a pointer's reach, a finger
251
+ * being broader than a cursor. It stays shown until a tap lands elsewhere.
252
+ */
253
+ private readonly handleTap;
254
+ /**
255
+ * The drawn point nearest a pointer, within `reach` pixels, `undefined` for
256
+ * none in reach, and `null` with no plot to measure.
257
+ */
258
+ private pointNear;
241
259
  private readonly handleLeave;
242
260
  }
243
261
  declare global {
@@ -4,7 +4,7 @@ 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 { CHART_PLOT, customElement, drawTooltip, extent, formatNumbers, formatWords, HelixElement, isRtl, linear, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, ticks, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { CHART_PLOT, customElement, drawTooltip, extent, formatNumbers, formatWords, HelixElement, HelixReadingChangeEvent, HelixReadingPressEvent, isRtl, linear, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, ticks, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
@@ -47,28 +47,27 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
47
47
  if (event.pointerType !== 'mouse' && event.pointerType !== 'pen') {
48
48
  return;
49
49
  }
50
- const box = this.partNamed('plot')?.getBoundingClientRect();
51
- if (!box || box.width === 0 || box.height === 0) {
50
+ const found = this.pointNear(event, REACH);
51
+ if (found === null) {
52
52
  return;
53
53
  }
54
- const across = event.clientX - box.left;
55
- const up = event.clientY - box.top;
56
- let found;
57
- let best = REACH ** 2;
58
- for (const point of this.drawn) {
59
- const distance = ((point.x / PLOT_WIDTH) * box.width - across) ** 2 +
60
- ((point.y / PLOT_HEIGHT) * box.height - up) ** 2;
61
- if (distance <= best) {
62
- best = distance;
63
- found = point;
64
- }
65
- }
66
54
  if (found !== this.nearest) {
67
55
  this.nearest = found;
68
56
  this.pointer.set(found?.row ?? -1);
69
57
  this.requestUpdate();
70
58
  }
71
59
  };
60
+ this.handleTap = (event) => {
61
+ if (event.pointerType !== 'touch') {
62
+ return;
63
+ }
64
+ const found = this.pointNear(event, REACH * 2);
65
+ if (found) {
66
+ this.nearest = found;
67
+ this.pointer.tapTo(found.row);
68
+ this.requestUpdate();
69
+ }
70
+ };
72
71
  this.handleLeave = () => {
73
72
  this.nearest = undefined;
74
73
  };
@@ -77,13 +76,18 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
77
76
  connectedCallback() {
78
77
  super.connectedCallback();
79
78
  this.pointer.axis = 'none';
79
+ this.pointer.onReading = (heard, detail) => this.dispatchEvent(heard === 'press'
80
+ ? new HelixReadingPressEvent(detail)
81
+ : new HelixReadingChangeEvent(detail));
80
82
  this.addEventListener('pointermove', this.handleMove);
81
83
  this.addEventListener('pointerleave', this.handleLeave);
84
+ this.addEventListener('pointerup', this.handleTap);
82
85
  }
83
86
  disconnectedCallback() {
84
87
  super.disconnectedCallback();
85
88
  this.removeEventListener('pointermove', this.handleMove);
86
89
  this.removeEventListener('pointerleave', this.handleLeave);
90
+ this.removeEventListener('pointerup', this.handleTap);
87
91
  }
88
92
  seriesOf(cell) {
89
93
  const depth = this.measures.ups.indexOf(cell);
@@ -253,6 +257,25 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
253
257
  y: `${point.y}%`,
254
258
  }, (cell) => this.seriesOf(cell), this.pointer);
255
259
  }
260
+ pointNear(event, reach) {
261
+ const box = this.partNamed('plot')?.getBoundingClientRect();
262
+ if (!box || box.width === 0 || box.height === 0) {
263
+ return null;
264
+ }
265
+ const across = event.clientX - box.left;
266
+ const up = event.clientY - box.top;
267
+ let found;
268
+ let best = reach ** 2;
269
+ for (const point of this.drawn) {
270
+ const distance = ((point.x / PLOT_WIDTH) * box.width - across) ** 2 +
271
+ ((point.y / PLOT_HEIGHT) * box.height - up) ** 2;
272
+ if (distance <= best) {
273
+ best = distance;
274
+ found = point;
275
+ }
276
+ }
277
+ return found;
278
+ }
256
279
  };
257
280
  __decorate([
258
281
  words,
@@ -290,6 +290,14 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
290
290
  * from the origin to the thumb as `--slider-from` and `--slider-to`.
291
291
  */
292
292
  private get fillStyle();
293
+ /**
294
+ * The range's axis, written on it, so a vertical slider is vertical to the
295
+ * accessibility tree with no stylesheet: Chromium reads a range's
296
+ * orientation off its writing mode and ignores `aria-orientation` there.
297
+ * `direction: rtl` puts the maximum at the top, which `vertical-lr` alone
298
+ * puts at the foot.
299
+ */
300
+ private get axis();
293
301
  /**
294
302
  * A value snapped to the step, counted from `min` the way a range counts it.
295
303
  *
@@ -112,10 +112,13 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
112
112
  get fillStyle() {
113
113
  const fraction = this.fraction;
114
114
  if (!this.origin) {
115
- return `--slider-fraction: ${fraction}`;
115
+ return `--slider-fraction: ${fraction}${this.axis}`;
116
116
  }
117
117
  const at = { center: 0.5, end: 1, start: 0 }[this.origin] ?? 0;
118
- return `--slider-fraction: ${fraction}; --slider-from: ${Math.min(at, fraction)}; --slider-to: ${Math.max(at, fraction)}`;
118
+ return `--slider-fraction: ${fraction}; --slider-from: ${Math.min(at, fraction)}; --slider-to: ${Math.max(at, fraction)}${this.axis}`;
119
+ }
120
+ get axis() {
121
+ return this.orientation === 'vertical' ? '; writing-mode: vertical-lr; direction: rtl' : '';
119
122
  }
120
123
  snapped(at) {
121
124
  if (!(this.step > 0)) {
@@ -208,7 +211,7 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
208
211
  id=${which === 'start' ? 'control' : 'control-end'}
209
212
  data-thumb=${which}
210
213
  type="range"
211
- style="--slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}"
214
+ style="--slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}${this.axis}"
212
215
  min=${this.min}
213
216
  max=${this.max}
214
217
  step=${this.step}
@@ -174,6 +174,7 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
174
174
  const named = { name, named: name ? 'yes' : 'no' };
175
175
  const grid = this.gridCell;
176
176
  const control = grid ? '-1' : nothing;
177
+ const width = Math.min(4096, Math.max(HelixHeadlessTableColumn_1.NARROWEST, this.drawn));
177
178
  return html `<span
178
179
  part="column"
179
180
  role=${grid ? 'columnheader' : nothing}
@@ -197,8 +198,8 @@ let HelixHeadlessTableColumn = class HelixHeadlessTableColumn extends HelixEleme
197
198
  aria-orientation="vertical"
198
199
  aria-valuemin=${HelixHeadlessTableColumn_1.NARROWEST}
199
200
  aria-valuemax="4096"
200
- aria-valuenow=${Math.max(HelixHeadlessTableColumn_1.NARROWEST, this.drawn)}
201
- aria-valuetext=${formatWords(this, 'widthLabel', { width: this.drawn })}
201
+ aria-valuenow=${width}
202
+ aria-valuetext=${formatWords(this, 'widthLabel', { width })}
202
203
  @keydown=${this.handleResizeKey}
203
204
  @pointerdown=${this.handleResizeStart}
204
205
  @pointermove=${this.handleResizeMove}
@@ -24,6 +24,16 @@ export interface TableGridOptions {
24
24
  part: (name: string) => HTMLElement | null;
25
25
  /** The rows in the order they are drawn. */
26
26
  rows: () => Element[];
27
+ /**
28
+ * The words each row is found by when a reader types, in the order the rows
29
+ * are drawn.
30
+ */
31
+ rowWords: () => string[];
32
+ /**
33
+ * Selects every row from the row last chosen, or from row `from` where none
34
+ * was, to row `to`, and says whether the table selects at all.
35
+ */
36
+ selectRange: (to: number, from: number) => boolean;
27
37
  /** Selects every row for Control or Command with A, and says whether it did. */
28
38
  selectAll: () => boolean;
29
39
  /** Whether the table is a grid now. */
@@ -31,8 +41,8 @@ export interface TableGridOptions {
31
41
  }
32
42
  /**
33
43
  * The keyboard of a `grid` table: one tab stop among its cells, the arrow
34
- * keys, Home, End and the Page keys between them, and Enter or F2 into a
35
- * cell's controls.
44
+ * keys, Home, End and the Page keys between them, Enter or F2 into a cell's
45
+ * controls, typeahead over the rows, and Shift ranges of rows.
36
46
  *
37
47
  * **Tab leaves the grid.** The controls in a cell are the consumer's, so their
38
48
  * tab order is left as written: Tab on a cell, or past the last control of a
@@ -59,6 +69,8 @@ export declare class TableGridController implements ReactiveController {
59
69
  private pointing;
60
70
  /** The cells given a `tabindex`, so a table that stops being a grid takes it back. */
61
71
  private readonly given;
72
+ /** The letters a reader has typed at the rows. */
73
+ private readonly typeahead;
62
74
  constructor(host: ReactiveControllerHost & HTMLElement, options: TableGridOptions);
63
75
  hostConnected(): void;
64
76
  hostDisconnected(): void;
@@ -1,4 +1,4 @@
1
- import { deepActiveElement, deepContains, focusableElements, gridMove, inlineStep, pageRows, scrollingAncestor, scrollRoom, selectsAll, whenHydrated, } from '@fusion.dev/helix-core';
1
+ import { deepActiveElement, deepContains, focusableElements, gridMove, inlineStep, pageRows, scrollingAncestor, scrollRoom, selectsAll, Typeahead, typesCharacter, whenHydrated, } from '@fusion.dev/helix-core';
2
2
  const HEADS = { grip: 'grip-head', select: 'select-all' };
3
3
  const shown = (element) => {
4
4
  const hidden = element.getAttribute('hidden');
@@ -13,6 +13,7 @@ export class TableGridController {
13
13
  this.focusWanted = false;
14
14
  this.pointing = false;
15
15
  this.given = new Set();
16
+ this.typeahead = new Typeahead();
16
17
  this.handleKeydown = (event) => {
17
18
  if (!this.options.on() || event.defaultPrevented || event.altKey) {
18
19
  return;
@@ -35,6 +36,25 @@ export class TableGridController {
35
36
  }
36
37
  return;
37
38
  }
39
+ if (typesCharacter(event) && (event.key !== ' ' || this.typeahead.typing())) {
40
+ const found = this.typeahead.match(event.key, this.options.rowWords(), place.row);
41
+ if (found >= 0) {
42
+ event.preventDefault();
43
+ if (found !== place.row) {
44
+ this.moveTo({
45
+ column: Math.max(0, Math.min(place.column, this.widthOf(found) - 1)),
46
+ row: found,
47
+ });
48
+ }
49
+ }
50
+ return;
51
+ }
52
+ if (event.key === ' ' && event.shiftKey && place.row >= 0) {
53
+ if (this.options.selectRange(place.row, place.row)) {
54
+ event.preventDefault();
55
+ }
56
+ return;
57
+ }
38
58
  if (event.key === 'Enter' || event.key === ' ' || event.key === 'F2') {
39
59
  if (event.key !== 'F2' && this.options.activate(place, event)) {
40
60
  event.preventDefault();
@@ -57,6 +77,12 @@ export class TableGridController {
57
77
  }
58
78
  event.preventDefault();
59
79
  to.column = Math.max(0, Math.min(to.column, this.widthOf(to.row) - 1));
80
+ if (event.shiftKey &&
81
+ (event.key === 'ArrowUp' || event.key === 'ArrowDown') &&
82
+ place.row >= 0 &&
83
+ to.row >= 0) {
84
+ this.options.selectRange(to.row, place.row);
85
+ }
60
86
  if (to.row !== place.row || to.column !== place.column) {
61
87
  this.moveTo(to);
62
88
  }
@@ -76,7 +76,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
76
76
  * through cell by cell, the headings included, as React Aria's table is. The
77
77
  * table is then a `grid` and each value a `gridcell`, and the table gives
78
78
  * every cell a `tabindex` so focus can rest on it: set `grid` only where the
79
- * cells may carry one. For typeahead and range selection, use `DataTable`.
79
+ * cells may carry one.
80
80
  *
81
81
  * | Key | What it does |
82
82
  * |---|---|
@@ -90,7 +90,10 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
90
90
  * | Enter, Space on a sortable heading | Sorts by the column. |
91
91
  * | Space on a row, Enter or Space on its checkbox cell | Selects the row, in a `selectable` table. |
92
92
  * | Enter or Space on a grip cell | Picks the row up, or drops it, in a `reorderable` table. Up, Down, Home, and End move a row picked up, and Escape puts it back. |
93
+ * | Shift with Down or Up | Selects the rows it moves across, in a `selectable` table. |
94
+ * | Shift with Space | Selects every row from the one last chosen, in a `selectable` table. |
93
95
  * | Control or Command with A | Selects every row, in a `selectable` table. |
96
+ * | A letter or a word | Moves to the next row whose first cell starts with what you typed. |
94
97
  * | Tab | Leaves the table. |
95
98
  *
96
99
  * The CSS custom properties listed for this component are written by it and
@@ -105,7 +108,9 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
105
108
  * @documentation https://fusion.dev/helix/components/table
106
109
  *
107
110
  * @slot - The table's rows.
111
+ * @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.
108
112
  * @slot heading - Words that name the table, shown above it, such as a heading.
113
+ * @slot loading - Content shown while the rows load, such as a spinner. Falls back to `loading-text`, which the live region still says.
109
114
  *
110
115
  * @event {HelixBeforeReorderEvent} helix-before-reorder - Fired when a row is dropped at a new place, in a `reorderable` table, with every row's key in the new order. Cancelable: cancel it to keep the row where it is.
111
116
  * @event {HelixReorderEvent} helix-reorder - Fired after `helix-before-reorder` when nobody canceled it. Move your rows to the order in `keys`. Cancelable: cancel it to say the move did not happen.
@@ -144,6 +149,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
144
149
  * `registerWords('de', { 'helix-table': { selectAllLabel: 'Alle Zeilen auswählen' } })`.
145
150
  */
146
151
  static words: string;
152
+ /** The empty and loading lines are the `empty` and `loading` slots. */
153
+ protected statusSlots: boolean;
147
154
  /**
148
155
  * The word for an ascending sort, in a `sorted-label` that writes `{order}`.
149
156
  *
@@ -383,9 +390,25 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
383
390
  private readonly handleToggle;
384
391
  /** Every row's key, in the order the rows are written. */
385
392
  private get everyKey();
386
- /** The checkbox in the heading: every row, or none where every row is chosen. */
393
+ /** The keys of the rows that can be chosen: every row that is not `disabled`. */
394
+ private get enabledKeys();
395
+ /** Whether the row of that key is `disabled`. */
396
+ private isDisabled;
397
+ /**
398
+ * The checkbox in the heading: every row that can be chosen, or none of
399
+ * them where each already is. A `disabled` row keeps what it had.
400
+ */
387
401
  private chooseAll;
388
- /** One row's checkbox, ticked or not. */
402
+ /** The key of the row a Shift range runs from: the one last chosen. */
403
+ private anchorKey;
404
+ /**
405
+ * Selects every row from the one last chosen to the drawn row `to`, keeping
406
+ * what was already selected. With no row to run from, the range is the row
407
+ * the reader is on, `from`, and it becomes the one to run from. Says whether
408
+ * the table selects at all.
409
+ */
410
+ private selectRange;
411
+ /** One row's checkbox, ticked or not. It becomes the row a range runs from. */
389
412
  private chooseRow;
390
413
  /**
391
414
  * Enter, Space or F2 on a cell of a `grid` table, and whether it did
@@ -498,7 +521,10 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
498
521
  * assignment has not happened.
499
522
  */
500
523
  static shadowRootOptions: ShadowRootInit;
501
- /** The rows: every child that is not a column, which is what the body slot is assigned. */
524
+ /**
525
+ * The rows: every child that is not a column, a heading, or the content of
526
+ * the `empty` or `loading` slot, which is what the body slot is assigned.
527
+ */
502
528
  protected get slotted(): Element[];
503
529
  private assign;
504
530
  /**