@fusion.dev/helix-core 0.147.0 → 0.149.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 +16160 -12084
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
- package/dist/controllers/children-controller/children-controller.d.ts +28 -0
- package/dist/controllers/children-controller/children-controller.js +18 -0
- package/dist/controllers/countdown-controller/countdown-controller.d.ts +15 -1
- package/dist/controllers/countdown-controller/countdown-controller.js +27 -2
- package/dist/controllers/floating-controller/floating-controller.d.ts +12 -1
- package/dist/controllers/floating-controller/floating-controller.js +2 -1
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +25 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +8 -0
- package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
- package/dist/controllers/machine-controller/machine-controller.js +78 -0
- package/dist/controllers/machine-controller/machine.d.ts +93 -0
- package/dist/controllers/machine-controller/machine.js +20 -0
- package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +10 -0
- package/dist/controllers/open-phase-controller/open-phase-controller.js +7 -4
- package/dist/controllers/option-window-controller/option-window-controller.d.ts +96 -0
- package/dist/controllers/option-window-controller/option-window-controller.js +81 -0
- package/dist/controllers/popover-controller/popover-controller.js +1 -1
- package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
- package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
- package/dist/controllers/source-controller/source-controller.js +1 -1
- package/dist/controllers/virtualize-controller/heights.js +1 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
- package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
- package/dist/events/before-reorder.d.ts +14 -0
- package/dist/events/before-reorder.js +11 -0
- package/dist/events/details.d.ts +28 -0
- package/dist/events/events.d.ts +4 -2
- package/dist/events/events.js +3 -1
- package/dist/events/names.js +1 -1
- package/dist/events/reorder.d.ts +18 -12
- package/dist/events/reordered.d.ts +4 -4
- package/dist/helix-element/helix-element.d.ts +14 -5
- package/dist/helix-element/helix-element.js +6 -3
- package/dist/helix-element/react-audit-off.d.ts +11 -0
- package/dist/helix-element/react-audit-off.js +1 -0
- package/dist/helix-element/react-audit.d.ts +20 -0
- package/dist/helix-element/react-audit.js +56 -0
- package/dist/helix-element/react-bridge.d.ts +2 -22
- package/dist/helix-element/react-bridge.js +1 -56
- package/dist/index.d.ts +34 -14
- package/dist/index.js +32 -12
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +6 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
- package/dist/mixins/mixin-field/mixin-field.js +1 -1
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
- package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
- package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
- package/dist/mixins/mixin-viewport/mixin-viewport.js +23 -41
- package/dist/utils/assign-slots/assign-slots.d.ts +18 -0
- package/dist/utils/assign-slots/assign-slots.js +14 -0
- package/dist/utils/autofill-select/autofill-select.d.ts +40 -0
- package/dist/utils/autofill-select/autofill-select.js +14 -0
- package/dist/utils/calendar/calendar.d.ts +27 -0
- package/dist/utils/calendar/calendar.js +17 -0
- package/dist/utils/center-in-view/center-in-view.d.ts +18 -0
- package/dist/utils/center-in-view/center-in-view.js +14 -0
- package/dist/utils/chord/key-names.d.ts +30 -0
- package/dist/utils/chord/key-names.js +52 -0
- package/dist/utils/column-step/column-step.d.ts +13 -0
- package/dist/utils/column-step/column-step.js +9 -0
- package/dist/utils/count-label/count-label.d.ts +7 -4
- package/dist/utils/count-label/count-label.js +2 -5
- package/dist/utils/data-options/data-options.d.ts +60 -0
- package/dist/utils/data-options/data-options.js +46 -0
- package/dist/utils/date-segments/date-segments.d.ts +2 -1
- package/dist/utils/date-segments/date-segments.js +10 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +7 -2
- package/dist/utils/draw-tooltip/draw-tooltip.js +1 -1
- package/dist/utils/draw-tooltip/spoken-reading.d.ts +3 -2
- package/dist/utils/draw-tooltip/spoken-reading.js +3 -2
- package/dist/utils/export-parts/export-parts.d.ts +14 -0
- package/dist/utils/export-parts/export-parts.js +1 -0
- package/dist/utils/fill-template/fill-template.d.ts +9 -0
- package/dist/utils/fill-template/fill-template.js +3 -0
- package/dist/utils/filter-marks/filter-marks.d.ts +36 -0
- package/dist/utils/filter-marks/filter-marks.js +41 -0
- package/dist/utils/focus-target/focus-target.d.ts +12 -0
- package/dist/utils/focus-target/focus-target.js +9 -0
- package/dist/utils/focusable-elements/focusable-elements.d.ts +17 -0
- package/dist/utils/focusable-elements/focusable-elements.js +8 -0
- package/dist/utils/grid-move/grid-move.d.ts +56 -0
- package/dist/utils/grid-move/grid-move.js +28 -0
- package/dist/utils/locale-of/locale-of.d.ts +5 -0
- package/dist/utils/locale-of/locale-of.js +8 -2
- package/dist/utils/number-format/number-format.d.ts +7 -0
- package/dist/utils/number-format/number-format.js +1 -0
- package/dist/utils/reorder/drag-preview.d.ts +46 -0
- package/dist/utils/reorder/drag-preview.js +58 -0
- package/dist/utils/reorder/reorder.d.ts +178 -0
- package/dist/utils/reorder/reorder.js +112 -0
- package/dist/utils/report-runtime/report-runtime.d.ts +14 -5
- package/dist/utils/report-values/report-values.d.ts +16 -4
- package/dist/utils/report-values/report-values.js +2 -2
- package/dist/utils/request-change/request-change.d.ts +11 -0
- package/dist/utils/request-change/request-change.js +10 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +17 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +1 -0
- package/dist/utils/slot-filled/slot-filled.d.ts +8 -0
- package/dist/utils/slot-filled/slot-filled.js +1 -0
- package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
- package/dist/utils/slotted-words/slotted-words.js +3 -1
- package/dist/utils/toolbar-around/toolbar-around.d.ts +18 -0
- package/dist/utils/toolbar-around/toolbar-around.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/words/index.d.ts +3 -0
- package/dist/words/index.js +3 -0
- package/dist/words/message.d.ts +47 -0
- package/dist/words/message.js +152 -0
- package/dist/words/registry.d.ts +89 -0
- package/dist/words/registry.js +101 -0
- package/dist/words/words-locale.d.ts +34 -0
- package/dist/words/words-locale.js +30 -0
- package/dist/words/words.d.ts +52 -0
- package/dist/words/words.js +100 -0
- package/package.json +40 -25
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { auditName } from '
|
|
1
|
+
import { auditName } from '@fusion.dev/helix-core/audits/name';
|
|
2
2
|
import { elementById } from '../../utils/element-by-id/element-by-id.js';
|
|
3
3
|
import { nameText } from '../../utils/name-text/name-text.js';
|
|
4
4
|
const RELATIONSHIPS = [
|
|
@@ -61,8 +61,8 @@ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
|
|
|
61
61
|
*/
|
|
62
62
|
readonly positionLabel?: string;
|
|
63
63
|
/**
|
|
64
|
-
* The locale the position's numbers are written in. Absent or empty
|
|
65
|
-
* runtime's own.
|
|
64
|
+
* The locale the position's numbers are written in. Absent or empty reads
|
|
65
|
+
* the nearest `lang`, then the runtime's own, as `wordsLocale` does.
|
|
66
66
|
*/
|
|
67
67
|
readonly locale?: string;
|
|
68
68
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { fillTemplate } from '../../utils/fill-template/fill-template.js';
|
|
2
|
-
import { formatNumbers } from '../../utils/format-numbers/format-numbers.js';
|
|
3
1
|
import { isRtl } from '../../utils/is-rtl/is-rtl.js';
|
|
2
|
+
import { formatWords } from '../../words/words.js';
|
|
4
3
|
import { stepReading } from './reading-step.js';
|
|
5
4
|
export const CHART_PLOT = 'helix-chart-plot';
|
|
6
5
|
export class ChartPointerController {
|
|
@@ -155,8 +154,10 @@ export class ChartPointerController {
|
|
|
155
154
|
if (words === '' || place < 0) {
|
|
156
155
|
return '';
|
|
157
156
|
}
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
return formatWords(this.host, 'positionLabel', {
|
|
158
|
+
count: shown.length,
|
|
159
|
+
position: place + 1,
|
|
160
|
+
});
|
|
160
161
|
}
|
|
161
162
|
columns() {
|
|
162
163
|
const host = this.host;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Calls back whenever a child arrives at its host or leaves it.
|
|
4
|
+
*
|
|
5
|
+
* **A root that assigns its slots by hand needs this, and nothing else
|
|
6
|
+
* does.** `slotchange` fires when an assignment changes, and under
|
|
7
|
+
* `slotAssignment: 'manual'` the only thing that changes one is the host
|
|
8
|
+
* calling `assign`. A child appended after the first render reaches no slot,
|
|
9
|
+
* fires no `slotchange` and schedules no render, so it is drawn nowhere and
|
|
10
|
+
* counted by nothing: a notification added to a feed of three was never shown
|
|
11
|
+
* until some property changed. A default slot takes whatever is there and
|
|
12
|
+
* needs no watcher.
|
|
13
|
+
*
|
|
14
|
+
* **`childList` and not `subtree`.** A child rewriting its own text, a
|
|
15
|
+
* streaming chat turn on every frame, is no arrival, and a subtree watch would
|
|
16
|
+
* render the whole host again for each change.
|
|
17
|
+
*
|
|
18
|
+
* Asks the host for an update unless given something else to do. Watches
|
|
19
|
+
* from connection to disconnection, and again after a move.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ChildrenController implements ReactiveController {
|
|
22
|
+
private readonly host;
|
|
23
|
+
private readonly changed;
|
|
24
|
+
private watcher;
|
|
25
|
+
constructor(host: ReactiveControllerHost & Node, changed?: () => void);
|
|
26
|
+
hostConnected(): void;
|
|
27
|
+
hostDisconnected(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class ChildrenController {
|
|
2
|
+
constructor(host, changed = () => host.requestUpdate()) {
|
|
3
|
+
this.host = host;
|
|
4
|
+
this.changed = changed;
|
|
5
|
+
this.watcher = undefined;
|
|
6
|
+
host.addController(this);
|
|
7
|
+
}
|
|
8
|
+
hostConnected() {
|
|
9
|
+
if (typeof MutationObserver === 'undefined') {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
this.watcher ??= new MutationObserver(() => this.changed());
|
|
13
|
+
this.watcher.observe(this.host, { childList: true });
|
|
14
|
+
}
|
|
15
|
+
hostDisconnected() {
|
|
16
|
+
this.watcher?.disconnect();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -3,6 +3,13 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
|
3
3
|
export interface CountdownOptions {
|
|
4
4
|
/** Runs once the time is spent. */
|
|
5
5
|
done: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a hidden page holds the clock, as a pointer on the message does,
|
|
8
|
+
* so a timed message is not spent in a background tab. Counted as one hold
|
|
9
|
+
* however often the page hides, and a clock started while the page is
|
|
10
|
+
* hidden waits for it to show. Defaults to `false`.
|
|
11
|
+
*/
|
|
12
|
+
holdWhileHidden?: boolean;
|
|
6
13
|
/** Runs with `true` when the clock stops partway and `false` when it runs again. */
|
|
7
14
|
paused?: (paused: boolean) => void;
|
|
8
15
|
}
|
|
@@ -23,7 +30,10 @@ export interface CountdownOptions {
|
|
|
23
30
|
* **Leaving the page pauses it**, keeping the time already spent, and drops
|
|
24
31
|
* the holds, since a pointer or focus on a detached element never leaves it.
|
|
25
32
|
* Coming back does not resume on its own: the host says whether it should,
|
|
26
|
-
* such as only while it is still showing.
|
|
33
|
+
* such as only while it is still showing.
|
|
34
|
+
*
|
|
35
|
+
* **A hidden page holds it too, where the host asks** with `holdWhileHidden`:
|
|
36
|
+
* RAC's toast queue stops its timers while the window is in the background. `DelayController` is the other wait
|
|
27
37
|
* in core, for a wait that starts over and forgets itself on leaving.
|
|
28
38
|
*/
|
|
29
39
|
export declare class CountdownController implements ReactiveController {
|
|
@@ -35,6 +45,10 @@ export declare class CountdownController implements ReactiveController {
|
|
|
35
45
|
private timer;
|
|
36
46
|
/** How many reasons there are to hold the clock. */
|
|
37
47
|
private holds;
|
|
48
|
+
/** Whether a hidden page is one of the holds, with `holdWhileHidden`. */
|
|
49
|
+
private hidden;
|
|
50
|
+
/** Holds the clock as the page hides, and lets it go as the page shows. */
|
|
51
|
+
private readonly handleVisibility;
|
|
38
52
|
constructor(host: ReactiveControllerHost, options: CountdownOptions);
|
|
39
53
|
/** Whether the clock is running now. */
|
|
40
54
|
get running(): boolean;
|
|
@@ -2,15 +2,37 @@ export class CountdownController {
|
|
|
2
2
|
constructor(host, options) {
|
|
3
3
|
this.options = options;
|
|
4
4
|
this.holds = 0;
|
|
5
|
+
this.hidden = false;
|
|
6
|
+
this.handleVisibility = () => {
|
|
7
|
+
if (document.hidden === this.hidden) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
this.hidden = document.hidden;
|
|
11
|
+
if (this.hidden) {
|
|
12
|
+
this.hold();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.release();
|
|
16
|
+
}
|
|
17
|
+
};
|
|
5
18
|
host.addController(this);
|
|
6
19
|
}
|
|
7
20
|
get running() {
|
|
8
21
|
return this.timer !== undefined;
|
|
9
22
|
}
|
|
10
|
-
hostConnected() {
|
|
23
|
+
hostConnected() {
|
|
24
|
+
if (this.options.holdWhileHidden && typeof document !== 'undefined') {
|
|
25
|
+
document.addEventListener('visibilitychange', this.handleVisibility);
|
|
26
|
+
this.handleVisibility();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
11
29
|
hostDisconnected() {
|
|
12
30
|
this.pause();
|
|
13
31
|
this.holds = 0;
|
|
32
|
+
this.hidden = false;
|
|
33
|
+
if (typeof document !== 'undefined') {
|
|
34
|
+
document.removeEventListener('visibilitychange', this.handleVisibility);
|
|
35
|
+
}
|
|
14
36
|
}
|
|
15
37
|
start(ms) {
|
|
16
38
|
if (ms === undefined || !(ms > 0)) {
|
|
@@ -30,7 +52,10 @@ export class CountdownController {
|
|
|
30
52
|
this.options.paused?.(true);
|
|
31
53
|
}
|
|
32
54
|
resume() {
|
|
33
|
-
if (this.
|
|
55
|
+
if (this.hidden ||
|
|
56
|
+
this.timer !== undefined ||
|
|
57
|
+
this.remaining === undefined ||
|
|
58
|
+
this.remaining <= 0) {
|
|
34
59
|
return;
|
|
35
60
|
}
|
|
36
61
|
this.startedAt = Date.now();
|
|
@@ -97,6 +97,17 @@ export interface FloatingOptions {
|
|
|
97
97
|
*/
|
|
98
98
|
within?: () => Element | null | undefined;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* How wide a panel that matches its anchor is drawn: the anchor's width, no
|
|
102
|
+
* wider than the room the panel has inside the window's margins.
|
|
103
|
+
*
|
|
104
|
+
* **A trigger as wide as the window gave a list as wide as the window**, and
|
|
105
|
+
* `shift` keeps a margin it then cannot keep: a Select spanning a phone with no
|
|
106
|
+
* gutter opened its list 8 pixels past the end edge, on both sides of the
|
|
107
|
+
* window at once. The room is Floating UI's `availableWidth` after the shift,
|
|
108
|
+
* so a panel that fits is the anchor's width exactly, as before.
|
|
109
|
+
*/
|
|
110
|
+
export declare const matchedInlineSize: (anchor: number, room: number) => number;
|
|
100
111
|
/**
|
|
101
112
|
* Places one element against another, and keeps it there.
|
|
102
113
|
*
|
|
@@ -123,7 +134,7 @@ export interface FloatingOptions {
|
|
|
123
134
|
* package and none of them is private. the `hooks` rule pairs a documented
|
|
124
135
|
* hook with a rule that reads one, in both directions.
|
|
125
136
|
*
|
|
126
|
-
* @cssproperty --floating-anchor-inline-size - The anchor's width, for a panel that matches it.
|
|
137
|
+
* @cssproperty --floating-anchor-inline-size - The anchor's width, for a panel that matches it, and never wider than the room the panel has inside the window's margins.
|
|
127
138
|
* @cssproperty --floating-arrow-x - Where the arrow meets the anchor, from the panel's left edge in both text directions.
|
|
128
139
|
* @cssproperty --floating-arrow-y - Where the arrow meets the anchor, from the panel's top edge.
|
|
129
140
|
* @cssproperty --floating-available-block-size - How much room is left below, for a panel that scrolls.
|
|
@@ -20,6 +20,7 @@ const corner = (floating) => {
|
|
|
20
20
|
}
|
|
21
21
|
return largest;
|
|
22
22
|
};
|
|
23
|
+
export const matchedInlineSize = (anchor, room) => Math.round(Math.max(0, Math.min(anchor, room)));
|
|
23
24
|
export class FloatingController {
|
|
24
25
|
constructor(host, options) {
|
|
25
26
|
this.run = 0;
|
|
@@ -53,7 +54,7 @@ export class FloatingController {
|
|
|
53
54
|
apply: ({ availableHeight, availableWidth, elements, rects }) => {
|
|
54
55
|
room = availableWidth;
|
|
55
56
|
elements.floating.style.setProperty('--floating-available-block-size', `${Math.round(availableHeight)}px`);
|
|
56
|
-
elements.floating.style.setProperty('--floating-anchor-inline-size', `${
|
|
57
|
+
elements.floating.style.setProperty('--floating-anchor-inline-size', `${matchedInlineSize(rects.reference.width, availableWidth)}px`);
|
|
57
58
|
},
|
|
58
59
|
padding: 8,
|
|
59
60
|
}));
|
|
@@ -25,6 +25,31 @@ export interface NavigableItem {
|
|
|
25
25
|
*/
|
|
26
26
|
readonly words?: string;
|
|
27
27
|
}
|
|
28
|
+
/** What `focusItemOf` reads of an element, where its defaults do not fit. */
|
|
29
|
+
export interface FocusItemOptions {
|
|
30
|
+
/** Whether the item is skipped. Defaults to `false`. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Moves focus to the item. Defaults to the element's own `focus()`. */
|
|
33
|
+
focus?: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Whether a node is the item, for `origin: 'focus'`. Defaults to the element
|
|
36
|
+
* and anything inside it; an item whose children are content of their own,
|
|
37
|
+
* an accordion section's panel, answers for itself alone.
|
|
38
|
+
*/
|
|
39
|
+
owns?: (node: Node | null) => boolean;
|
|
40
|
+
/** The words a reader types the start of. */
|
|
41
|
+
words?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* An item that is only ever focused: arrows move between such items and
|
|
45
|
+
* nothing is chosen on arrival, the shape of a toolbar, a megamenu bar or a
|
|
46
|
+
* legend.
|
|
47
|
+
*
|
|
48
|
+
* Eight families wrote the same object literal by hand, `select` a no-op and
|
|
49
|
+
* `selected` false, and an item that forgot one of them type-checked
|
|
50
|
+
* wherever its `contains` came from an element.
|
|
51
|
+
*/
|
|
52
|
+
export declare const focusItemOf: (element: HTMLElement, options?: FocusItemOptions) => NavigableItem;
|
|
28
53
|
/** Where a move starts from: the item that has focus, or the chosen item. */
|
|
29
54
|
export type KeyboardOrigin = 'focus' | 'selection';
|
|
30
55
|
/** How the controller is told what it is moving through. */
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { inlineStep } from '../../utils/inline-step/inline-step.js';
|
|
2
2
|
import { keyClaimed } from '../../utils/key-claimed/key-claimed.js';
|
|
3
3
|
import { typesCharacter } from '../../utils/typeahead/typeahead.js';
|
|
4
|
+
export const focusItemOf = (element, options = {}) => ({
|
|
5
|
+
contains: options.owns ?? ((node) => element.contains(node)),
|
|
6
|
+
disabled: options.disabled ?? false,
|
|
7
|
+
focusItem: options.focus ?? (() => element.focus()),
|
|
8
|
+
select: () => { },
|
|
9
|
+
selected: false,
|
|
10
|
+
...(options.words === undefined ? {} : { words: options.words }),
|
|
11
|
+
});
|
|
4
12
|
const KEYS = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'End', 'Home']);
|
|
5
13
|
export class KeyboardNavigationController {
|
|
6
14
|
constructor(host, { items, origin = 'selection', reserved = () => [], selects = () => true, typeahead, wrap = true, }) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type Machine, type MachineEvent } from './machine.js';
|
|
3
|
+
export * from './machine.js';
|
|
4
|
+
/** An effect a state runs: it may return a function that ends it. */
|
|
5
|
+
export type MachineEffect<E extends MachineEvent, C> = (context: C, by: E | undefined) => undefined | (() => void);
|
|
6
|
+
/** What a `MachineController` starts from and what it runs. */
|
|
7
|
+
export interface MachineOptions<S extends string, E extends MachineEvent, C, F extends string> {
|
|
8
|
+
/** The context the machine starts with. */
|
|
9
|
+
context: C;
|
|
10
|
+
/**
|
|
11
|
+
* Each effect a state names, by name. It runs after the host renders the
|
|
12
|
+
* state, with the event that entered it, and never on a server. The function
|
|
13
|
+
* it returns runs when the state is left and when the host is disconnected.
|
|
14
|
+
*/
|
|
15
|
+
effects: {
|
|
16
|
+
[K in F]: MachineEffect<E, C>;
|
|
17
|
+
};
|
|
18
|
+
/** The state the machine starts in, before the host's first `willUpdate`. */
|
|
19
|
+
initial: S;
|
|
20
|
+
/**
|
|
21
|
+
* Records a state on the host, or takes it off: the host's own
|
|
22
|
+
* `toggleState`, passed in because it is protected.
|
|
23
|
+
*/
|
|
24
|
+
mark?: (state: S, on: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Runs a `Machine` on a host: holds the state and the context, moves them by
|
|
28
|
+
* `step`, and runs each state's effects with their clean-ups.
|
|
29
|
+
*
|
|
30
|
+
* The host turns its property writes into events in `willUpdate`, so a write
|
|
31
|
+
* moves the machine the way a press does, and the state a server prints comes
|
|
32
|
+
* from properties. Effects run in `hostUpdated`, which a server never calls,
|
|
33
|
+
* and none runs at the first update except a state's `atStart` effects: an
|
|
34
|
+
* element served in a state was not moved there by anybody.
|
|
35
|
+
*/
|
|
36
|
+
export declare class MachineController<S extends string, E extends MachineEvent, C, F extends string = string> implements ReactiveController {
|
|
37
|
+
/** The current state. */
|
|
38
|
+
state: S;
|
|
39
|
+
/** The current context. */
|
|
40
|
+
context: C;
|
|
41
|
+
private readonly host;
|
|
42
|
+
private readonly machine;
|
|
43
|
+
private readonly options;
|
|
44
|
+
/** The event that entered the current state, which its effects are told. */
|
|
45
|
+
private by;
|
|
46
|
+
/** The clean-ups of the effects that are running. */
|
|
47
|
+
private ends;
|
|
48
|
+
/** Whether the host has finished its first update. */
|
|
49
|
+
private live;
|
|
50
|
+
/** Whether the current state's effects have run. */
|
|
51
|
+
private ran;
|
|
52
|
+
constructor(host: ReactiveControllerHost & EventTarget, machine: Machine<S, E, C, F>, options: MachineOptions<S, E, C, F>);
|
|
53
|
+
/** Whether an event would move the machine, with nothing run or asked. */
|
|
54
|
+
can(event: E): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Moves the machine by an event, and returns whether it moved.
|
|
57
|
+
*
|
|
58
|
+
* A request is announced first and applies only where nobody refused it.
|
|
59
|
+
* Entering a state ends the effects of the state it leaves at once, and runs
|
|
60
|
+
* its own after the host's next update.
|
|
61
|
+
*/
|
|
62
|
+
send(event: E): boolean;
|
|
63
|
+
hostConnected(): void;
|
|
64
|
+
hostUpdated(): void;
|
|
65
|
+
hostDisconnected(): void;
|
|
66
|
+
/** Ends the running effects, so the next update runs the current state's. */
|
|
67
|
+
private end;
|
|
68
|
+
private mark;
|
|
69
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { requestChange } from '../../utils/request-change/request-change.js';
|
|
2
|
+
import { step } from './machine.js';
|
|
3
|
+
export * from './machine.js';
|
|
4
|
+
export class MachineController {
|
|
5
|
+
constructor(host, machine, options) {
|
|
6
|
+
this.by = undefined;
|
|
7
|
+
this.ends = [];
|
|
8
|
+
this.live = false;
|
|
9
|
+
this.ran = false;
|
|
10
|
+
this.host = host;
|
|
11
|
+
this.machine = machine;
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.state = options.initial;
|
|
14
|
+
this.context = options.context;
|
|
15
|
+
this.mark(this.state, true);
|
|
16
|
+
host.addController(this);
|
|
17
|
+
}
|
|
18
|
+
can(event) {
|
|
19
|
+
return step(this.machine, this.state, this.context, event).changed;
|
|
20
|
+
}
|
|
21
|
+
send(event) {
|
|
22
|
+
const next = step(this.machine, this.state, this.context, event);
|
|
23
|
+
if (!next.changed) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const apply = () => {
|
|
27
|
+
if (next.entered) {
|
|
28
|
+
this.end();
|
|
29
|
+
this.by = event;
|
|
30
|
+
}
|
|
31
|
+
this.mark(this.state, false);
|
|
32
|
+
this.state = next.state;
|
|
33
|
+
this.context = next.context;
|
|
34
|
+
this.mark(this.state, true);
|
|
35
|
+
this.host.requestUpdate();
|
|
36
|
+
};
|
|
37
|
+
if (next.ask) {
|
|
38
|
+
return requestChange({ apply, event: next.ask, host: this.host, next: 0 });
|
|
39
|
+
}
|
|
40
|
+
apply();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
hostConnected() {
|
|
44
|
+
if (this.live) {
|
|
45
|
+
this.by = undefined;
|
|
46
|
+
this.hostUpdated();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
hostUpdated() {
|
|
50
|
+
const first = !this.live;
|
|
51
|
+
this.live = true;
|
|
52
|
+
if (this.ran) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.ran = true;
|
|
56
|
+
const state = this.machine.states[this.state];
|
|
57
|
+
for (const name of (first ? state.atStart : state.effects) ?? []) {
|
|
58
|
+
const end = this.options.effects[name](this.context, first ? undefined : this.by);
|
|
59
|
+
if (end) {
|
|
60
|
+
this.ends.push(end);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
hostDisconnected() {
|
|
65
|
+
this.end();
|
|
66
|
+
}
|
|
67
|
+
end() {
|
|
68
|
+
for (const end of this.ends.splice(0)) {
|
|
69
|
+
end();
|
|
70
|
+
}
|
|
71
|
+
this.ran = false;
|
|
72
|
+
}
|
|
73
|
+
mark(state, on) {
|
|
74
|
+
if (this.machine.states[state].mark) {
|
|
75
|
+
this.options.mark?.(state, on);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Something that happens to a machine, named by `type`, with whatever it carries. */
|
|
2
|
+
export interface MachineEvent {
|
|
3
|
+
/** The event's name, such as `NEXT`. */
|
|
4
|
+
type: string;
|
|
5
|
+
}
|
|
6
|
+
/** One way an event moves a machine. Every function on it is pure. */
|
|
7
|
+
export interface Transition<S extends string, E extends MachineEvent, C> {
|
|
8
|
+
/**
|
|
9
|
+
* The cancelable event announced before the move, which makes the move a
|
|
10
|
+
* request. It goes through `requestChange`, so a listener calling
|
|
11
|
+
* `preventDefault()` refuses it, and a refusal changes nothing, as a failed
|
|
12
|
+
* guard does.
|
|
13
|
+
*/
|
|
14
|
+
ask?: ((context: C, event: E) => Event) | undefined;
|
|
15
|
+
/** The context after the move. */
|
|
16
|
+
assign?: (context: C, event: E) => C;
|
|
17
|
+
/** Whether this way applies. The first way whose guard passes is taken. */
|
|
18
|
+
guard?: (context: C, event: E) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The state the move enters. Given, the state is entered again even where it
|
|
21
|
+
* is the same state, so its effects end and run again. Left out, the machine
|
|
22
|
+
* stays where it is and only `assign` applies.
|
|
23
|
+
*/
|
|
24
|
+
to?: S | ((context: C, event: E) => S) | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What a state does with one event: one way, ways tried in order, or
|
|
28
|
+
* `'ignore'`, which says in the table that the state hears the event and does
|
|
29
|
+
* nothing with it.
|
|
30
|
+
*/
|
|
31
|
+
export type MachineAnswer<S extends string, E extends MachineEvent, C> = Transition<S, E, C> | readonly Transition<S, E, C>[] | 'ignore';
|
|
32
|
+
/** One state of a machine: its answers to events, and the effects it runs. */
|
|
33
|
+
export interface MachineState<S extends string, E extends MachineEvent, C, F extends string> {
|
|
34
|
+
/**
|
|
35
|
+
* The effects that also run when the element starts in this state, at its
|
|
36
|
+
* first update. They run with no event, because nobody moved the element
|
|
37
|
+
* there. Every other effect waits for a move.
|
|
38
|
+
*/
|
|
39
|
+
atStart?: readonly F[];
|
|
40
|
+
/** The effects that run while the element is in this state, by name. */
|
|
41
|
+
effects?: readonly F[];
|
|
42
|
+
/** Records the state on the host as `:state(<name>)` while it is current. */
|
|
43
|
+
mark?: boolean;
|
|
44
|
+
/** What each event does in this state. An event left out changes nothing. */
|
|
45
|
+
on?: {
|
|
46
|
+
[T in E['type']]?: MachineAnswer<S, E, C>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A table of states. Each state lists the events it answers and the effects
|
|
51
|
+
* it runs. The effects are named here and written on the element, so the table
|
|
52
|
+
* stays pure and a spec can walk it.
|
|
53
|
+
*/
|
|
54
|
+
export interface Machine<S extends string, E extends MachineEvent, C, F extends string = string> {
|
|
55
|
+
/** Every state, by name. */
|
|
56
|
+
states: {
|
|
57
|
+
[K in S]: MachineState<S, E, C, F>;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The exhaustive form of a machine: every state answers every event, with a
|
|
62
|
+
* transition or with `'ignore'`. A table typed this way cannot leave a pair
|
|
63
|
+
* out by accident, so a reviewer reads every pair the machine ignores.
|
|
64
|
+
*/
|
|
65
|
+
export interface ExhaustiveMachine<S extends string, E extends MachineEvent, C, F extends string = string> extends Machine<S, E, C, F> {
|
|
66
|
+
states: {
|
|
67
|
+
[K in S]: MachineState<S, E, C, F> & {
|
|
68
|
+
on: {
|
|
69
|
+
[T in E['type']]: MachineAnswer<S, E, C>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** What one event does to a state and its context. */
|
|
75
|
+
export interface MachineStep<S extends string, C> {
|
|
76
|
+
/** Makes the event announced before the move, where the move is a request. */
|
|
77
|
+
ask?: (() => Event) | undefined;
|
|
78
|
+
/** Whether anything moves: the state, the context, or both. */
|
|
79
|
+
changed: boolean;
|
|
80
|
+
/** The context after the event. */
|
|
81
|
+
context: C;
|
|
82
|
+
/** Whether the event enters a state, so its effects end and run again. */
|
|
83
|
+
entered: boolean;
|
|
84
|
+
/** The state after the event. */
|
|
85
|
+
state: S;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What an event does to a state and its context, with nothing run.
|
|
89
|
+
*
|
|
90
|
+
* Takes the first way whose guard passes. An event the state does not answer,
|
|
91
|
+
* an event it ignores, and an event whose guards all fail change nothing.
|
|
92
|
+
*/
|
|
93
|
+
export declare const step: <S extends string, E extends MachineEvent, C>(machine: Machine<S, E, C, string>, state: S, context: C, event: E) => MachineStep<S, C>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const step = (machine, state, context, event) => {
|
|
2
|
+
const answer = machine.states[state].on?.[event.type];
|
|
3
|
+
const ways = answer === undefined || answer === 'ignore' ? [] : [answer].flat();
|
|
4
|
+
for (const way of ways) {
|
|
5
|
+
if (way.guard?.(context, event) === false) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
const next = way.assign ? way.assign(context, event) : context;
|
|
9
|
+
const entered = way.to !== undefined;
|
|
10
|
+
const ask = way.ask;
|
|
11
|
+
return {
|
|
12
|
+
ask: ask && (() => ask(context, event)),
|
|
13
|
+
changed: entered || next !== context,
|
|
14
|
+
context: next,
|
|
15
|
+
entered,
|
|
16
|
+
state: typeof way.to === 'function' ? way.to(context, event) : (way.to ?? state),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { changed: false, context, entered: false, state };
|
|
20
|
+
};
|
|
@@ -14,6 +14,16 @@ export interface OpenPhaseOptions {
|
|
|
14
14
|
/** The element whose animations are the entrance and the exit. */
|
|
15
15
|
surface: () => Element | null | undefined;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves once every animation running on an element and inside it has
|
|
19
|
+
* finished, or at once where none is running.
|
|
20
|
+
*
|
|
21
|
+
* **Asked now, with no frame waited first**, for an exit that is already
|
|
22
|
+
* running when the caller asks: a section closing, a message leaving. A
|
|
23
|
+
* canceled animation counts as finished. `animationsSettled` waits two frames
|
|
24
|
+
* first, for an entrance that has not started yet.
|
|
25
|
+
*/
|
|
26
|
+
export declare const animationsFinished: (element: Element | null | undefined) => Promise<void>;
|
|
17
27
|
/**
|
|
18
28
|
* Resolves once an element's animations have finished, or two frames on where
|
|
19
29
|
* it has none.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const animationsFinished = async (element) => {
|
|
2
|
+
if (!element || typeof element.getAnimations !== 'function') {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
await Promise.allSettled(element.getAnimations({ subtree: true }).map((one) => one.finished));
|
|
6
|
+
};
|
|
1
7
|
export const animationsSettled = async (element) => {
|
|
2
8
|
const frame = () => new Promise((resolve) => {
|
|
3
9
|
if (typeof requestAnimationFrame === 'function') {
|
|
@@ -9,10 +15,7 @@ export const animationsSettled = async (element) => {
|
|
|
9
15
|
});
|
|
10
16
|
await frame();
|
|
11
17
|
await frame();
|
|
12
|
-
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
await Promise.allSettled(element.getAnimations({ subtree: true }).map((one) => one.finished));
|
|
18
|
+
await animationsFinished(element);
|
|
16
19
|
};
|
|
17
20
|
export class OpenPhaseController {
|
|
18
21
|
constructor(host, options) {
|