@fusion.dev/helix-core 0.143.0 → 0.145.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 (107) hide show
  1. package/custom-elements.json +10266 -4161
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +10 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +6 -0
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +89 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +102 -1
  6. package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
  7. package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
  8. package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
  9. package/dist/controllers/copy-controller/copy-controller.js +11 -9
  10. package/dist/controllers/countdown-controller/countdown-controller.d.ts +55 -0
  11. package/dist/controllers/countdown-controller/countdown-controller.js +57 -0
  12. package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
  13. package/dist/controllers/delay-controller/delay-controller.js +54 -0
  14. package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
  15. package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
  16. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +11 -2
  17. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +4 -1
  18. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
  19. package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
  20. package/dist/controllers/source-controller/source-controller.d.ts +16 -0
  21. package/dist/controllers/source-controller/source-controller.js +44 -1
  22. package/dist/controllers/status-controller/status-controller.d.ts +47 -0
  23. package/dist/controllers/status-controller/status-controller.js +34 -0
  24. package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
  25. package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
  26. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +68 -35
  27. package/dist/controllers/virtualize-controller/virtualize-controller.js +77 -10
  28. package/dist/events/events.d.ts +221 -0
  29. package/dist/events/events.js +150 -0
  30. package/dist/helix-element/helix-element.js +14 -1
  31. package/dist/helix-element/react-bridge.d.ts +9 -4
  32. package/dist/helix-element/react-bridge.js +14 -1
  33. package/dist/index.d.ts +31 -9
  34. package/dist/index.js +29 -7
  35. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
  36. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +5 -1
  37. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
  38. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
  39. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
  40. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +7 -3
  41. package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
  42. package/dist/mixins/mixin-field/mixin-field.js +8 -0
  43. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +16 -6
  44. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +19 -1
  45. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +5 -4
  46. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +12 -1
  47. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
  48. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
  49. package/dist/utils/arrow/arrow.d.ts +15 -0
  50. package/dist/utils/arrow/arrow.js +2 -0
  51. package/dist/utils/audit-name/audit-name.d.ts +28 -0
  52. package/dist/utils/audit-name/audit-name.js +95 -0
  53. package/dist/utils/calendar/calendar.d.ts +117 -0
  54. package/dist/utils/calendar/calendar.js +96 -0
  55. package/dist/utils/chord/chord.d.ts +43 -0
  56. package/dist/utils/chord/chord.js +53 -0
  57. package/dist/utils/clock/clock.d.ts +87 -0
  58. package/dist/utils/clock/clock.js +68 -0
  59. package/dist/utils/color-name/color-name.d.ts +28 -0
  60. package/dist/utils/color-name/color-name.js +150 -0
  61. package/dist/utils/count-label/count-label.d.ts +13 -0
  62. package/dist/utils/count-label/count-label.js +5 -0
  63. package/dist/utils/css-color/css-color.d.ts +12 -0
  64. package/dist/utils/css-color/css-color.js +2 -2
  65. package/dist/utils/date-segments/date-segments.d.ts +122 -0
  66. package/dist/utils/date-segments/date-segments.js +206 -0
  67. package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
  68. package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
  69. package/dist/utils/draw-icon/draw-icon.d.ts +7 -6
  70. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -21
  71. package/dist/utils/draw-tooltip/draw-tooltip.js +11 -5
  72. package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
  73. package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
  74. package/dist/utils/follow-link/follow-link.d.ts +46 -0
  75. package/dist/utils/follow-link/follow-link.js +37 -0
  76. package/dist/utils/heading-in/heading-in.d.ts +19 -0
  77. package/dist/utils/heading-in/heading-in.js +14 -0
  78. package/dist/utils/number-format/number-format.d.ts +23 -0
  79. package/dist/utils/number-format/number-format.js +55 -0
  80. package/dist/utils/platform/platform.d.ts +23 -0
  81. package/dist/utils/platform/platform.js +11 -0
  82. package/dist/utils/range-add/range-add.d.ts +19 -0
  83. package/dist/utils/range-add/range-add.js +5 -0
  84. package/dist/utils/read-color/read-color.d.ts +14 -0
  85. package/dist/utils/read-color/read-color.js +13 -0
  86. package/dist/utils/read-series/read-series.d.ts +6 -0
  87. package/dist/utils/read-series/read-series.js +1 -0
  88. package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
  89. package/dist/utils/reflect-elements/reflect-elements.js +40 -0
  90. package/dist/utils/relative-time/relative-time.d.ts +13 -1
  91. package/dist/utils/relative-time/relative-time.js +7 -4
  92. package/dist/utils/report-runtime/report-runtime.d.ts +5 -1
  93. package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
  94. package/dist/utils/roving-stop/roving-stop.js +3 -0
  95. package/dist/utils/swipe/swipe.d.ts +63 -0
  96. package/dist/utils/swipe/swipe.js +48 -0
  97. package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
  98. package/dist/utils/tab-stops/tab-stops.js +22 -0
  99. package/dist/utils/value-as/value-as.d.ts +20 -0
  100. package/dist/utils/value-as/value-as.js +16 -0
  101. package/dist/utils/write-time/write-time.d.ts +15 -5
  102. package/dist/utils/write-time/write-time.js +7 -7
  103. package/dist/utils/written-list/written-list.d.ts +18 -0
  104. package/dist/utils/written-list/written-list.js +8 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +4 -4
@@ -0,0 +1,54 @@
1
+ export class DelayController {
2
+ constructor(host, options = {}) {
3
+ this.host = host;
4
+ this.options = options;
5
+ this.elapsed = false;
6
+ this.timer = undefined;
7
+ this.done = undefined;
8
+ host.addController(this);
9
+ }
10
+ get pending() {
11
+ return this.timer !== undefined;
12
+ }
13
+ hostConnected() { }
14
+ hostDisconnected() {
15
+ this.cancel();
16
+ }
17
+ start(ms, done) {
18
+ this.cancel();
19
+ this.mark(false);
20
+ if (ms === undefined) {
21
+ return;
22
+ }
23
+ this.done = done;
24
+ this.timer = setTimeout(() => this.finish(), ms);
25
+ }
26
+ cancel() {
27
+ clearTimeout(this.timer);
28
+ this.timer = undefined;
29
+ this.done = undefined;
30
+ }
31
+ flush() {
32
+ if (this.timer === undefined) {
33
+ return;
34
+ }
35
+ clearTimeout(this.timer);
36
+ this.finish();
37
+ }
38
+ finish() {
39
+ const done = this.done;
40
+ this.timer = undefined;
41
+ this.done = undefined;
42
+ this.mark(true);
43
+ done?.();
44
+ }
45
+ mark(elapsed) {
46
+ if (elapsed === this.elapsed) {
47
+ return;
48
+ }
49
+ this.elapsed = elapsed;
50
+ if (this.options.renders !== false) {
51
+ this.host.requestUpdate();
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,56 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ export interface FocusScopeOptions {
3
+ /**
4
+ * The elements whose controls are inside the scope, in order. For a surface
5
+ * that holds a slot, these are the elements assigned to it: a query on the
6
+ * surface stops at the slot and never reaches them.
7
+ */
8
+ contents: () => readonly Element[];
9
+ /** Whether `Tab` is kept inside the scope, wrapping at either end. */
10
+ contain?: () => boolean;
11
+ /**
12
+ * Where focus goes on the way in: the first control inside, or the surface
13
+ * itself. The first control unless said, falling back to the surface where
14
+ * there is none.
15
+ */
16
+ start?: 'first' | 'surface';
17
+ /** The box focus is kept in, which takes focus itself where nothing inside can. */
18
+ surface: () => HTMLElement | null | undefined;
19
+ }
20
+ /** The controls among some elements and inside them, in order. */
21
+ export declare const controlsIn: (elements: readonly Element[]) => HTMLElement[];
22
+ /**
23
+ * Moves focus into a surface and back out of it, and can keep `Tab` inside
24
+ * while it is open.
25
+ *
26
+ * The part of an overlay that is about focus and nothing else: a popover that
27
+ * moves focus in as it opens, a modal popover that keeps it there, a toast
28
+ * region a hotkey takes focus to. Each of them decides *when*; this does the
29
+ * moving, and remembers where focus came from so `leave` can give it back.
30
+ *
31
+ * **Back only from inside.** A surface closed by a press somewhere else has not
32
+ * taken focus with it, and pulling focus back would move a reader who had
33
+ * already gone where they meant to.
34
+ */
35
+ export declare class FocusScopeController implements ReactiveController {
36
+ private readonly host;
37
+ private readonly options;
38
+ /** Where focus was when it was moved in. */
39
+ private origin;
40
+ constructor(host: ReactiveControllerHost & HTMLElement, options: FocusScopeOptions);
41
+ hostConnected(): void;
42
+ hostDisconnected(): void;
43
+ /** Whether focus is in the surface or anything assigned into it. */
44
+ get holdsFocus(): boolean;
45
+ /** The controls inside the scope, in order. */
46
+ get controls(): HTMLElement[];
47
+ /** Moves focus into the scope, and remembers where it was. */
48
+ enter(): void;
49
+ /**
50
+ * Gives focus back to where it was before `enter`, if focus is still inside
51
+ * the scope. Returns whether it did.
52
+ */
53
+ leave(): boolean;
54
+ /** `Tab` at either end wraps to the other, while the scope is containing. */
55
+ private readonly handleKeydown;
56
+ }
@@ -0,0 +1,71 @@
1
+ import { deepActiveElement, deepContains, focusableElements, } from '../../utils/focusable-elements/focusable-elements.js';
2
+ const takesFocus = (element) => element instanceof HTMLElement &&
3
+ (element.matches('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])') ||
4
+ element.shadowRoot?.delegatesFocus === true);
5
+ export const controlsIn = (elements) => elements.flatMap((one) => [
6
+ ...(takesFocus(one) ? [one] : []),
7
+ ...(one instanceof HTMLElement ? focusableElements(one) : []),
8
+ ]);
9
+ export class FocusScopeController {
10
+ constructor(host, options) {
11
+ this.handleKeydown = (event) => {
12
+ if (event.key !== 'Tab' || event.defaultPrevented || !this.options.contain?.()) {
13
+ return;
14
+ }
15
+ const controls = this.controls;
16
+ const active = deepActiveElement();
17
+ const first = controls[0];
18
+ const last = controls[controls.length - 1];
19
+ if (!first || !last) {
20
+ event.preventDefault();
21
+ this.options.surface()?.focus({ preventScroll: true });
22
+ return;
23
+ }
24
+ const at = (control) => active !== null && deepContains(control, active);
25
+ if (event.shiftKey && (at(first) || active === this.options.surface())) {
26
+ event.preventDefault();
27
+ last.focus();
28
+ }
29
+ else if (!event.shiftKey && at(last)) {
30
+ event.preventDefault();
31
+ first.focus();
32
+ }
33
+ };
34
+ this.host = host;
35
+ this.options = options;
36
+ host.addController(this);
37
+ }
38
+ hostConnected() {
39
+ this.host.addEventListener('keydown', this.handleKeydown);
40
+ }
41
+ hostDisconnected() {
42
+ this.host.removeEventListener('keydown', this.handleKeydown);
43
+ }
44
+ get holdsFocus() {
45
+ const active = deepActiveElement();
46
+ const surface = this.options.surface();
47
+ return (active !== null &&
48
+ ((surface !== null && surface !== undefined && deepContains(surface, active)) ||
49
+ this.options.contents().some((one) => deepContains(one, active))));
50
+ }
51
+ get controls() {
52
+ return controlsIn(this.options.contents());
53
+ }
54
+ enter() {
55
+ const active = deepActiveElement();
56
+ if (active instanceof HTMLElement && !this.holdsFocus) {
57
+ this.origin = active;
58
+ }
59
+ const first = this.options.start === 'surface' ? undefined : this.controls[0];
60
+ (first ?? this.options.surface())?.focus({ preventScroll: true });
61
+ }
62
+ leave() {
63
+ const origin = this.origin;
64
+ this.origin = undefined;
65
+ if (!origin?.isConnected || !this.holdsFocus) {
66
+ return false;
67
+ }
68
+ origin.focus({ preventScroll: true });
69
+ return true;
70
+ }
71
+ }
@@ -19,6 +19,8 @@ export interface NavigableItem {
19
19
  /** Whether the item is the current one, which is where movement starts. */
20
20
  selected: boolean;
21
21
  }
22
+ /** Where a move starts from: the item that has focus, or the chosen item. */
23
+ export type KeyboardOrigin = 'focus' | 'selection';
22
24
  /** How the controller is told what it is moving through. */
23
25
  export interface KeyboardNavigationOptions {
24
26
  /** The items, in the order a reader moves through them. */
@@ -35,8 +37,13 @@ export interface KeyboardNavigationOptions {
35
37
  * every time. It is also the only reading that can tell a press on a tab from
36
38
  * a press somewhere else in the component, which matters wherever the host
37
39
  * holds more than the set (a tab panel is a tab stop of its own).
40
+ *
41
+ * A function where the answer changes with the element's state, read on every
42
+ * press. A read-only radio group is the case: while it is read-only the arrows
43
+ * move focus between radios without choosing, so the move has to start where
44
+ * focus is, and once it is editable again the selection is the start.
38
45
  */
39
- origin?: 'focus' | 'selection';
46
+ origin?: KeyboardOrigin | (() => KeyboardOrigin);
40
47
  /**
41
48
  * Whether arriving also chooses.
42
49
  *
@@ -176,13 +183,15 @@ export declare class Typeahead {
176
183
  export declare class KeyboardNavigationController implements ReactiveController {
177
184
  private readonly host;
178
185
  private readonly items;
179
- private readonly origin;
186
+ private readonly originOf;
180
187
  private readonly selects;
181
188
  private readonly reserved;
182
189
  private readonly wrap;
183
190
  private readonly words;
184
191
  private readonly typeahead;
185
192
  constructor(host: ReactiveControllerHost & HTMLElement, { items, origin, reserved, selects, typeahead, wrap, }: KeyboardNavigationOptions);
193
+ /** Where this press starts from, read again on every press. */
194
+ private get origin();
186
195
  /** Moves to an item, and chooses it where arriving is choosing. */
187
196
  private arrive;
188
197
  /**
@@ -81,13 +81,16 @@ export class KeyboardNavigationController {
81
81
  };
82
82
  this.host = host;
83
83
  this.items = items;
84
- this.origin = origin;
84
+ this.originOf = typeof origin === 'function' ? origin : () => origin;
85
85
  this.reserved = reserved;
86
86
  this.selects = selects;
87
87
  this.words = typeahead;
88
88
  this.wrap = wrap;
89
89
  host.addController(this);
90
90
  }
91
+ get origin() {
92
+ return this.originOf();
93
+ }
91
94
  arrive(target) {
92
95
  target?.focusItem();
93
96
  if (this.selects()) {
@@ -0,0 +1,49 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /** The two journeys an overlay makes. */
3
+ export type OpenPhase = 'entering' | 'exiting';
4
+ /** What an `OpenPhaseController` reads of its host and writes onto it. */
5
+ export interface OpenPhaseOptions {
6
+ /**
7
+ * Records each phase on the host, or takes it off: the host's own
8
+ * `toggleState`, passed in because it is protected, and one callback per
9
+ * phase so each state's name is written out where it is recorded.
10
+ */
11
+ mark: Record<OpenPhase, (on: boolean) => void>;
12
+ /** Whether the host is open, read after every update. */
13
+ open: () => boolean;
14
+ /** The element whose animations are the entrance and the exit. */
15
+ surface: () => Element | null | undefined;
16
+ }
17
+ /**
18
+ * Resolves once an element's animations have finished, or two frames on where
19
+ * it has none.
20
+ *
21
+ * Two frames first, so an entrance that starts on the frame after the change,
22
+ * as a `@starting-style` transition does once the element is shown, is
23
+ * running by the time the element is asked. `subtree`, so a `::backdrop` and
24
+ * anything inside the surface count too.
25
+ */
26
+ export declare const animationsSettled: (element: Element | null | undefined) => Promise<void>;
27
+ /**
28
+ * Says on the host that an overlay is on its way in or out, until its
29
+ * animations have finished, as `:state(entering)` and `:state(exiting)`.
30
+ *
31
+ * `:state(open)` turns at once, which is right for what the overlay is and too
32
+ * early for a stylesheet that wants to draw the journey: an exit has to know it
33
+ * is an exit while the panel is still on screen.
34
+ *
35
+ * **Never on the first update.** An overlay served open has made no journey,
36
+ * and marking one would put a state on a page that nobody moved.
37
+ *
38
+ * A turn counter, because a close that arrives during an entrance starts a new
39
+ * phase, and the entrance settling late must not clear the exit.
40
+ */
41
+ export declare class OpenPhaseController implements ReactiveController {
42
+ private readonly options;
43
+ /** What `open` was after the last update, and `undefined` before the first. */
44
+ private last;
45
+ /** Which phase is the latest. */
46
+ private turn;
47
+ constructor(host: ReactiveControllerHost, options: OpenPhaseOptions);
48
+ hostUpdated(): void;
49
+ }
@@ -0,0 +1,41 @@
1
+ export const animationsSettled = async (element) => {
2
+ const frame = () => new Promise((resolve) => {
3
+ if (typeof requestAnimationFrame === 'function') {
4
+ requestAnimationFrame(() => resolve());
5
+ }
6
+ else {
7
+ setTimeout(resolve, 16);
8
+ }
9
+ });
10
+ await frame();
11
+ await frame();
12
+ if (!element || typeof element.getAnimations !== 'function') {
13
+ return;
14
+ }
15
+ await Promise.allSettled(element.getAnimations({ subtree: true }).map((one) => one.finished));
16
+ };
17
+ export class OpenPhaseController {
18
+ constructor(host, options) {
19
+ this.turn = 0;
20
+ this.options = options;
21
+ host.addController(this);
22
+ }
23
+ hostUpdated() {
24
+ const open = this.options.open();
25
+ const last = this.last;
26
+ this.last = open;
27
+ if (last === undefined || last === open) {
28
+ return;
29
+ }
30
+ this.turn += 1;
31
+ const turn = this.turn;
32
+ this.options.mark.entering(open);
33
+ this.options.mark.exiting(!open);
34
+ void animationsSettled(this.options.surface()).then(() => {
35
+ if (turn === this.turn) {
36
+ this.options.mark.entering(false);
37
+ this.options.mark.exiting(false);
38
+ }
39
+ });
40
+ }
41
+ }
@@ -100,6 +100,22 @@ export declare class SourceController implements ReactiveController {
100
100
  get rows(): Row[];
101
101
  /** A held parse where it still describes the table, or a fresh one. */
102
102
  private parsed;
103
+ /**
104
+ * The series hidden on the table, by lowercased name. Empty where there is
105
+ * no table, which is every server render.
106
+ */
107
+ get hidden(): ReadonlySet<string>;
108
+ /**
109
+ * Hides these series on the table and shows every other, for every chart
110
+ * and legend reading it, which then redraw.
111
+ *
112
+ * Names are matched without regard to case or surrounding space, as a
113
+ * column's heading is everywhere else. Nothing happens where `for` names no
114
+ * table, or the set is already this one.
115
+ */
116
+ hide(names: Iterable<string>): void;
117
+ /** Counts this controller among the readers of the table it watches, or of none. */
118
+ private enlist;
103
119
  hostConnected(): void;
104
120
  hostUpdated(): void;
105
121
  hostDisconnected(): void;
@@ -1,5 +1,8 @@
1
1
  import { elementById } from '../../utils/element-by-id/element-by-id.js';
2
- import { readRows, readSeries } from '../../utils/read-series/read-series.js';
2
+ import { readRows, readSeries, seriesKey, } from '../../utils/read-series/read-series.js';
3
+ const HIDDEN = new WeakMap();
4
+ const READERS = new WeakMap();
5
+ const NONE = new Set();
3
6
  export class SourceController {
4
7
  constructor(host) {
5
8
  this.watched = null;
@@ -47,6 +50,43 @@ export class SourceController {
47
50
  }
48
51
  return { element, value: read(element), version: this.version };
49
52
  }
53
+ get hidden() {
54
+ const element = this.element;
55
+ return element ? (HIDDEN.get(element) ?? NONE) : NONE;
56
+ }
57
+ hide(names) {
58
+ const element = this.resolve();
59
+ if (!element) {
60
+ return;
61
+ }
62
+ const next = new Set([...names].map(seriesKey).filter(Boolean));
63
+ const now = HIDDEN.get(element) ?? NONE;
64
+ if (next.size === now.size && [...next].every((one) => now.has(one))) {
65
+ return;
66
+ }
67
+ if (next.size === 0) {
68
+ HIDDEN.delete(element);
69
+ }
70
+ else {
71
+ HIDDEN.set(element, next);
72
+ }
73
+ for (const reader of READERS.get(element) ?? []) {
74
+ reader.host.requestUpdate();
75
+ }
76
+ }
77
+ enlist(element) {
78
+ if (this.watched) {
79
+ READERS.get(this.watched)?.delete(this);
80
+ }
81
+ if (element) {
82
+ let readers = READERS.get(element);
83
+ if (!readers) {
84
+ readers = new Set();
85
+ READERS.set(element, readers);
86
+ }
87
+ readers.add(this);
88
+ }
89
+ }
50
90
  hostConnected() {
51
91
  this.watch();
52
92
  }
@@ -57,6 +97,8 @@ export class SourceController {
57
97
  }
58
98
  }
59
99
  hostDisconnected() {
100
+ this.enlist(null);
101
+ this.watched = null;
60
102
  this.observer?.disconnect();
61
103
  this.observer = undefined;
62
104
  this.tree?.disconnect();
@@ -67,6 +109,7 @@ export class SourceController {
67
109
  this.tree?.disconnect();
68
110
  this.version += 1;
69
111
  const element = this.resolve();
112
+ this.enlist(element);
70
113
  this.watched = element;
71
114
  if (typeof MutationObserver === 'undefined') {
72
115
  return;
@@ -0,0 +1,47 @@
1
+ import { nothing, type ReactiveController, type ReactiveControllerHost, type TemplateResult } from 'lit';
2
+ /** How `say` speaks. */
3
+ export interface SayOptions {
4
+ /**
5
+ * Whether the region is emptied before the words land, so the same words
6
+ * said twice are heard twice. Defaults to `true`.
7
+ */
8
+ clear?: boolean;
9
+ }
10
+ /**
11
+ * A live status region an element renders only once it has something to say,
12
+ * and the words it says.
13
+ *
14
+ * **Not rendered until the first time**, so an element that never speaks
15
+ * carries no live region: a button that never waits, a file field that never
16
+ * refuses a file. `spoken` says whether it is rendered and `said` what it
17
+ * holds, and `drawStatus` draws it.
18
+ *
19
+ * **The words land a frame after the region**, through `say`, because a region
20
+ * that arrives with its words is read inconsistently, and emptied first, so
21
+ * the same words twice are a change both times. `sayNow` puts them in at once,
22
+ * for a region already rendered that is only changing what it says.
23
+ *
24
+ * `ArrivalController` is the other half of the same idea, for an element that
25
+ * is itself the news.
26
+ */
27
+ export declare class StatusController implements ReactiveController {
28
+ private readonly host;
29
+ /** Whether the region is rendered: from the first time anything is said. */
30
+ spoken: boolean;
31
+ /** What the region says now. */
32
+ said: string;
33
+ constructor(host: ReactiveControllerHost);
34
+ hostConnected(): void;
35
+ /** Says `words` a frame from now, rendering the region at once where it is not yet. */
36
+ say(words: string, { clear }?: SayOptions): void;
37
+ /** Says `words` now, in the same update that renders the region. */
38
+ sayNow(words: string): void;
39
+ /** Records what the region holds, and asks for an update only where that moved. */
40
+ private hold;
41
+ }
42
+ /**
43
+ * The region a `StatusController` speaks in, or nothing before it has spoken:
44
+ * `<span part="live" role="status">`, which the element documents as its
45
+ * `live` part.
46
+ */
47
+ export declare const drawStatus: (status: StatusController) => TemplateResult | typeof nothing;
@@ -0,0 +1,34 @@
1
+ import { html, nothing, } from 'lit';
2
+ export class StatusController {
3
+ constructor(host) {
4
+ this.host = host;
5
+ this.spoken = false;
6
+ this.said = '';
7
+ host.addController(this);
8
+ }
9
+ hostConnected() { }
10
+ say(words, { clear = true } = {}) {
11
+ this.hold(true, clear ? '' : this.said);
12
+ const fill = () => {
13
+ this.hold(true, words);
14
+ };
15
+ if (typeof requestAnimationFrame === 'function') {
16
+ requestAnimationFrame(fill);
17
+ }
18
+ else {
19
+ fill();
20
+ }
21
+ }
22
+ sayNow(words) {
23
+ this.hold(true, words);
24
+ }
25
+ hold(spoken, said) {
26
+ if (spoken === this.spoken && said === this.said) {
27
+ return;
28
+ }
29
+ this.spoken = spoken;
30
+ this.said = said;
31
+ this.host.requestUpdate();
32
+ }
33
+ }
34
+ export const drawStatus = (status) => status.spoken ? html `<span part="live" role="status">${status.said}</span>` : nothing;
@@ -0,0 +1,61 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ import type { SwipeAxis } from '../../utils/swipe/swipe.js';
3
+ export interface SwipeOptions {
4
+ /** The axis a swipe is read along, or `undefined` while swiping is off. */
5
+ axis: () => SwipeAxis | undefined;
6
+ /**
7
+ * Called on the release, with how far the pointer went and how fast it was
8
+ * going, in pixels per millisecond, on each axis.
9
+ */
10
+ end: (dx: number, dy: number, vx: number, vy: number) => void;
11
+ /** Called as the pointer moves, with how far it has gone from the press. */
12
+ move: (dx: number, dy: number) => void;
13
+ /**
14
+ * Whether a press may start a swipe, given the element it landed on. Every
15
+ * press may where this is not given. A field, a scroller that has its own use
16
+ * for the drag, or a mouse where dragging selects text, says no.
17
+ */
18
+ start?: (target: Element, event: PointerEvent) => boolean;
19
+ /** The element a swipe starts on, which carries `data-swiping` while one runs. */
20
+ surface: () => HTMLElement | null | undefined;
21
+ }
22
+ /**
23
+ * Follows a pointer dragged across a surface along one axis, for a surface the
24
+ * reader swipes away or along: a drawer, a toast, a lightbox's pictures.
25
+ *
26
+ * It decides only that a swipe is happening and reports where the pointer is.
27
+ * What a swipe means is the host's, which is why `move` and `end` are told the
28
+ * distance and nothing else: a drawer settles it against snap points, a toast
29
+ * against half its width, and a lightbox turns it into next or previous.
30
+ *
31
+ * **A press is a swipe only once it has moved `SLOP` pixels, mostly along the
32
+ * axis.** Before that it is still a press, so a tap on a button inside the
33
+ * surface is a tap, and a drag across the axis, a reader scrolling, is left to
34
+ * the page. Once it is a swipe, the click that ends it is swallowed, so a
35
+ * swipe that started on a button does not also press it.
36
+ *
37
+ * The move and the release are heard on the window, so a pointer that leaves
38
+ * the surface mid-swipe is still followed.
39
+ */
40
+ export declare class SwipeController implements ReactiveController {
41
+ private readonly host;
42
+ private readonly options;
43
+ /** Where the press began, while one is down. */
44
+ private origin;
45
+ /** The last two readings, for the speed. */
46
+ private samples;
47
+ /** Whether the press has become a swipe. */
48
+ private swiping;
49
+ /** The pointer being followed. */
50
+ private pointer;
51
+ constructor(host: ReactiveControllerHost & HTMLElement, options: SwipeOptions);
52
+ hostConnected(): void;
53
+ hostDisconnected(): void;
54
+ private readonly handleDown;
55
+ private readonly handleMove;
56
+ private readonly handleUp;
57
+ /** A swipe the browser took over, for a scroll of its own, goes back to where it began. */
58
+ private readonly handleCancel;
59
+ /** Forgets the press and stops listening for it. */
60
+ private stop;
61
+ }
@@ -0,0 +1,106 @@
1
+ const SLOP = 8;
2
+ export class SwipeController {
3
+ constructor(host, options) {
4
+ this.samples = [];
5
+ this.swiping = false;
6
+ this.handleDown = (event) => {
7
+ const surface = this.options.surface();
8
+ const path = event.composedPath();
9
+ const target = path[0];
10
+ if (event.button !== 0 ||
11
+ this.options.axis() === undefined ||
12
+ !surface ||
13
+ !(target instanceof Element) ||
14
+ !path.includes(surface) ||
15
+ (this.options.start && !this.options.start(target, event))) {
16
+ return;
17
+ }
18
+ this.pointer = event.pointerId;
19
+ this.origin = { t: event.timeStamp, x: event.clientX, y: event.clientY };
20
+ this.samples = [this.origin];
21
+ this.swiping = false;
22
+ window.addEventListener('pointermove', this.handleMove);
23
+ window.addEventListener('pointerup', this.handleUp);
24
+ window.addEventListener('pointercancel', this.handleCancel);
25
+ };
26
+ this.handleMove = (event) => {
27
+ const origin = this.origin;
28
+ const axis = this.options.axis();
29
+ if (!origin || event.pointerId !== this.pointer || axis === undefined) {
30
+ return;
31
+ }
32
+ const dx = event.clientX - origin.x;
33
+ const dy = event.clientY - origin.y;
34
+ if (!this.swiping) {
35
+ const along = Math.abs(axis === 'x' ? dx : dy);
36
+ const across = Math.abs(axis === 'x' ? dy : dx);
37
+ if (Math.max(along, across) < SLOP) {
38
+ return;
39
+ }
40
+ if (across >= along) {
41
+ this.stop();
42
+ return;
43
+ }
44
+ this.swiping = true;
45
+ this.options.surface()?.setAttribute('data-swiping', '');
46
+ }
47
+ this.samples = [
48
+ ...this.samples.slice(-1),
49
+ { t: event.timeStamp, x: event.clientX, y: event.clientY },
50
+ ];
51
+ this.options.move(dx, dy);
52
+ };
53
+ this.handleUp = (event) => {
54
+ const origin = this.origin;
55
+ if (!origin || event.pointerId !== this.pointer) {
56
+ return;
57
+ }
58
+ const swiped = this.swiping;
59
+ const [before, last] = [this.samples[0], this.samples[this.samples.length - 1]];
60
+ const elapsed = before && last ? last.t - before.t : 0;
61
+ const vx = before && last && elapsed > 0 ? (last.x - before.x) / elapsed : 0;
62
+ const vy = before && last && elapsed > 0 ? (last.y - before.y) / elapsed : 0;
63
+ this.stop();
64
+ if (!swiped) {
65
+ return;
66
+ }
67
+ window.addEventListener('click', swallow, { capture: true, once: true });
68
+ setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 0);
69
+ this.options.end(event.clientX - origin.x, event.clientY - origin.y, vx, vy);
70
+ };
71
+ this.handleCancel = (event) => {
72
+ if (event.pointerId !== this.pointer) {
73
+ return;
74
+ }
75
+ const swiped = this.swiping;
76
+ this.stop();
77
+ if (swiped) {
78
+ this.options.end(0, 0, 0, 0);
79
+ }
80
+ };
81
+ this.host = host;
82
+ this.options = options;
83
+ host.addController(this);
84
+ }
85
+ hostConnected() {
86
+ this.host.addEventListener('pointerdown', this.handleDown);
87
+ }
88
+ hostDisconnected() {
89
+ this.host.removeEventListener('pointerdown', this.handleDown);
90
+ this.stop();
91
+ }
92
+ stop() {
93
+ window.removeEventListener('pointermove', this.handleMove);
94
+ window.removeEventListener('pointerup', this.handleUp);
95
+ window.removeEventListener('pointercancel', this.handleCancel);
96
+ this.options.surface()?.removeAttribute('data-swiping');
97
+ this.origin = undefined;
98
+ this.pointer = undefined;
99
+ this.samples = [];
100
+ this.swiping = false;
101
+ }
102
+ }
103
+ const swallow = (event) => {
104
+ event.preventDefault();
105
+ event.stopPropagation();
106
+ };