@fusion.dev/helix-headless 0.147.0 → 0.148.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 (141) hide show
  1. package/custom-elements.json +4512 -790
  2. package/dist/accordion/item/accordion-item.d.ts +5 -2
  3. package/dist/accordion/item/accordion-item.js +11 -7
  4. package/dist/accordion/root/accordion-root.js +4 -6
  5. package/dist/announcement/root/announcement-root.d.ts +56 -5
  6. package/dist/announcement/root/announcement-root.js +124 -21
  7. package/dist/area-chart/root/area-chart-root.js +3 -4
  8. package/dist/badge/count.d.ts +14 -0
  9. package/dist/badge/count.js +20 -0
  10. package/dist/badge/root/badge-root.js +8 -22
  11. package/dist/blockquote/root/blockquote-root.d.ts +66 -8
  12. package/dist/blockquote/root/blockquote-root.js +85 -8
  13. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +2 -2
  14. package/dist/button-group/arrows.d.ts +14 -0
  15. package/dist/button-group/arrows.js +10 -0
  16. package/dist/button-group/root/button-group-root.d.ts +141 -3
  17. package/dist/button-group/root/button-group-root.js +142 -2
  18. package/dist/canvas/edge-pan.d.ts +61 -0
  19. package/dist/canvas/edge-pan.js +63 -0
  20. package/dist/canvas/node/canvas-node.d.ts +12 -2
  21. package/dist/canvas/node/canvas-node.js +6 -2
  22. package/dist/canvas/root/canvas-document.d.ts +10 -0
  23. package/dist/canvas/root/canvas-document.js +1 -0
  24. package/dist/canvas/root/canvas-root.d.ts +107 -5
  25. package/dist/canvas/root/canvas-root.js +140 -36
  26. package/dist/canvas/snap.d.ts +42 -0
  27. package/dist/canvas/snap.js +32 -0
  28. package/dist/carousel/root/carousel-root.d.ts +4 -3
  29. package/dist/carousel/root/carousel-root.js +2 -6
  30. package/dist/chat/root/chat-root.d.ts +13 -3
  31. package/dist/chat/root/chat-root.js +2 -6
  32. package/dist/checkbox-group/root/checkbox-group-root.d.ts +13 -0
  33. package/dist/checkbox-group/root/checkbox-group-root.js +18 -10
  34. package/dist/checkbox-group/select-all.d.ts +11 -0
  35. package/dist/checkbox-group/select-all.js +5 -0
  36. package/dist/code-block/root/code-block-root.d.ts +8 -6
  37. package/dist/code-block/root/code-block-root.js +4 -3
  38. package/dist/color-picker/root/color-picker-root.js +6 -12
  39. package/dist/color-picker/wheel.d.ts +11 -0
  40. package/dist/color-picker/wheel.js +9 -0
  41. package/dist/combobox/root/combobox-root.d.ts +70 -11
  42. package/dist/combobox/root/combobox-root.js +68 -18
  43. package/dist/command/root/command-root.d.ts +4 -3
  44. package/dist/data-table/root/data-table-root.js +17 -44
  45. package/dist/description-list/item/description-list-item.d.ts +4 -0
  46. package/dist/description-list/item/description-list-item.js +7 -1
  47. package/dist/divider/root/divider-root.d.ts +45 -16
  48. package/dist/divider/root/divider-root.js +28 -4
  49. package/dist/drilldown/item/drilldown-item.js +3 -4
  50. package/dist/element-events.generated.js +1 -0
  51. package/dist/empty/root/empty-root.d.ts +17 -3
  52. package/dist/empty/root/empty-root.js +29 -5
  53. package/dist/file-input/root/file-input-root.js +3 -4
  54. package/dist/file-input/status.js +2 -1
  55. package/dist/filter-input/chip/filter-chip.js +2 -3
  56. package/dist/format-date/fields.d.ts +20 -0
  57. package/dist/format-date/fields.js +14 -0
  58. package/dist/format-date/options.d.ts +2 -1
  59. package/dist/format-date/options.js +9 -38
  60. package/dist/format-date/root/format-date-root.d.ts +35 -3
  61. package/dist/format-date/root/format-date-root.js +24 -32
  62. package/dist/graph/root/graph-root.d.ts +23 -5
  63. package/dist/graph/root/graph-root.js +17 -20
  64. package/dist/heatmap/root/heatmap-root.d.ts +12 -0
  65. package/dist/heatmap/root/heatmap-root.js +26 -11
  66. package/dist/image/root/image-root.d.ts +11 -0
  67. package/dist/kbd/root/kbd-root.js +1 -2
  68. package/dist/legend/root/legend-root.js +4 -17
  69. package/dist/list/root/list-root.js +8 -22
  70. package/dist/listbox/root/listbox-root.d.ts +119 -24
  71. package/dist/listbox/root/listbox-root.js +130 -132
  72. package/dist/map/marker/map-marker.d.ts +2 -1
  73. package/dist/map/root/map-root.d.ts +40 -2
  74. package/dist/map/root/map-root.js +64 -23
  75. package/dist/megamenu/root/megamenu-root.js +4 -11
  76. package/dist/menu/root/menu-root.d.ts +2 -2
  77. package/dist/navigation/root/navigation-root.js +5 -8
  78. package/dist/notifications/group/notification-group.d.ts +8 -1
  79. package/dist/notifications/group/notification-group.js +12 -6
  80. package/dist/notifications/notification/notification.d.ts +12 -1
  81. package/dist/notifications/notification/notification.js +3 -1
  82. package/dist/notifications/root/notifications-root.d.ts +41 -5
  83. package/dist/notifications/root/notifications-root.js +27 -7
  84. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -1
  85. package/dist/object-inspector/root/object-inspector-root.js +21 -11
  86. package/dist/pagination/size/pagination-size.d.ts +1 -0
  87. package/dist/pagination/size/pagination-size.js +1 -1
  88. package/dist/pin-input/code.d.ts +23 -0
  89. package/dist/pin-input/code.js +25 -0
  90. package/dist/pin-input/root/pin-input-root.js +9 -23
  91. package/dist/qr-code/root/qr-code-root.d.ts +14 -3
  92. package/dist/qr-code/root/qr-code-root.js +19 -5
  93. package/dist/scatter-chart/measures.d.ts +22 -0
  94. package/dist/scatter-chart/measures.js +11 -0
  95. package/dist/scatter-chart/root/scatter-chart-root.d.ts +2 -6
  96. package/dist/scatter-chart/root/scatter-chart-root.js +6 -19
  97. package/dist/select/root/select-root.d.ts +56 -8
  98. package/dist/select/root/select-root.js +33 -9
  99. package/dist/speed-dial/group/speed-dial-group.d.ts +48 -0
  100. package/dist/speed-dial/group/speed-dial-group.js +43 -0
  101. package/dist/speed-dial/index.d.ts +1 -0
  102. package/dist/speed-dial/index.js +1 -0
  103. package/dist/speed-dial/item/speed-dial-item.d.ts +26 -1
  104. package/dist/speed-dial/item/speed-dial-item.js +41 -3
  105. package/dist/speed-dial/long-press.d.ts +50 -0
  106. package/dist/speed-dial/long-press.js +48 -0
  107. package/dist/speed-dial/root/speed-dial-root.d.ts +80 -1
  108. package/dist/speed-dial/root/speed-dial-root.js +114 -5
  109. package/dist/splitter/root/splitter-root.d.ts +7 -3
  110. package/dist/splitter/root/splitter-root.js +16 -3
  111. package/dist/table/cell/table-cell.d.ts +8 -1
  112. package/dist/table/cell/table-cell.js +6 -2
  113. package/dist/table/grid-controller.d.ts +117 -0
  114. package/dist/table/grid-controller.js +293 -0
  115. package/dist/table/root/table-root.d.ts +47 -3
  116. package/dist/table/root/table-root.js +81 -24
  117. package/dist/table/row/table-row.d.ts +13 -1
  118. package/dist/table/row/table-row.js +20 -6
  119. package/dist/tag/group/tag-group.js +6 -15
  120. package/dist/time-picker/root/time-picker-root.d.ts +1 -0
  121. package/dist/time-picker/root/time-picker-root.js +3 -0
  122. package/dist/toast/item/toast-item.d.ts +6 -3
  123. package/dist/toast/item/toast-item.js +1 -0
  124. package/dist/toast/root/toast-root.d.ts +13 -1
  125. package/dist/toast/root/toast-root.js +20 -14
  126. package/dist/toggle/group/toggle-group.js +3 -5
  127. package/dist/toolbar/root/toolbar-root.d.ts +31 -0
  128. package/dist/toolbar/root/toolbar-root.js +39 -8
  129. package/dist/tooltip/root/tooltip-root.js +1 -2
  130. package/dist/video/root/video-root.d.ts +11 -0
  131. package/dist/video/root/video-root.js +15 -1
  132. package/dist/virtual-list/root/virtual-list-root.js +6 -18
  133. package/lint.json +1 -1
  134. package/package.json +5 -5
  135. package/types/jsx.d.ts +2005 -266
  136. package/types/svelte.d.ts +1 -0
  137. package/types/vue.d.ts +338 -62
  138. package/dist/kbd/keys.d.ts +0 -29
  139. package/dist/kbd/keys.js +0 -52
  140. package/dist/tooltip/focus-target.d.ts +0 -6
  141. package/dist/tooltip/focus-target.js +0 -9
@@ -4,20 +4,31 @@ 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 { anchorElement, customElement, drawIcon, HelixElement, KeyboardNavigationController, MixinControllerOpen, PopoverController, slottedWords, tellDisclosure, typeaheadBy, } from '@fusion.dev/helix-core';
7
+ import { anchorElement, customElement, drawIcon, focusTarget, HelixElement, KeyboardNavigationController, linkDescription, MixinControllerOpen, PopoverController, slottedWords, tellDisclosure, typeaheadByWords, unlinkDescription, } from '@fusion.dev/helix-core';
8
8
  import { plus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
+ import { LongPress } from '../long-press.js';
12
+ const navigable = (item) => ({
13
+ contains: (node) => item.contains(node),
14
+ disabled: item.disabled,
15
+ focusItem: () => item.focusItem(),
16
+ select: () => item.select(),
17
+ selected: item.active,
18
+ words: slottedWords(item, ['']),
19
+ });
11
20
  let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControllerOpen(HelixElement) {
12
21
  constructor() {
13
22
  super(...arguments);
14
23
  this.label = 'Actions';
24
+ this.longPressLabel = 'Long press or press Alt+Down to show the actions';
15
25
  this.offset = 12;
26
+ this.openOn = 'click';
16
27
  this.placement = 'top-end';
17
28
  this.keys = new KeyboardNavigationController(this, {
18
- items: () => (this.open ? this.items : []),
29
+ items: () => (this.open ? this.items.map(navigable) : []),
19
30
  selects: () => false,
20
- typeahead: typeaheadBy((item) => slottedWords(item, [''])),
31
+ typeahead: typeaheadByWords(),
21
32
  });
22
33
  this.focusOnOpen = false;
23
34
  this.panel = new PopoverController(this, {
@@ -29,13 +40,48 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
29
40
  placement: () => this.placement,
30
41
  surface: () => this.partNamed('actions'),
31
42
  });
43
+ this.longPress = new LongPress(this, {
44
+ onLongPress: () => {
45
+ if (!this.open) {
46
+ this.focusOnOpen = this.requestOpen(true);
47
+ }
48
+ },
49
+ });
32
50
  this.handleTriggerClick = () => {
33
51
  if (this.open) {
34
52
  this.requestOpen(false);
35
53
  return;
36
54
  }
55
+ if (this.pressHeld) {
56
+ return;
57
+ }
37
58
  this.focusOnOpen = this.requestOpen(true);
38
59
  };
60
+ this.handleHeldClick = (event) => {
61
+ if (this.pressHeld && this.longPress.takeClick()) {
62
+ event.stopImmediatePropagation();
63
+ event.preventDefault();
64
+ }
65
+ };
66
+ this.handleTriggerPointerDown = (event) => {
67
+ if (this.pressHeld && !this.open) {
68
+ this.longPress.down(event);
69
+ }
70
+ };
71
+ this.handleTriggerPointerMove = (event) => {
72
+ this.longPress.move(event);
73
+ };
74
+ this.handleTriggerPointerUp = () => {
75
+ this.longPress.up();
76
+ };
77
+ this.handleTriggerPointerCancel = () => {
78
+ this.longPress.cancel();
79
+ };
80
+ this.handleTriggerContextMenu = (event) => {
81
+ if (this.pressHeld) {
82
+ event.preventDefault();
83
+ }
84
+ };
39
85
  this.handleSelect = (event) => {
40
86
  queueMicrotask(() => {
41
87
  if (!event.defaultPrevented) {
@@ -45,6 +91,12 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
45
91
  };
46
92
  this.handleKeydown = (event) => {
47
93
  if (!this.open) {
94
+ if (this.pressHeld &&
95
+ event.altKey &&
96
+ (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
97
+ event.preventDefault();
98
+ this.focusOnOpen = this.requestOpen(true);
99
+ }
48
100
  return;
49
101
  }
50
102
  if (event.key === 'Tab') {
@@ -58,13 +110,26 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
58
110
  this.close();
59
111
  event.preventDefault();
60
112
  };
113
+ this.handleTriggerFocusIn = () => {
114
+ if (this.pressHeld) {
115
+ this.describeTrigger(true);
116
+ }
117
+ };
61
118
  }
62
119
  get trigger() {
63
120
  return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this.partNamed('trigger') ?? this);
64
121
  }
65
122
  get items() {
66
123
  const slot = this.renderRoot?.querySelector('slot:not([name])');
67
- return (slot?.assignedElements?.() ?? []).filter((one) => one.isSpeedDialItem === true);
124
+ const isItem = (one) => one.isSpeedDialItem === true;
125
+ return (slot?.assignedElements?.() ?? []).flatMap((one) => isItem(one)
126
+ ? [one]
127
+ : one.isSpeedDialGroup === true
128
+ ? [...one.children].filter(isItem)
129
+ : []);
130
+ }
131
+ get pressHeld() {
132
+ return this.openOn === 'long-press';
68
133
  }
69
134
  focus(options) {
70
135
  const trigger = this.trigger;
@@ -84,11 +149,18 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
84
149
  super.connectedCallback();
85
150
  this.addEventListener('helix-select', this.handleSelect);
86
151
  this.addEventListener('keydown', this.handleKeydown);
152
+ this.addEventListener('click', this.handleHeldClick, true);
87
153
  }
88
154
  disconnectedCallback() {
89
155
  super.disconnectedCallback();
156
+ if (this.described) {
157
+ unlinkDescription(this.described, this);
158
+ this.described = undefined;
159
+ this.internals.ariaLabel = null;
160
+ }
90
161
  this.removeEventListener('helix-select', this.handleSelect);
91
162
  this.removeEventListener('keydown', this.handleKeydown);
163
+ this.removeEventListener('click', this.handleHeldClick, true);
92
164
  }
93
165
  announce() {
94
166
  const trigger = this.trigger;
@@ -97,9 +169,27 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
97
169
  }
98
170
  tellDisclosure(trigger, this.open, 'menu');
99
171
  }
172
+ describeTrigger(light = false) {
173
+ const trigger = this.trigger;
174
+ const slotted = this.pressHeld && trigger !== this && trigger !== this.partNamed('trigger');
175
+ const control = slotted ? focusTarget(trigger) : undefined;
176
+ if (this.described && this.described !== control) {
177
+ unlinkDescription(this.described, this);
178
+ this.described = undefined;
179
+ }
180
+ if (control && control !== this.described && linkDescription(control, this, light)) {
181
+ this.described = control;
182
+ }
183
+ this.internals.ariaLabel = this.described ? this.longPressLabel : null;
184
+ }
185
+ firstUpdated(changed) {
186
+ super.firstUpdated(changed);
187
+ requestAnimationFrame(() => this.describeTrigger());
188
+ }
100
189
  updated(changed) {
101
190
  super.updated?.(changed);
102
191
  this.announce();
192
+ this.describeTrigger();
103
193
  if (this.open && this.focusOnOpen) {
104
194
  this.focusOnOpen = false;
105
195
  this.updateComplete.then(() => {
@@ -110,10 +200,22 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
110
200
  }
111
201
  }
112
202
  renderTrigger() {
113
- return html `<slot name="trigger" @click=${this.handleTriggerClick}>
203
+ const held = this.pressHeld;
204
+ return html `<slot
205
+ name="trigger"
206
+ @click=${this.handleTriggerClick}
207
+ @contextmenu=${this.handleTriggerContextMenu}
208
+ @focusin=${this.handleTriggerFocusIn}
209
+ @pointercancel=${this.handleTriggerPointerCancel}
210
+ @pointerdown=${this.handleTriggerPointerDown}
211
+ @pointermove=${this.handleTriggerPointerMove}
212
+ @pointerup=${this.handleTriggerPointerUp}
213
+ @slotchange=${() => this.describeTrigger()}
214
+ >
114
215
  <button
115
216
  part="trigger"
116
217
  type="button"
218
+ aria-describedby=${held ? 'long-press' : nothing}
117
219
  aria-expanded=${this.open ? 'true' : 'false'}
118
220
  aria-haspopup="menu"
119
221
  aria-label=${this.label}
@@ -121,6 +223,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
121
223
  ${''}
122
224
  ${drawIcon(plus)}
123
225
  </button>
226
+ ${held ? html `<span id="long-press" hidden>${this.longPressLabel}</span>` : nothing}
124
227
  </slot>`;
125
228
  }
126
229
  render() {
@@ -139,9 +242,15 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
139
242
  __decorate([
140
243
  property()
141
244
  ], HelixHeadlessSpeedDial.prototype, "label", void 0);
245
+ __decorate([
246
+ property({ attribute: 'long-press-label' })
247
+ ], HelixHeadlessSpeedDial.prototype, "longPressLabel", void 0);
142
248
  __decorate([
143
249
  property({ type: Number })
144
250
  ], HelixHeadlessSpeedDial.prototype, "offset", void 0);
251
+ __decorate([
252
+ property({ attribute: 'open-on' })
253
+ ], HelixHeadlessSpeedDial.prototype, "openOn", void 0);
145
254
  __decorate([
146
255
  property()
147
256
  ], HelixHeadlessSpeedDial.prototype, "placement", void 0);
@@ -13,8 +13,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
13
13
  * added at `firstUpdated` and is not in the server markup. Before it appears,
14
14
  * both dividers work on their own.
15
15
  *
16
- * The divider has `role="separator"` and `aria-valuenow`, and `aria-controls`
17
- * names the start pane. Drag it with a pointer or move it with the arrow keys,
16
+ * The divider has `role="separator"` and `aria-valuenow`, with
17
+ * `aria-valuetext` saying the start pane's share as a percentage in the
18
+ * reader's locale, and `aria-controls` names the start pane. Drag it with a pointer or move it with the arrow keys,
18
19
  * press `Home` or `End` to move it to its bounds, and press `Enter` to collapse
19
20
  * the start pane and open it again. A drag in progress is tracked as a state, so both dividers a
20
21
  * corner moves stay lit for as long as they move.
@@ -36,13 +37,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
36
37
  * @documentation https://fusion.dev/helix/components/splitter
37
38
  *
38
39
  * @slot end - The pane after the divider.
40
+ * @slot handle - A mark drawn at the middle of the divider, such as a grip icon. It takes no focus and no pointer of its own: a press on it is a press on the divider.
39
41
  * @slot start - The pane before the divider.
40
42
  *
41
43
  * @event {HelixBeforeCollapseEvent} helix-before-collapse - Fired before a pane shuts or reopens, whether or not the resize that caused it was allowed. It carries its own position, because a shut pane sits at `collapsed-size`. Cancelable: canceling it leaves the pane as it was and the divider where the gesture put it.
42
44
  * @event {HelixBeforeResizeEvent} helix-before-resize - Fired before the divider settles, and only when the gesture moved it. A press that moved nothing, a drag that came back, an arrow key with no room left, and a corner that moved only the other divider do not fire it. Cancelable: canceling it leaves the position to you and keeps the rest of the gesture, so a collapse the gesture reached still fires `helix-before-collapse`, which you can also cancel. A dragged divider stays where it was dropped until you set your own `position`.
43
45
  * @event {HelixCollapseEvent} helix-collapse - Fired after a pane shuts or reopens, with the side that is shut, or empty where both are open.
44
46
  * @event {HelixResizedEvent} helix-resized - Fired after the divider settles somewhere new, with the new position.
45
- * @event {Event} input - Fired each time a drag moves the divider, before it settles, as a range input does while it is dragged. Read `position` for where it is now.
47
+ * @event {Event} input - Fired each time the divider moves, as a range input does: on each step of a drag, and before `helix-resized` for a key, a double click or a corner. Read `position` for where it is now.
46
48
  *
47
49
  * @csspart corner - The handle where two dividers cross, added on the client.
48
50
  * @csspart end - The box that holds the second pane.
@@ -52,7 +54,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
52
54
  * @cssproperty --splitter-corner-at - Where a nested splitter crosses this one, as a percentage.
53
55
  * @cssproperty --splitter-position - Where the divider sits, as a percentage of the box.
54
56
  *
57
+ * @cssstate collapsed - While a pane is shut. `data-collapsed` on the host says which.
55
58
  * @cssstate dragging - While the pointer or a corner holds the divider.
59
+ * @cssstate vertical - While the panes are one above the other, whether `orientation` says so or `auto` turned them.
56
60
  */
57
61
  export declare class HelixHeadlessSplitter extends HelixElement {
58
62
  /**
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessSplitter_1;
8
- import { AriaForwardController, capturePointer, closestInstance, commaList, customElement, HelixBeforeCollapseEvent, HelixBeforeResizeEvent, HelixCollapseEvent, HelixElement, HelixResizedEvent, inlineStep, isRtl, requestChange, } from '@fusion.dev/helix-core';
8
+ import { AriaForwardController, capturePointer, closestInstance, commaList, customElement, formatPercent, HelixBeforeCollapseEvent, HelixBeforeResizeEvent, HelixCollapseEvent, HelixElement, HelixResizedEvent, inlineStep, isRtl, requestChange, } from '@fusion.dev/helix-core';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { snapTo } from '../snap.js';
@@ -144,6 +144,9 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
144
144
  const previous = this.position;
145
145
  this.positionState = value;
146
146
  this.requestUpdate('position', previous);
147
+ if (value !== previous) {
148
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
149
+ }
147
150
  this.dispatchEvent(new HelixResizedEvent({ value: String(value) }));
148
151
  },
149
152
  event: (value) => new HelixBeforeResizeEvent({ value: String(value) }),
@@ -447,6 +450,7 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
447
450
  super.firstUpdated(changed);
448
451
  this.watchWidth();
449
452
  this.findOuter();
453
+ this.finishHydration();
450
454
  }
451
455
  updated(changed) {
452
456
  super.updated(changed);
@@ -473,6 +477,8 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
473
477
  willUpdate(changed) {
474
478
  super.willUpdate(changed);
475
479
  this.toggleState('dragging', this.dragging);
480
+ this.toggleState('collapsed', this.collapsed !== undefined);
481
+ this.toggleState('vertical', this.vertical);
476
482
  }
477
483
  render() {
478
484
  const position = this.showing;
@@ -494,7 +500,11 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
494
500
  aria-valuemax=${this.collapsed ? 100 : this.clamp(this.maxPercent)}
495
501
  aria-valuemin=${this.collapsed ? 0 : this.clamp(this.minPercent)}
496
502
  aria-valuenow=${Math.round(position)}
497
- aria-valuetext=${this.collapsed ? this.collapsedLabel : nothing}
503
+ aria-valuetext=${this.collapsed
504
+ ? this.collapsedLabel
505
+ : this.hydrating
506
+ ? nothing
507
+ : formatPercent(Math.round(position) / 100)}
498
508
  ?data-dragging=${this.dragging}
499
509
  ?data-flush=${this.flush}
500
510
  @dblclick=${this.handleDoubleClick}
@@ -503,7 +513,9 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
503
513
  @pointerdown=${this.handlePointerDown}
504
514
  @pointermove=${this.handlePointerMove}
505
515
  @pointerup=${this.handlePointerUp}
506
- ></div>
516
+ >
517
+ <slot name="handle"></slot>
518
+ </div>
507
519
  <div part="end" ?data-dragging=${this.dragging}>
508
520
  <slot name="end"></slot>
509
521
  </div>
@@ -516,6 +528,7 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
516
528
  aria-valuemax="100"
517
529
  aria-valuemin="0"
518
530
  aria-valuenow=${Math.round(crossing.at)}
531
+ aria-valuetext=${formatPercent(Math.round(crossing.at) / 100)}
519
532
  data-cross=${index}
520
533
  style="--splitter-corner-at: ${crossing.at}%; --splitter-position: ${position}%"
521
534
  @dblclick=${this.handleCornerDoubleClick}
@@ -14,7 +14,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
14
14
  * table with no `TableColumn` heads its columns with.
15
15
  *
16
16
  * The role is on the host so the cell stays a child of its row in the flat
17
- * tree, which keeps the table structure intact.
17
+ * tree, which keeps the table structure intact. In a `grid` table a value is
18
+ * a `gridcell`, and the table gives the cell a `tabindex` so focus can rest on
19
+ * it.
18
20
  *
19
21
  * @since 0.1.0
20
22
  * @status stable
@@ -33,6 +35,11 @@ export declare class HelixHeadlessTableCell extends HelixElement {
33
35
  * column in a row of headers alone.
34
36
  */
35
37
  header: boolean;
38
+ /**
39
+ * @internal Whether the cell is a cell of a `grid` table, which moves focus
40
+ * between its cells, written by that table. Not a consumer's to set.
41
+ */
42
+ gridCell: boolean;
36
43
  /**
37
44
  * @internal Read by the row and by the cells beside it, to tell a header
38
45
  * cell from a value. Not a consumer's to read or set.
@@ -6,11 +6,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessTableCell = class HelixHeadlessTableCell extends HelixElement {
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.header = false;
14
+ this.gridCell = false;
14
15
  this.isTableCell = true;
15
16
  this.handleSlotChange = () => {
16
17
  const text = [...this.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim() !== '');
@@ -27,7 +28,7 @@ let HelixHeadlessTableCell = class HelixHeadlessTableCell extends HelixElement {
27
28
  willUpdate(changed) {
28
29
  super.willUpdate(changed);
29
30
  const header = this.headsRow ? 'rowheader' : 'columnheader';
30
- hostAria(this.internals, { role: this.header ? header : 'cell' });
31
+ hostAria(this.internals, { role: this.header ? header : this.gridCell ? 'gridcell' : 'cell' });
31
32
  }
32
33
  updated(changed) {
33
34
  super.updated(changed);
@@ -47,6 +48,9 @@ let HelixHeadlessTableCell = class HelixHeadlessTableCell extends HelixElement {
47
48
  __decorate([
48
49
  property({ reflect: true, type: Boolean })
49
50
  ], HelixHeadlessTableCell.prototype, "header", void 0);
51
+ __decorate([
52
+ state()
53
+ ], HelixHeadlessTableCell.prototype, "gridCell", void 0);
50
54
  HelixHeadlessTableCell = __decorate([
51
55
  customElement('helix-headless-table-cell')
52
56
  ], HelixHeadlessTableCell);
@@ -0,0 +1,117 @@
1
+ import { type GridPlace } from '@fusion.dev/helix-core';
2
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
3
+ import type { HelixHeadlessTableColumn } from './column/table-column.js';
4
+ /** A place in the grid with the element that holds focus there. */
5
+ export interface GridCell extends GridPlace {
6
+ cell: HTMLElement;
7
+ }
8
+ export interface TableGridOptions {
9
+ /**
10
+ * Acts on a cell for Enter, Space or F2, and says whether it did. A key it
11
+ * leaves alone moves into the cell's controls, Enter and F2 only.
12
+ */
13
+ activate: (place: GridCell, event: KeyboardEvent) => boolean;
14
+ /** The headings that are drawn, in order. */
15
+ columns: () => HelixHeadlessTableColumn[];
16
+ /** The element in the table's own shadow root holding a part of that name. */
17
+ part: (name: string) => HTMLElement | null;
18
+ /** The rows in the order they are drawn. */
19
+ rows: () => Element[];
20
+ /** Selects every row for Control or Command with A, and says whether it did. */
21
+ selectAll: () => boolean;
22
+ /** Whether a checkbox column leads. */
23
+ selectable: () => boolean;
24
+ /** Whether the table is a grid now. */
25
+ on: () => boolean;
26
+ }
27
+ /**
28
+ * The keyboard of a `grid` table: one tab stop among its cells, the arrow
29
+ * keys, Home, End and the Page keys between them, and Enter or F2 into a
30
+ * cell's controls.
31
+ *
32
+ * **Tab leaves the grid.** The controls in a cell are the consumer's, so their
33
+ * tab order is left as written: Tab on a cell, or past the last control of a
34
+ * cell, focuses an empty box at the grid's end (Shift: its start) and lets the
35
+ * key go on from there, as React Aria's collections do. A control in a cell
36
+ * that focus reaches from outside by the keyboard hands it to the stop, so the
37
+ * grid is one stop from either side. A press, or a script focusing a cell, is
38
+ * left where it lands, and the stop follows it.
39
+ */
40
+ export declare class TableGridController implements ReactiveController {
41
+ private readonly host;
42
+ private readonly options;
43
+ /** The row holding the stop, `head` for the headings, or nothing for the first row. */
44
+ private stopRow;
45
+ /** The column holding the stop. */
46
+ private stopColumn;
47
+ /** Where the stop's row was drawn when last marked, for the row that takes over when it goes. */
48
+ private stopIndex;
49
+ /** The row focus is in, while it is in the grid's rows. */
50
+ private focusRow;
51
+ /** Whether the next update focuses the stop, after a key moved it. */
52
+ private focusWanted;
53
+ /** Whether focus is arriving from a press, which keeps its place. */
54
+ private pointing;
55
+ /** The cells given a `tabindex`, so a table that stops being a grid takes it back. */
56
+ private readonly given;
57
+ constructor(host: ReactiveControllerHost & HTMLElement, options: TableGridOptions);
58
+ hostConnected(): void;
59
+ hostDisconnected(): void;
60
+ /**
61
+ * A row taken away while it held the stop hands it to the row drawn in its
62
+ * place, or to the row before where it was the last, at the same column.
63
+ * Where focus was in that row and went with it, focus follows the stop, as
64
+ * React Aria's collections move it to a neighbor; focus that had already
65
+ * left the grid is left where it is. Before the render, so the cells the
66
+ * table draws itself are drawn with the stop where it now is.
67
+ */
68
+ hostUpdate(): void;
69
+ /** How many columns lead the consumer's cells: the checkbox column, where there is one. */
70
+ private get offset();
71
+ /** The first row: the headings where there are any, and the first row of cells otherwise. */
72
+ private get top();
73
+ /** A row's cells that are drawn, in order. */
74
+ private cellsOf;
75
+ /** How many columns a row has. */
76
+ private widthOf;
77
+ /** The element that takes focus at a place, or nothing where there is none. */
78
+ cellAt({ column, row }: GridPlace): HTMLElement | null;
79
+ /** The place holding the stop now, held to the grid as it is. */
80
+ current(): GridPlace;
81
+ /** Whether the stop is at a place, for a cell the table draws itself. */
82
+ isStop(place: GridPlace): boolean;
83
+ /** The place an event came from or through, with the element that holds focus there. */
84
+ private placeOf;
85
+ /** Moves the stop to a place, and focus with it on the next update. */
86
+ private moveTo;
87
+ /**
88
+ * Writes the stops: a `tabindex` on every cell, `0` on the one holding the
89
+ * stop, and the grid's state on the rows and headings. Takes it all back
90
+ * once the table stops being a grid. Called by the table after each update.
91
+ */
92
+ mark(): void;
93
+ /** Focuses the stop where a key asked for it. Called by the table after `mark`. */
94
+ place(): void;
95
+ /** The controls in a cell, in order: what Enter and F2 move into, and Tab moves between. */
96
+ private controlsIn;
97
+ /**
98
+ * Hands focus to the empty box at the grid's end, or its start, so the Tab
99
+ * under way goes on from there and out of the grid.
100
+ */
101
+ private leave;
102
+ /** How many rows a Page key moves from a cell. */
103
+ private pageFrom;
104
+ private readonly handleKeydown;
105
+ /** The keys the grid takes while focus is on a control inside a cell. */
106
+ private handleControlKey;
107
+ /**
108
+ * Focus arriving on a cell, or inside one. From outside the grid by the
109
+ * keyboard, anywhere but the stop is handed to the stop; otherwise the stop
110
+ * moves to where focus went.
111
+ */
112
+ private readonly handleFocusIn;
113
+ /** Focus leaving for somewhere outside the grid's host is no longer in a row. */
114
+ private readonly handleFocusOut;
115
+ /** A press is under way, so the focus it brings stays where it lands. */
116
+ private readonly handlePointerDown;
117
+ }