@fusion.dev/helix-headless 0.171.0 → 0.173.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 (67) hide show
  1. package/custom-elements.json +1350 -124
  2. package/dist/button-group/root/button-group-root.d.ts +30 -0
  3. package/dist/button-group/root/button-group-root.js +50 -4
  4. package/dist/canvas/root/canvas-root.d.ts +7 -0
  5. package/dist/canvas/root/canvas-root.js +7 -0
  6. package/dist/chat/composer/chat-composer.d.ts +28 -1
  7. package/dist/chat/composer/chat-composer.js +22 -3
  8. package/dist/chat/root/chat-root.d.ts +38 -2
  9. package/dist/chat/root/chat-root.js +38 -1
  10. package/dist/color-input/root/color-input-root.d.ts +15 -1
  11. package/dist/color-input/root/color-input-root.js +19 -1
  12. package/dist/color-picker/root/color-picker-root.d.ts +5 -1
  13. package/dist/color-picker/root/color-picker-root.js +1 -1
  14. package/dist/copy-button/root/copy-button-root.d.ts +43 -3
  15. package/dist/copy-button/root/copy-button-root.js +49 -3
  16. package/dist/data-table/root/data-table-root.d.ts +5 -1
  17. package/dist/data-table/root/data-table-root.js +1 -0
  18. package/dist/dialog/root/dialog-root.js +1 -0
  19. package/dist/drawer/root/drawer-root.d.ts +4 -0
  20. package/dist/drawer/root/drawer-root.js +1 -0
  21. package/dist/element-events.generated.js +3 -0
  22. package/dist/graph/root/graph-root.d.ts +22 -0
  23. package/dist/graph/root/graph-root.js +21 -5
  24. package/dist/lightbox/root/lightbox-root.js +1 -0
  25. package/dist/list/root/list-root.d.ts +6 -1
  26. package/dist/list/root/list-root.js +3 -2
  27. package/dist/listbox/root/listbox-root.d.ts +2 -1
  28. package/dist/listbox/root/listbox-root.js +3 -2
  29. package/dist/menu/item/menu-item.d.ts +2 -0
  30. package/dist/menu/item/menu-item.js +2 -0
  31. package/dist/menu/root/menu-root.d.ts +73 -12
  32. package/dist/menu/root/menu-root.js +103 -7
  33. package/dist/outline/root/outline-root.d.ts +57 -0
  34. package/dist/outline/root/outline-root.js +98 -29
  35. package/dist/popover/root/popover-root.d.ts +59 -6
  36. package/dist/popover/root/popover-root.js +51 -3
  37. package/dist/select/root/select-root.d.ts +2 -2
  38. package/dist/select/root/select-root.js +2 -2
  39. package/dist/speed-dial/root/speed-dial-root.d.ts +14 -0
  40. package/dist/speed-dial/root/speed-dial-root.js +34 -2
  41. package/dist/splitter/root/splitter-root.d.ts +25 -2
  42. package/dist/splitter/root/splitter-root.js +19 -4
  43. package/dist/tag/group/tag-group.d.ts +8 -1
  44. package/dist/tag/group/tag-group.js +8 -0
  45. package/dist/tag/root/tag-root.d.ts +12 -0
  46. package/dist/tag/root/tag-root.js +12 -0
  47. package/dist/textarea/root/textarea-root.d.ts +8 -0
  48. package/dist/textarea/root/textarea-root.js +16 -0
  49. package/dist/time-picker/root/time-picker-root.d.ts +14 -0
  50. package/dist/time-picker/root/time-picker-root.js +8 -1
  51. package/dist/timeline/item/timeline-item.d.ts +5 -1
  52. package/dist/timeline/item/timeline-item.js +9 -0
  53. package/dist/timeline/root/timeline-root.d.ts +23 -0
  54. package/dist/timeline/root/timeline-root.js +17 -3
  55. package/dist/tooltip/root/tooltip-root.d.ts +10 -0
  56. package/dist/tooltip/root/tooltip-root.js +14 -2
  57. package/dist/virtual-list/reorder/index.d.ts +1 -0
  58. package/dist/virtual-list/reorder/index.js +4 -0
  59. package/dist/virtual-list/reorder/virtual-list-reorder.d.ts +86 -0
  60. package/dist/virtual-list/reorder/virtual-list-reorder.js +193 -0
  61. package/dist/virtual-list/root/virtual-list-root.d.ts +49 -11
  62. package/dist/virtual-list/root/virtual-list-root.js +10 -1
  63. package/lint.json +1 -1
  64. package/package.json +5 -5
  65. package/types/jsx.d.ts +880 -48
  66. package/types/vue.d.ts +174 -19
  67. package/types/words.d.ts +22 -0
@@ -0,0 +1,193 @@
1
+ import { ReorderController } from '@fusion.dev/helix-core';
2
+ const WORDS = {
3
+ canceledLabel: ['canceled-label', 'Canceled'],
4
+ droppedLabel: ['dropped-label', 'Dropped'],
5
+ movedLabel: ['moved-label', '{name} moved to position {position} of {count}'],
6
+ pickedUpLabel: ['picked-up-label', 'Picked up {name}'],
7
+ };
8
+ const MOVES = ['dragging', 'drop-after', 'drop-before'];
9
+ export class VirtualListReorderController {
10
+ constructor(host, sheet) {
11
+ this.marked = new Set();
12
+ this.names = new Map();
13
+ this.handleReorder = (event) => {
14
+ const { to } = event.detail;
15
+ const focused = this.host.contains(document.activeElement);
16
+ if (!focused) {
17
+ return;
18
+ }
19
+ this.waiting = to;
20
+ setTimeout(() => {
21
+ const at = this.waiting;
22
+ this.waiting = undefined;
23
+ if (at !== undefined && !event.defaultPrevented && this.host.isConnected) {
24
+ void this.host.focusIndex(at);
25
+ }
26
+ }, 0);
27
+ };
28
+ this.host = host;
29
+ this.sheet = sheet;
30
+ host.addController(this);
31
+ this.reorder = new ReorderController(host, {
32
+ get axis() {
33
+ return host.orientation === 'horizontal' ? 'inline' : 'block';
34
+ },
35
+ box: (key) => this.rowFor(key)?.getBoundingClientRect(),
36
+ current: () => {
37
+ const row = this.rows.find((one) => one.hasAttribute('data-active'));
38
+ return row && !row.hasAttribute('data-disabled') ? this.keyOf(row) : undefined;
39
+ },
40
+ enabled: () => this.enabled,
41
+ grip: (event) => {
42
+ const row = this.rowOf(event);
43
+ const key = row && !row.hasAttribute('data-disabled') ? this.keyOf(row) : undefined;
44
+ return row && key !== undefined ? { element: row, key } : undefined;
45
+ },
46
+ holds: false,
47
+ keys: () => host.keys(),
48
+ name: (key) => this.names.get(key) || key,
49
+ preview: (key) => this.rowFor(key),
50
+ say: (words) => {
51
+ if (this.live) {
52
+ this.live.textContent = words;
53
+ }
54
+ },
55
+ scroller: () => host.renderRoot?.querySelector('[part="viewport"]') ?? null,
56
+ });
57
+ }
58
+ get enabled() {
59
+ const host = this.host;
60
+ return (host.reorderable && host.columns <= 1 && host.minColumnWidth <= 0 && host.sectionField === '');
61
+ }
62
+ hostUpdated() {
63
+ queueMicrotask(() => this.mark());
64
+ }
65
+ hostConnected() {
66
+ this.host.addEventListener('helix-reorder', this.handleReorder);
67
+ }
68
+ hostDisconnected() {
69
+ this.host.removeEventListener('helix-reorder', this.handleReorder);
70
+ this.waiting = undefined;
71
+ }
72
+ get rows() {
73
+ return [...this.host.querySelectorAll(':scope > [data-index]')];
74
+ }
75
+ keyOf(row) {
76
+ const at = Number(row.dataset.index);
77
+ return Number.isInteger(at) ? this.host.keys()[at] : undefined;
78
+ }
79
+ rowFor(key) {
80
+ const at = this.host.keys().indexOf(key);
81
+ return at < 0
82
+ ? undefined
83
+ : (this.host.querySelector(`:scope > [data-index="${at}"]`) ?? undefined);
84
+ }
85
+ rowOf(event) {
86
+ const rows = this.rows;
87
+ return event
88
+ .composedPath()
89
+ .find((node) => rows.includes(node));
90
+ }
91
+ draw(on) {
92
+ const root = this.host.renderRoot;
93
+ if (!root) {
94
+ return;
95
+ }
96
+ const drawn = root.querySelector('[part="drag-preview"]');
97
+ if (on && !drawn) {
98
+ const preview = document.createElement('div');
99
+ for (const [name, value] of [
100
+ ['part', 'drag-preview'],
101
+ ['popover', 'manual'],
102
+ ['aria-hidden', 'true'],
103
+ ['inert', ''],
104
+ ]) {
105
+ preview.setAttribute(name, value);
106
+ }
107
+ const live = document.createElement('span');
108
+ live.setAttribute('role', 'status');
109
+ live.setAttribute('data-reorder-live', '');
110
+ root.append(preview, live);
111
+ this.live = live;
112
+ }
113
+ else if (!on && drawn) {
114
+ drawn.remove();
115
+ this.live?.remove();
116
+ this.live = undefined;
117
+ }
118
+ }
119
+ mark() {
120
+ const on = this.enabled;
121
+ adopt(this.host, this.sheet, on);
122
+ this.draw(on);
123
+ const marked = new Set();
124
+ this.names = new Map();
125
+ for (const row of this.rows) {
126
+ const key = on ? this.keyOf(row) : undefined;
127
+ if (key !== undefined) {
128
+ this.names.set(key, row.textContent?.trim() ?? '');
129
+ }
130
+ const move = key === undefined ? undefined : this.reorder.moveOf(key);
131
+ if (move || this.marked.has(row)) {
132
+ for (const one of MOVES) {
133
+ row.toggleAttribute(`data-${one}`, one === move);
134
+ }
135
+ }
136
+ if (move) {
137
+ marked.add(row);
138
+ }
139
+ }
140
+ for (const row of this.marked) {
141
+ if (!marked.has(row)) {
142
+ for (const one of MOVES) {
143
+ row.removeAttribute(`data-${one}`);
144
+ }
145
+ }
146
+ }
147
+ this.marked = marked;
148
+ }
149
+ }
150
+ const adopt = (element, given, on) => {
151
+ const root = element.shadowRoot;
152
+ const sheet = given?.styleSheet;
153
+ if (!root || !sheet || root.adoptedStyleSheets.includes(sheet) === on) {
154
+ return;
155
+ }
156
+ root.adoptedStyleSheets = on
157
+ ? [...root.adoptedStyleSheets, sheet]
158
+ : root.adoptedStyleSheets.filter((one) => one !== sheet);
159
+ };
160
+ const given = new WeakMap();
161
+ const attach = (list, sheet) => {
162
+ const controller = given.get(list) ?? new VirtualListReorderController(list);
163
+ given.set(list, controller);
164
+ controller.sheet ??= sheet;
165
+ };
166
+ const onPage = (of, root = document) => [...root.querySelectorAll('*')].flatMap((node) => [
167
+ ...(node instanceof of ? [node] : []),
168
+ ...(node.shadowRoot ? onPage(of, node.shadowRoot) : []),
169
+ ]);
170
+ export const reorderVirtualList = (element, sheet) => {
171
+ element.addInitializer((host) => attach(host, sheet));
172
+ if (typeof document !== 'undefined') {
173
+ for (const list of onPage(element)) {
174
+ attach(list, sheet);
175
+ list.requestUpdate();
176
+ }
177
+ }
178
+ const proto = element.prototype;
179
+ for (const [name, [attribute, fallback]] of Object.entries(WORDS)) {
180
+ if (name in proto) {
181
+ continue;
182
+ }
183
+ Object.defineProperty(proto, name, {
184
+ configurable: true,
185
+ get() {
186
+ return this.getAttribute(attribute) ?? fallback;
187
+ },
188
+ set(next) {
189
+ this.setAttribute(attribute, next);
190
+ },
191
+ });
192
+ }
193
+ };
@@ -85,11 +85,14 @@ export interface VirtualListRange {
85
85
  * | Page Down, Page Up | Moves by the number of items, or of lines in a grid, on screen. |
86
86
  * | A letter or a word | Moves to the next item whose text starts with what you typed. |
87
87
  * | Escape | Lets go of every chosen item, in a list with `selection-mode`. |
88
+ * | Alt with Up or Down | In a `reorderable` list, moves the item a place. Alt with Home or End moves it to either end. |
88
89
  * | Tab | Moves into the focused row's own links and buttons, then out of the list. |
89
90
  *
90
91
  * An item that is not drawn is scrolled into view and focused, so a keyboard
91
- * reaches the whole set. When the row holding the tab stop scrolls away, the
92
- * stop moves to the first row on screen. Typing matches each item's
92
+ * reaches the whole set. The row a reader last focused keeps the tab stop and
93
+ * stays drawn when it scrolls away, so Tab and Shift+Tab come back to it, as
94
+ * React Aria's collections do. In a list nobody has focused yet, the stop
95
+ * moves to the first row on screen. Typing matches each item's
93
96
  * `text-field`, which is the first `data-field` of the row template by default,
94
97
  * or the item itself when it is a string or a number.
95
98
  *
@@ -131,6 +134,23 @@ export interface VirtualListRange {
131
134
  * the last item says "Loading more" (`loading-label`, or your own content in
132
135
  * the `loading` slot). `scrollToIndex` takes a reader to an item.
133
136
  *
137
+ * **Moving items** is a separate import, `@fusion.dev/helix/virtual-list/reorder`,
138
+ * as React Aria's drag and drop is. With it imported, `reorderable` lets a
139
+ * reader move items in a list of one column without sections:
140
+ *
141
+ * - Alt with Up or Down moves the item the keys are on a place, and Alt with
142
+ * Home or End to either end, as the APG's rearrangeable listbox does.
143
+ * - A pointer drags a whole row. A press that does not travel is still a
144
+ * press. The list scrolls near an edge, and an item far down a long set is
145
+ * reached by dragging to the edge.
146
+ * - A drop sends `helix-before-reorder`, which you can refuse, and then
147
+ * `helix-reorder`, whose `keys` hold every item's key in the new order.
148
+ * **You move the items**: set `items` to them in that order. The keys stay
149
+ * on the item that moved.
150
+ * - Each move is said in a region screen readers announce. The row being
151
+ * dragged carries `data-dragging`, and the row a drop would land beside
152
+ * `data-drop-before` or `data-drop-after`.
153
+ *
134
154
  * **An empty list.** Set `empty-label`, or put your own content in the `empty`
135
155
  * slot, for a row shown when there are no items and none are loading, such as
136
156
  * "No messages yet" and a link to write one. Without either, an empty list
@@ -168,8 +188,10 @@ export interface VirtualListRange {
168
188
  *
169
189
  * @event {Event} change - Fired once a press or a key has changed which items are selected, in a list with `selection-mode`.
170
190
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a press or a key changes which items are selected, with the `value` it would leave. Cancelable.
191
+ * @event {HelixBeforeReorderEvent} helix-before-reorder - Fired when an item is dropped at a new place, in a `reorderable` list with `@fusion.dev/helix/virtual-list/reorder` imported, with every key in the new order. Cancelable: cancel it to keep the item where it is.
171
192
  * @event {HelixLoadMoreEvent} helix-load-more - Fired when a reader comes within one screen of the last item, once for each length of the set.
172
193
  * @event {HelixRangeChangeEvent} helix-range-change - Fired when the items on screen change, with the new `first` and `last`, which `range` also holds.
194
+ * @event {HelixReorderEvent} helix-reorder - Fired after `helix-before-reorder` when nobody canceled it. Set `items` to the order in `keys`. Cancelable: cancel it to say the move did not happen.
173
195
  *
174
196
  * @csspart after - The space that stands in for the rows below the rendered ones.
175
197
  * @csspart before - The space that stands in for the rows above the rendered ones.
@@ -295,6 +317,15 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
295
317
  * it is unset, the header is the name as text.
296
318
  */
297
319
  renderSection: VirtualListSectionRenderer | undefined;
320
+ /**
321
+ * Lets a reader move items, once `@fusion.dev/helix/virtual-list/reorder`
322
+ * (or `@fusion.dev/helix-headless/virtual-list/reorder`) is imported: Alt
323
+ * with an arrow moves the row the keys are on, and a pointer drags a row.
324
+ * The list moves no item itself: you move your `items` on `helix-reorder`.
325
+ * Without that import it does nothing, so a list that never moves items
326
+ * carries none of the code. A list of one column, without sections.
327
+ */
328
+ reorderable: boolean;
298
329
  /**
299
330
  * The field of each item that names its section, as a key or a path of
300
331
  * keys. Set, the items are drawn in sections: a header, then the items, in
@@ -511,8 +542,8 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
511
542
  private readonly handleKeydown;
512
543
  /** The key of item `at` in `value`. See `keyField`. */
513
544
  private keyAt;
514
- /** Every item's key, in the order of the set. */
515
- private keys;
545
+ /** @internal Every item's key, in the order of the set, which the reorder entry moves. */
546
+ keys(): string[];
516
547
  /** The keys chosen now. */
517
548
  private get chosen();
518
549
  /**
@@ -532,10 +563,10 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
532
563
  */
533
564
  private readonly handleClick;
534
565
  /**
535
- * Focuses the row for item `index`, scrolling it into view and drawing it
536
- * first when it is not drawn.
566
+ * @internal Focuses the row for item `index`, scrolling it into view and
567
+ * drawing it first when it is not drawn.
537
568
  */
538
- private focusIndex;
569
+ focusIndex(index: number): Promise<void>;
539
570
  /**
540
571
  * The row kept for focus is let go once focus has left it.
541
572
  *
@@ -558,7 +589,13 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
558
589
  private focusedIndex;
559
590
  /** The item a key is moving focus to, drawn until it has it. */
560
591
  private focusing;
561
- /** The item drawn outside the window to keep focus, or to take it, if there is one. */
592
+ /** Whether a reader has focused a row, after which their row keeps the tab stop. */
593
+ private visited;
594
+ /**
595
+ * The item drawn outside the window to keep focus, to take it, or to take
596
+ * it back: the row a reader last focused stays drawn after focus leaves the
597
+ * list, so Shift+Tab returns to it.
598
+ */
562
599
  private keptIndex;
563
600
  /**
564
601
  * Writes a row's role, and whether it is chosen in a list that selects:
@@ -596,9 +633,10 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
596
633
  * **The row holding focus is drawn outside the window too**, beside it on the
597
634
  * side it belongs, and the spacer on that side gives up its room.
598
635
  *
599
- * **One row holds the tab stop**, the one the reader last focused while it is
600
- * drawn, and the first row on screen once it is not. Its own links and
601
- * buttons are in the tab order, and those of every other row wait.
636
+ * **One row holds the tab stop**, the one the reader last focused, which
637
+ * stays drawn outside the window, and the first row on screen in a list
638
+ * nobody has focused yet. Its own links and buttons are in the tab order,
639
+ * and those of every other row wait.
602
640
  */
603
641
  private place;
604
642
  /** The sections the rows were last placed in. */
@@ -34,6 +34,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
34
34
  this.measured = false;
35
35
  this.minColumnWidth = 0;
36
36
  this.orientation = 'vertical';
37
+ this.reorderable = false;
37
38
  this.sectionField = '';
38
39
  this.selectionBehavior = 'toggle';
39
40
  this.textField = '';
@@ -171,9 +172,11 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
171
172
  const index = this.indexOf(node);
172
173
  if (index !== undefined) {
173
174
  this.active = index;
175
+ this.visited = true;
174
176
  }
175
177
  this.requestUpdate();
176
178
  };
179
+ this.visited = false;
177
180
  this.sectionId = nextSectionId();
178
181
  }
179
182
  static { this.words = 'helix-virtual-list'; }
@@ -524,7 +527,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
524
527
  return index === undefined ? undefined : Number(index);
525
528
  }
526
529
  keptIndex() {
527
- const index = this.focusing ?? this.focusedIndex();
530
+ const index = this.focusing ?? this.focusedIndex() ?? (this.visited ? this.active : undefined);
528
531
  const { first, last } = this.itemsOn(this.virtualize.window);
529
532
  return index !== undefined && index < this.data.length && (index < first || index >= last)
530
533
  ? index
@@ -585,6 +588,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
585
588
  const count = this.data.length;
586
589
  const { first, last } = this.itemsOn(this.virtualize.window);
587
590
  const holding = this.focusedIndex();
591
+ if (this.filledFrom !== this.data && holding === undefined) {
592
+ this.visited = false;
593
+ }
588
594
  const onScreen = this.range.first;
589
595
  if (this.hasUpdated) {
590
596
  const spacers = this.spacerSizes();
@@ -868,6 +874,9 @@ __decorate([
868
874
  __decorate([
869
875
  property({ attribute: false })
870
876
  ], HelixHeadlessVirtualList.prototype, "renderSection", void 0);
877
+ __decorate([
878
+ property({ type: Boolean })
879
+ ], HelixHeadlessVirtualList.prototype, "reorderable", void 0);
871
880
  __decorate([
872
881
  property({ attribute: 'section-field' })
873
882
  ], HelixHeadlessVirtualList.prototype, "sectionField", void 0);