@fusion.dev/helix-headless 0.172.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.
- package/custom-elements.json +912 -60
- package/dist/button-group/root/button-group-root.d.ts +30 -0
- package/dist/button-group/root/button-group-root.js +50 -4
- package/dist/canvas/root/canvas-root.d.ts +7 -0
- package/dist/canvas/root/canvas-root.js +7 -0
- package/dist/copy-button/root/copy-button-root.d.ts +43 -3
- package/dist/copy-button/root/copy-button-root.js +49 -3
- package/dist/element-events.generated.js +2 -0
- package/dist/graph/root/graph-root.d.ts +22 -0
- package/dist/graph/root/graph-root.js +21 -5
- package/dist/list/root/list-root.d.ts +6 -1
- package/dist/list/root/list-root.js +3 -2
- package/dist/outline/root/outline-root.d.ts +57 -0
- package/dist/outline/root/outline-root.js +98 -29
- package/dist/speed-dial/root/speed-dial-root.d.ts +14 -0
- package/dist/speed-dial/root/speed-dial-root.js +34 -2
- package/dist/splitter/root/splitter-root.d.ts +25 -2
- package/dist/splitter/root/splitter-root.js +19 -4
- package/dist/tag/group/tag-group.d.ts +8 -1
- package/dist/tag/group/tag-group.js +8 -0
- package/dist/tag/root/tag-root.d.ts +12 -0
- package/dist/tag/root/tag-root.js +12 -0
- package/dist/time-picker/root/time-picker-root.d.ts +14 -0
- package/dist/time-picker/root/time-picker-root.js +8 -1
- package/dist/timeline/item/timeline-item.d.ts +5 -1
- package/dist/timeline/item/timeline-item.js +9 -0
- package/dist/timeline/root/timeline-root.d.ts +23 -0
- package/dist/timeline/root/timeline-root.js +17 -3
- package/dist/tooltip/root/tooltip-root.d.ts +10 -0
- package/dist/tooltip/root/tooltip-root.js +14 -2
- package/dist/virtual-list/reorder/index.d.ts +1 -0
- package/dist/virtual-list/reorder/index.js +4 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.d.ts +86 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.js +193 -0
- package/dist/virtual-list/root/virtual-list-root.d.ts +49 -11
- package/dist/virtual-list/root/virtual-list-root.js +10 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +568 -18
- package/types/vue.d.ts +115 -11
- package/types/words.d.ts +14 -0
|
@@ -14,7 +14,9 @@ type TimelineTone = 'danger' | 'info' | 'neutral' | 'primary' | 'success' | 'war
|
|
|
14
14
|
* hours ago", from `datetime`, where the `time` slot is empty. The words move
|
|
15
15
|
* on as they go stale, "3 hours ago" becoming "4 hours ago", with one wakeup
|
|
16
16
|
* shared by every reading on the page, as `FormatDate` keeps its own. They are
|
|
17
|
-
* in the language of the page, or of the nearest `lang`.
|
|
17
|
+
* in the language of the page, or of the nearest `lang`. The whole date and
|
|
18
|
+
* time is the reading's `title`, so a pointer resting on "3 hours ago" shows
|
|
19
|
+
* when it was, and a screen reader reads it as the description.
|
|
18
20
|
*
|
|
19
21
|
* Each entry draws the connector line that arrives at it and hides it when it
|
|
20
22
|
* is the first entry, using `:host(:first-child)`. This needs no index, no
|
|
@@ -77,6 +79,8 @@ export declare class HelixHeadlessTimelineItem extends HelixElement {
|
|
|
77
79
|
* otherwise.
|
|
78
80
|
*/
|
|
79
81
|
private reading;
|
|
82
|
+
/** The whole date and time `datetime` names, the reading's title under `relative`. */
|
|
83
|
+
private whole;
|
|
80
84
|
/** The instant `datetime` names, where `relative` asks for a reading of it. */
|
|
81
85
|
private get moment();
|
|
82
86
|
/** What the reading says at `now`, and when it stops being true. */
|
|
@@ -21,6 +21,7 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
|
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
23
|
this.reading = '';
|
|
24
|
+
this.whole = '';
|
|
24
25
|
this.pendingLabel = 'Scheduled';
|
|
25
26
|
this.toneLabel = '';
|
|
26
27
|
this.toneLabels = {
|
|
@@ -52,6 +53,13 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
|
|
|
52
53
|
}
|
|
53
54
|
const moment = this.moment;
|
|
54
55
|
this.reading = moment ? this.readingAt(moment, new Date(this.clock.now)).text : '';
|
|
56
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(this.datetime);
|
|
57
|
+
this.whole = moment
|
|
58
|
+
? new Intl.DateTimeFormat(wordsLocale(this), {
|
|
59
|
+
dateStyle: 'long',
|
|
60
|
+
...(dateOnly ? { timeZone: 'UTC' } : { timeStyle: 'short' }),
|
|
61
|
+
}).format(moment)
|
|
62
|
+
: '';
|
|
55
63
|
hostAria(this.internals, { role: 'listitem' });
|
|
56
64
|
}
|
|
57
65
|
renderMarker() {
|
|
@@ -78,6 +86,7 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
|
|
|
78
86
|
: html `<time
|
|
79
87
|
part="time"
|
|
80
88
|
datetime=${this.datetime}
|
|
89
|
+
title=${this.whole || nothing}
|
|
81
90
|
>${when}</time
|
|
82
91
|
>`}
|
|
83
92
|
<div part="content"><slot></slot></div>`;
|
|
@@ -26,6 +26,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
26
26
|
* Write the entries in the order the events happened, oldest or newest first,
|
|
27
27
|
* and the layout only moves where they are drawn.
|
|
28
28
|
*
|
|
29
|
+
* **A feed that loads more entries sets `loading` while it fetches.** The
|
|
30
|
+
* timeline is busy, and an entry after the last says "Loading more"
|
|
31
|
+
* (`loading-label`, or your own content in the `loading` slot). Put a button
|
|
32
|
+
* that asks for more after the timeline, and move focus to the first new
|
|
33
|
+
* entry when they arrive.
|
|
34
|
+
*
|
|
29
35
|
* @since 0.1.0
|
|
30
36
|
* @status stable
|
|
31
37
|
* @category data
|
|
@@ -33,12 +39,29 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
33
39
|
* @documentation https://fusion.dev/helix/components/timeline
|
|
34
40
|
*
|
|
35
41
|
* @slot - The entries, and any groups of them.
|
|
42
|
+
* @slot loading - Content for the entry after the last while `loading` is set. Falls back to `loading-label`.
|
|
43
|
+
*
|
|
44
|
+
* @csspart loading - The entry after the last while `loading` is set.
|
|
45
|
+
*
|
|
46
|
+
* @cssstate loading - While `loading` is set.
|
|
36
47
|
*/
|
|
37
48
|
export declare class HelixHeadlessTimeline extends HelixElement {
|
|
49
|
+
/**
|
|
50
|
+
* The key this family's words are registered under, its styled tag:
|
|
51
|
+
* `registerWords('de', { 'helix-timeline': { loadingLabel: 'Wird geladen' } })`.
|
|
52
|
+
*/
|
|
53
|
+
static words: string;
|
|
38
54
|
/**
|
|
39
55
|
* Accessible name for the timeline, for people who reach it out of context.
|
|
40
56
|
*/
|
|
41
57
|
label: string;
|
|
58
|
+
/**
|
|
59
|
+
* Marks the timeline as busy fetching more entries, and draws an entry
|
|
60
|
+
* after the last that says so.
|
|
61
|
+
*/
|
|
62
|
+
loading: boolean;
|
|
63
|
+
/** The text of the entry drawn while `loading` is set, where the `loading` slot is empty. */
|
|
64
|
+
loadingLabel: string;
|
|
42
65
|
protected render(): TemplateResult;
|
|
43
66
|
protected willUpdate(changed: PropertyValues): void;
|
|
44
67
|
}
|
|
@@ -4,25 +4,39 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
|
|
8
|
-
import { html } from 'lit';
|
|
7
|
+
import { customElement, HelixElement, hostAria, words } from '@fusion.dev/helix-core';
|
|
8
|
+
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
let HelixHeadlessTimeline = class HelixHeadlessTimeline extends HelixElement {
|
|
11
11
|
constructor() {
|
|
12
12
|
super(...arguments);
|
|
13
13
|
this.label = '';
|
|
14
|
+
this.loading = false;
|
|
15
|
+
this.loadingLabel = 'Loading more';
|
|
14
16
|
}
|
|
17
|
+
static { this.words = 'helix-timeline'; }
|
|
15
18
|
render() {
|
|
16
|
-
return html `<slot></slot
|
|
19
|
+
return html `<slot></slot>${this.loading
|
|
20
|
+
? html `<div part="loading" role="listitem"><slot name="loading">${this.loadingLabel}</slot></div>`
|
|
21
|
+
: nothing}`;
|
|
17
22
|
}
|
|
18
23
|
willUpdate(changed) {
|
|
19
24
|
super.willUpdate(changed);
|
|
20
25
|
hostAria(this.internals, { label: this.label, role: 'list' });
|
|
26
|
+
this.internals.ariaBusy = this.loading ? 'true' : null;
|
|
27
|
+
this.toggleState('loading', this.loading);
|
|
21
28
|
}
|
|
22
29
|
};
|
|
23
30
|
__decorate([
|
|
24
31
|
property()
|
|
25
32
|
], HelixHeadlessTimeline.prototype, "label", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
property({ reflect: true, type: Boolean })
|
|
35
|
+
], HelixHeadlessTimeline.prototype, "loading", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
words,
|
|
38
|
+
property({ attribute: 'loading-label' })
|
|
39
|
+
], HelixHeadlessTimeline.prototype, "loadingLabel", void 0);
|
|
26
40
|
HelixHeadlessTimeline = __decorate([
|
|
27
41
|
customElement('helix-headless-timeline')
|
|
28
42
|
], HelixHeadlessTimeline);
|
|
@@ -32,6 +32,9 @@ declare const HelixHeadlessTooltip_base: (new (...args: any[]) => import("@fusio
|
|
|
32
32
|
* describes a Helix component and leaves a plain element undescribed. In the
|
|
33
33
|
* `for` form the tooltip itself is out of the accessibility tree while its
|
|
34
34
|
* bubble is down, so you meet its words once, as the trigger's description.
|
|
35
|
+
* A trigger whose name is already the tooltip's words, such as an icon button
|
|
36
|
+
* named Settings under a tooltip reading Settings, is not described by them
|
|
37
|
+
* again.
|
|
35
38
|
*
|
|
36
39
|
* The tooltip shows on hover and on focus. It hides on pointer leave, on blur,
|
|
37
40
|
* on Escape, and when the trigger is activated. The pointer can move from the
|
|
@@ -394,6 +397,13 @@ export declare class HelixHeadlessTooltip extends HelixHeadlessTooltip_base {
|
|
|
394
397
|
* plain element in the consumer's markup. See `linkDescription`.
|
|
395
398
|
*/
|
|
396
399
|
private link;
|
|
400
|
+
/**
|
|
401
|
+
* Whether the bubble's words add anything to the trigger: they are not
|
|
402
|
+
* empty, and they are not the trigger's own name, such as an icon button
|
|
403
|
+
* named Settings under a tooltip reading Settings, which a screen reader
|
|
404
|
+
* read as "Settings, Settings".
|
|
405
|
+
*/
|
|
406
|
+
private get describes();
|
|
397
407
|
/** A reader arriving on the trigger, which is late enough to write the link. */
|
|
398
408
|
private readonly linkOnArrival;
|
|
399
409
|
private unbind;
|
|
@@ -213,17 +213,22 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
link(light, target = focusTarget(this.trigger)) {
|
|
216
|
-
|
|
216
|
+
const describes = this.describes;
|
|
217
|
+
if (this.linked === target && describes) {
|
|
217
218
|
return;
|
|
218
219
|
}
|
|
219
220
|
if (this.linked) {
|
|
220
221
|
unlinkDescription(this.linked, this);
|
|
221
222
|
this.linked = undefined;
|
|
222
223
|
}
|
|
223
|
-
if (linkDescription(target, this, light)) {
|
|
224
|
+
if (describes && linkDescription(target, this, light)) {
|
|
224
225
|
this.linked = target;
|
|
225
226
|
}
|
|
226
227
|
}
|
|
228
|
+
get describes() {
|
|
229
|
+
const text = this.describedText;
|
|
230
|
+
return text !== '' && !this.triggerSays(text);
|
|
231
|
+
}
|
|
227
232
|
unbind() {
|
|
228
233
|
if (this.linked) {
|
|
229
234
|
unlinkDescription(this.linked, this);
|
|
@@ -314,6 +319,13 @@ let HelixHeadlessTooltip = class HelixHeadlessTooltip extends MixinControllerOpe
|
|
|
314
319
|
if (changed.has('for')) {
|
|
315
320
|
this.bind();
|
|
316
321
|
}
|
|
322
|
+
if (this.linked && !this.describes) {
|
|
323
|
+
unlinkDescription(this.linked, this);
|
|
324
|
+
this.linked = undefined;
|
|
325
|
+
}
|
|
326
|
+
else if (!this.linked && this.bound) {
|
|
327
|
+
this.link(false);
|
|
328
|
+
}
|
|
317
329
|
if (changed.has('open')) {
|
|
318
330
|
const warm = warmth();
|
|
319
331
|
if (this.open) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { reorderVirtualList, VirtualListReorderController } from './virtual-list-reorder.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { HelixHeadlessVirtualList } from '../root/virtual-list-root.js';
|
|
2
|
+
import { reorderVirtualList } from './virtual-list-reorder.js';
|
|
3
|
+
reorderVirtualList(HelixHeadlessVirtualList);
|
|
4
|
+
export { reorderVirtualList, VirtualListReorderController } from './virtual-list-reorder.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { CSSResult, ReactiveController } from 'lit';
|
|
2
|
+
import type { HelixHeadlessVirtualList } from '../root/virtual-list-root.js';
|
|
3
|
+
/**
|
|
4
|
+
* Moves the items of a `reorderable` virtual list, by pointer and by keyboard,
|
|
5
|
+
* and says each move in a region screen readers announce.
|
|
6
|
+
*
|
|
7
|
+
* **Its own entry**, `@fusion.dev/helix-headless/virtual-list/reorder`, so a
|
|
8
|
+
* list that never moves items carries none of this. Importing the entry gives
|
|
9
|
+
* every virtual list one, and `reorderable` turns it on.
|
|
10
|
+
*
|
|
11
|
+
* - **Alt with Up or Down** moves the item the keys are on a place at once,
|
|
12
|
+
* and Alt with Home or End to either end, as the APG's rearrangeable
|
|
13
|
+
* listbox does. Across, Alt with Left or Right does, mirrored right to left.
|
|
14
|
+
* The keys stay on the item that moved.
|
|
15
|
+
* - **A pointer** drags a whole row, with no grip: a press that travels is a
|
|
16
|
+
* drag, and one that does not is still a press. The list scrolls near an
|
|
17
|
+
* edge.
|
|
18
|
+
* - **The page moves the items.** A drop sends `helix-before-reorder` and then
|
|
19
|
+
* `helix-reorder` with every key in the new order.
|
|
20
|
+
*
|
|
21
|
+
* A row being dragged carries `data-dragging`, and the row a drop would land
|
|
22
|
+
* beside `data-drop-before` or `data-drop-after`. While a pointer drags a row,
|
|
23
|
+
* a copy of it follows the pointer in the list's `drag-preview` part. The
|
|
24
|
+
* styled tier's entry hands in the sheet that draws those, which the list
|
|
25
|
+
* adopts while it reorders.
|
|
26
|
+
*/
|
|
27
|
+
export declare class VirtualListReorderController implements ReactiveController {
|
|
28
|
+
private readonly host;
|
|
29
|
+
/** The sheet the list adopts while it reorders, from the styled tier. */
|
|
30
|
+
sheet: CSSResult | undefined;
|
|
31
|
+
/** The rows marked last, so a mark that moves is taken off where it was. */
|
|
32
|
+
private marked;
|
|
33
|
+
/** The region each move is said in, drawn while the list reorders. */
|
|
34
|
+
private live;
|
|
35
|
+
private readonly reorder;
|
|
36
|
+
constructor(host: HelixHeadlessVirtualList, sheet?: CSSResult);
|
|
37
|
+
/** Whether the list moves its items now: one column, without sections. */
|
|
38
|
+
private get enabled();
|
|
39
|
+
/**
|
|
40
|
+
* The words of each drawn row by its item's key, as they were when the list
|
|
41
|
+
* last drew. A move is said after the page has moved the items and before
|
|
42
|
+
* the rows are drawn again, so a row's own words then belong to another item.
|
|
43
|
+
*/
|
|
44
|
+
private names;
|
|
45
|
+
hostUpdated(): void;
|
|
46
|
+
hostConnected(): void;
|
|
47
|
+
hostDisconnected(): void;
|
|
48
|
+
/** The rows the list draws now. */
|
|
49
|
+
private get rows();
|
|
50
|
+
/** The key of the item a row draws. */
|
|
51
|
+
private keyOf;
|
|
52
|
+
/** The row that draws the item with that key, where one is drawn. */
|
|
53
|
+
private rowFor;
|
|
54
|
+
/** The row an event passed through. */
|
|
55
|
+
private rowOf;
|
|
56
|
+
/** The place an item moved by a key goes to, until the page has moved it. */
|
|
57
|
+
private waiting;
|
|
58
|
+
/**
|
|
59
|
+
* Keeps the keys on the item that moved. The page moves the items as it
|
|
60
|
+
* hears this, and the rows are drawn again in place, so the row that had
|
|
61
|
+
* focus now draws another item. Focus goes to the item's new row once the
|
|
62
|
+
* list has drawn it.
|
|
63
|
+
*/
|
|
64
|
+
private readonly handleReorder;
|
|
65
|
+
/**
|
|
66
|
+
* Draws the list's `drag-preview` part and its region for the words while it
|
|
67
|
+
* reorders, and takes them away after. The list's own template draws
|
|
68
|
+
* neither.
|
|
69
|
+
*/
|
|
70
|
+
private draw;
|
|
71
|
+
/** Puts each row's mark in a move on it, and the styled tier's sheet on the list. */
|
|
72
|
+
private mark;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Gives every virtual list of a class a `VirtualListReorderController`, and
|
|
76
|
+
* the class the words it says, as properties that read and write their
|
|
77
|
+
* attributes: `movedLabel`, `canceledLabel`, `droppedLabel`, and
|
|
78
|
+
* `pickedUpLabel`.
|
|
79
|
+
*
|
|
80
|
+
* The entries call it for their own tier's class: the headless entry for
|
|
81
|
+
* `HelixHeadlessVirtualList`, and the styled one for `HelixVirtualList`, with
|
|
82
|
+
* the sheet that draws a move. **Lists already on the page are given one
|
|
83
|
+
* too**, so a page that imports the list and then this entry has its lists
|
|
84
|
+
* reorder.
|
|
85
|
+
*/
|
|
86
|
+
export declare const reorderVirtualList: (element: typeof HelixHeadlessVirtualList, sheet?: CSSResult) => void;
|
|
@@ -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.
|
|
92
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
600
|
-
* drawn, and the first row on screen
|
|
601
|
-
*
|
|
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);
|