@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.
Files changed (124) hide show
  1. package/custom-elements.json +16160 -12084
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
  5. package/dist/controllers/children-controller/children-controller.d.ts +28 -0
  6. package/dist/controllers/children-controller/children-controller.js +18 -0
  7. package/dist/controllers/countdown-controller/countdown-controller.d.ts +15 -1
  8. package/dist/controllers/countdown-controller/countdown-controller.js +27 -2
  9. package/dist/controllers/floating-controller/floating-controller.d.ts +12 -1
  10. package/dist/controllers/floating-controller/floating-controller.js +2 -1
  11. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +25 -0
  12. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +8 -0
  13. package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
  14. package/dist/controllers/machine-controller/machine-controller.js +78 -0
  15. package/dist/controllers/machine-controller/machine.d.ts +93 -0
  16. package/dist/controllers/machine-controller/machine.js +20 -0
  17. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +10 -0
  18. package/dist/controllers/open-phase-controller/open-phase-controller.js +7 -4
  19. package/dist/controllers/option-window-controller/option-window-controller.d.ts +96 -0
  20. package/dist/controllers/option-window-controller/option-window-controller.js +81 -0
  21. package/dist/controllers/popover-controller/popover-controller.js +1 -1
  22. package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
  23. package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
  24. package/dist/controllers/source-controller/source-controller.js +1 -1
  25. package/dist/controllers/virtualize-controller/heights.js +1 -1
  26. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
  27. package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
  28. package/dist/events/before-reorder.d.ts +14 -0
  29. package/dist/events/before-reorder.js +11 -0
  30. package/dist/events/details.d.ts +28 -0
  31. package/dist/events/events.d.ts +4 -2
  32. package/dist/events/events.js +3 -1
  33. package/dist/events/names.js +1 -1
  34. package/dist/events/reorder.d.ts +18 -12
  35. package/dist/events/reordered.d.ts +4 -4
  36. package/dist/helix-element/helix-element.d.ts +14 -5
  37. package/dist/helix-element/helix-element.js +6 -3
  38. package/dist/helix-element/react-audit-off.d.ts +11 -0
  39. package/dist/helix-element/react-audit-off.js +1 -0
  40. package/dist/helix-element/react-audit.d.ts +20 -0
  41. package/dist/helix-element/react-audit.js +56 -0
  42. package/dist/helix-element/react-bridge.d.ts +2 -22
  43. package/dist/helix-element/react-bridge.js +1 -56
  44. package/dist/index.d.ts +34 -14
  45. package/dist/index.js +32 -12
  46. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
  47. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +6 -0
  48. package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
  49. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
  50. package/dist/mixins/mixin-field/mixin-field.js +1 -1
  51. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
  52. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
  53. package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
  54. package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
  55. package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
  56. package/dist/mixins/mixin-viewport/mixin-viewport.js +23 -41
  57. package/dist/utils/assign-slots/assign-slots.d.ts +18 -0
  58. package/dist/utils/assign-slots/assign-slots.js +14 -0
  59. package/dist/utils/autofill-select/autofill-select.d.ts +40 -0
  60. package/dist/utils/autofill-select/autofill-select.js +14 -0
  61. package/dist/utils/calendar/calendar.d.ts +27 -0
  62. package/dist/utils/calendar/calendar.js +17 -0
  63. package/dist/utils/center-in-view/center-in-view.d.ts +18 -0
  64. package/dist/utils/center-in-view/center-in-view.js +14 -0
  65. package/dist/utils/chord/key-names.d.ts +30 -0
  66. package/dist/utils/chord/key-names.js +52 -0
  67. package/dist/utils/column-step/column-step.d.ts +13 -0
  68. package/dist/utils/column-step/column-step.js +9 -0
  69. package/dist/utils/count-label/count-label.d.ts +7 -4
  70. package/dist/utils/count-label/count-label.js +2 -5
  71. package/dist/utils/data-options/data-options.d.ts +60 -0
  72. package/dist/utils/data-options/data-options.js +46 -0
  73. package/dist/utils/date-segments/date-segments.d.ts +2 -1
  74. package/dist/utils/date-segments/date-segments.js +10 -0
  75. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +7 -2
  76. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -1
  77. package/dist/utils/draw-tooltip/spoken-reading.d.ts +3 -2
  78. package/dist/utils/draw-tooltip/spoken-reading.js +3 -2
  79. package/dist/utils/export-parts/export-parts.d.ts +14 -0
  80. package/dist/utils/export-parts/export-parts.js +1 -0
  81. package/dist/utils/fill-template/fill-template.d.ts +9 -0
  82. package/dist/utils/fill-template/fill-template.js +3 -0
  83. package/dist/utils/filter-marks/filter-marks.d.ts +36 -0
  84. package/dist/utils/filter-marks/filter-marks.js +41 -0
  85. package/dist/utils/focus-target/focus-target.d.ts +12 -0
  86. package/dist/utils/focus-target/focus-target.js +9 -0
  87. package/dist/utils/focusable-elements/focusable-elements.d.ts +17 -0
  88. package/dist/utils/focusable-elements/focusable-elements.js +8 -0
  89. package/dist/utils/grid-move/grid-move.d.ts +56 -0
  90. package/dist/utils/grid-move/grid-move.js +28 -0
  91. package/dist/utils/locale-of/locale-of.d.ts +5 -0
  92. package/dist/utils/locale-of/locale-of.js +8 -2
  93. package/dist/utils/number-format/number-format.d.ts +7 -0
  94. package/dist/utils/number-format/number-format.js +1 -0
  95. package/dist/utils/reorder/drag-preview.d.ts +46 -0
  96. package/dist/utils/reorder/drag-preview.js +58 -0
  97. package/dist/utils/reorder/reorder.d.ts +178 -0
  98. package/dist/utils/reorder/reorder.js +112 -0
  99. package/dist/utils/report-runtime/report-runtime.d.ts +14 -5
  100. package/dist/utils/report-values/report-values.d.ts +16 -4
  101. package/dist/utils/report-values/report-values.js +2 -2
  102. package/dist/utils/request-change/request-change.d.ts +11 -0
  103. package/dist/utils/request-change/request-change.js +10 -0
  104. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +17 -0
  105. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +1 -0
  106. package/dist/utils/slot-filled/slot-filled.d.ts +8 -0
  107. package/dist/utils/slot-filled/slot-filled.js +1 -0
  108. package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
  109. package/dist/utils/slotted-words/slotted-words.js +3 -1
  110. package/dist/utils/toolbar-around/toolbar-around.d.ts +18 -0
  111. package/dist/utils/toolbar-around/toolbar-around.js +5 -0
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/dist/words/index.d.ts +3 -0
  115. package/dist/words/index.js +3 -0
  116. package/dist/words/message.d.ts +47 -0
  117. package/dist/words/message.js +152 -0
  118. package/dist/words/registry.d.ts +89 -0
  119. package/dist/words/registry.js +101 -0
  120. package/dist/words/words-locale.d.ts +34 -0
  121. package/dist/words/words-locale.js +30 -0
  122. package/dist/words/words.d.ts +52 -0
  123. package/dist/words/words.js +100 -0
  124. package/package.json +40 -25
@@ -1,4 +1,4 @@
1
- import { auditName } from '#audit-name';
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 is the
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
- const format = formatNumbers(this.host.locale ?? '');
159
- return fillTemplate(words, { count: format(shown.length), position: format(place + 1) });
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. `DelayController` is the other wait
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.timer !== undefined || this.remaining === undefined || this.remaining <= 0) {
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', `${Math.round(rects.reference.width)}px`);
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
- if (!element || typeof element.getAnimations !== 'function') {
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) {