@fusion.dev/helix-core 0.142.1 → 0.144.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 (112) hide show
  1. package/custom-elements.json +10436 -3804
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +65 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +77 -16
  4. package/dist/controllers/arrival-controller/arrival-controller.d.ts +12 -0
  5. package/dist/controllers/arrival-controller/arrival-controller.js +10 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +99 -0
  7. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +107 -2
  8. package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
  9. package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
  10. package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
  11. package/dist/controllers/copy-controller/copy-controller.js +16 -9
  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/dismiss-controller/dismiss-controller.js +40 -3
  15. package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
  16. package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
  17. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +79 -1
  18. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +80 -10
  19. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
  20. package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
  21. package/dist/controllers/popover-controller/popover-controller.d.ts +11 -0
  22. package/dist/controllers/popover-controller/popover-controller.js +6 -0
  23. package/dist/controllers/source-controller/source-controller.d.ts +38 -0
  24. package/dist/controllers/source-controller/source-controller.js +71 -2
  25. package/dist/controllers/status-controller/status-controller.d.ts +47 -0
  26. package/dist/controllers/status-controller/status-controller.js +34 -0
  27. package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
  28. package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
  29. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +16 -0
  30. package/dist/controllers/virtualize-controller/virtualize-controller.js +24 -2
  31. package/dist/events/events.d.ts +137 -4
  32. package/dist/events/events.js +96 -0
  33. package/dist/helix-element/react-bridge.js +6 -4
  34. package/dist/helix-form-element/helix-form-element.d.ts +131 -2
  35. package/dist/helix-form-element/helix-form-element.js +216 -6
  36. package/dist/index.d.ts +30 -9
  37. package/dist/index.js +29 -8
  38. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +15 -2
  39. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +9 -1
  40. package/dist/mixins/mixin-collection/mixin-collection.d.ts +4 -2
  41. package/dist/mixins/mixin-collection/mixin-collection.js +8 -6
  42. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
  43. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
  44. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +19 -3
  45. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
  46. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +43 -0
  47. package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
  48. package/dist/mixins/mixin-field/mixin-field.js +46 -8
  49. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +23 -8
  50. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +25 -1
  51. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +29 -5
  52. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +94 -1
  53. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
  54. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
  55. package/dist/utils/arrow/arrow.d.ts +15 -0
  56. package/dist/utils/arrow/arrow.js +2 -0
  57. package/dist/utils/audit-name/audit-name.d.ts +28 -0
  58. package/dist/utils/audit-name/audit-name.js +95 -0
  59. package/dist/utils/calendar/calendar.d.ts +117 -0
  60. package/dist/utils/calendar/calendar.js +96 -0
  61. package/dist/utils/chord/chord.d.ts +43 -0
  62. package/dist/utils/chord/chord.js +53 -0
  63. package/dist/utils/clock/clock.d.ts +87 -0
  64. package/dist/utils/clock/clock.js +68 -0
  65. package/dist/utils/color-name/color-name.d.ts +28 -0
  66. package/dist/utils/color-name/color-name.js +150 -0
  67. package/dist/utils/count-label/count-label.d.ts +13 -0
  68. package/dist/utils/count-label/count-label.js +5 -0
  69. package/dist/utils/css-color/css-color.d.ts +12 -0
  70. package/dist/utils/css-color/css-color.js +2 -2
  71. package/dist/utils/date-segments/date-segments.d.ts +122 -0
  72. package/dist/utils/date-segments/date-segments.js +206 -0
  73. package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
  74. package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
  75. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -18
  76. package/dist/utils/draw-tooltip/draw-tooltip.js +16 -5
  77. package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
  78. package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
  79. package/dist/utils/follow-link/follow-link.d.ts +46 -0
  80. package/dist/utils/follow-link/follow-link.js +37 -0
  81. package/dist/utils/heading-in/heading-in.d.ts +19 -0
  82. package/dist/utils/heading-in/heading-in.js +14 -0
  83. package/dist/utils/number-format/number-format.d.ts +23 -0
  84. package/dist/utils/number-format/number-format.js +55 -0
  85. package/dist/utils/platform/platform.d.ts +23 -0
  86. package/dist/utils/platform/platform.js +11 -0
  87. package/dist/utils/read-color/read-color.d.ts +14 -0
  88. package/dist/utils/read-color/read-color.js +13 -0
  89. package/dist/utils/read-series/read-series.d.ts +34 -0
  90. package/dist/utils/read-series/read-series.js +32 -4
  91. package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
  92. package/dist/utils/reflect-elements/reflect-elements.js +40 -0
  93. package/dist/utils/relative-time/relative-time.d.ts +13 -1
  94. package/dist/utils/relative-time/relative-time.js +7 -4
  95. package/dist/utils/report-runtime/report-runtime.d.ts +12 -0
  96. package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
  97. package/dist/utils/roving-stop/roving-stop.js +3 -0
  98. package/dist/utils/scroll-lock/scroll-lock.d.ts +16 -0
  99. package/dist/utils/scroll-lock/scroll-lock.js +30 -0
  100. package/dist/utils/swipe/swipe.d.ts +63 -0
  101. package/dist/utils/swipe/swipe.js +48 -0
  102. package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
  103. package/dist/utils/tab-stops/tab-stops.js +22 -0
  104. package/dist/utils/value-as/value-as.d.ts +20 -0
  105. package/dist/utils/value-as/value-as.js +16 -0
  106. package/dist/utils/write-time/write-time.d.ts +11 -1
  107. package/dist/utils/write-time/write-time.js +7 -7
  108. package/dist/utils/written-list/written-list.d.ts +29 -2
  109. package/dist/utils/written-list/written-list.js +11 -1
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +4 -4
@@ -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
+ }
@@ -122,6 +122,7 @@ export declare class PopoverController implements ReactiveController {
122
122
  private readonly dismisser;
123
123
  /** Whether the surface is currently shown, so the calls can be idempotent. */
124
124
  private shown;
125
+ private readonly host;
125
126
  constructor(host: ReactiveControllerHost & HTMLElement, options: PopoverOptions);
126
127
  /** Whether the surface is showing. */
127
128
  get open(): boolean;
@@ -152,4 +153,14 @@ export declare class PopoverController implements ReactiveController {
152
153
  */
153
154
  reposition(): void;
154
155
  hostDisconnected(): void;
156
+ /**
157
+ * Asks a reconnected host that is still open to update, so the panel is shown
158
+ * again.
159
+ *
160
+ * The flag above lets it show, and nothing asked it to: a host moved in the
161
+ * DOM while open kept `open` and `aria-expanded="true"` with the panel
162
+ * hidden until something else updated it, and Escape did not close it,
163
+ * because the dismissal is started by showing.
164
+ */
165
+ hostConnected(): void;
155
166
  }
@@ -4,6 +4,7 @@ export class PopoverController {
4
4
  constructor(host, options) {
5
5
  this.shown = false;
6
6
  this.options = options;
7
+ this.host = host;
7
8
  this.floating = new FloatingController(host, {
8
9
  anchor: options.anchor,
9
10
  ...(options.arrow === undefined ? {} : { arrow: options.arrow }),
@@ -85,4 +86,9 @@ export class PopoverController {
85
86
  hostDisconnected() {
86
87
  this.shown = false;
87
88
  }
89
+ hostConnected() {
90
+ if (this.options.open?.()) {
91
+ this.host.requestUpdate();
92
+ }
93
+ }
88
94
  }
@@ -26,10 +26,24 @@ export interface SourceHost extends ReactiveControllerHost, HTMLElement {
26
26
  * The element is watched for `childList` and `characterData` alike, because
27
27
  * renaming a series edits a text node in place rather than replacing a cell,
28
28
  * and nothing in a page has reason to know a chart is reading it.
29
+ *
30
+ * **The tree around it is watched as well, for the table that is not there
31
+ * yet.** A framework page renders a chart first and its table once the data
32
+ * loads, or re-keys the table and so replaces it under the same id. Watching
33
+ * only the element resolved on connect found nothing in the first case and
34
+ * kept a detached table in the second, and the chart drew neither until some
35
+ * unrelated property changed. So the root the id resolves in is watched for
36
+ * nodes arriving and leaving and for `id` and `role` changing, and each batch
37
+ * asks the id again. That is one lookup by id per batch, and a chart redraws
38
+ * only when the answer is a different element.
29
39
  */
30
40
  export declare class SourceController implements ReactiveController {
31
41
  private host;
32
42
  private observer;
43
+ /** Watches the tree `for` resolves in, for the table arriving, leaving or being replaced. */
44
+ private tree;
45
+ /** The element the watcher is on, or null where `for` named nothing. */
46
+ private watched;
33
47
  /** Whether a hydrating render was told there was nothing, and needs the render after. */
34
48
  private deferred;
35
49
  /**
@@ -86,6 +100,22 @@ export declare class SourceController implements ReactiveController {
86
100
  get rows(): Row[];
87
101
  /** A held parse where it still describes the table, or a fresh one. */
88
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;
89
119
  hostConnected(): void;
90
120
  hostUpdated(): void;
91
121
  hostDisconnected(): void;
@@ -96,4 +126,12 @@ export declare class SourceController implements ReactiveController {
96
126
  * there is no `MutationObserver`, which is the server.
97
127
  */
98
128
  watch(): void;
129
+ /**
130
+ * Watches the root `for` resolves in, so a table that arrives, leaves or is
131
+ * replaced under the same id is followed.
132
+ *
133
+ * Only while `for` names something and the host sits in a tree that can
134
+ * resolve an id, which a detached host and a server render do not.
135
+ */
136
+ private watchTree;
99
137
  }
@@ -1,7 +1,11 @@
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) {
8
+ this.watched = null;
5
9
  this.deferred = false;
6
10
  this.version = 0;
7
11
  this.host = host;
@@ -46,6 +50,43 @@ export class SourceController {
46
50
  }
47
51
  return { element, value: read(element), version: this.version };
48
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
+ }
49
90
  hostConnected() {
50
91
  this.watch();
51
92
  }
@@ -56,14 +97,25 @@ export class SourceController {
56
97
  }
57
98
  }
58
99
  hostDisconnected() {
100
+ this.enlist(null);
101
+ this.watched = null;
59
102
  this.observer?.disconnect();
60
103
  this.observer = undefined;
104
+ this.tree?.disconnect();
105
+ this.tree = undefined;
61
106
  }
62
107
  watch() {
63
108
  this.observer?.disconnect();
109
+ this.tree?.disconnect();
64
110
  this.version += 1;
65
111
  const element = this.resolve();
66
- if (!element || typeof MutationObserver === 'undefined') {
112
+ this.enlist(element);
113
+ this.watched = element;
114
+ if (typeof MutationObserver === 'undefined') {
115
+ return;
116
+ }
117
+ this.watchTree();
118
+ if (!element) {
67
119
  return;
68
120
  }
69
121
  this.observer ??= new MutationObserver(() => {
@@ -76,4 +128,21 @@ export class SourceController {
76
128
  subtree: true,
77
129
  });
78
130
  }
131
+ watchTree() {
132
+ const root = this.host.getRootNode();
133
+ if (this.host.for === '' || typeof root.getElementById !== 'function') {
134
+ return;
135
+ }
136
+ this.tree ??= new MutationObserver(() => {
137
+ if (this.resolve() !== this.watched) {
138
+ this.watch();
139
+ this.host.requestUpdate();
140
+ }
141
+ });
142
+ this.tree.observe(root, {
143
+ attributeFilter: ['id', 'role'],
144
+ childList: true,
145
+ subtree: true,
146
+ });
147
+ }
79
148
  }
@@ -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
+ };
@@ -198,6 +198,22 @@ export declare class VirtualizeController implements ReactiveController {
198
198
  * Call it from `willUpdate`, before the render that draws the grown set.
199
199
  */
200
200
  shift(by: number): void;
201
+ /**
202
+ * How tall item `index` is: what was measured, or the estimate.
203
+ *
204
+ * For a host that draws an item outside the window, the row holding focus
205
+ * say, and has to take that item's room out of a spacer.
206
+ */
207
+ sizeOf(index: number): number;
208
+ /**
209
+ * Scrolls the box the least distance that shows item `index` whole.
210
+ *
211
+ * For a host moving focus to an item outside the window, whose own element
212
+ * cannot be scrolled to because it is not drawn where it belongs. `inset` is
213
+ * how far into the box the items start, for a heading that sticks at its
214
+ * top and covers that much of every item scrolled under it.
215
+ */
216
+ reveal(index: number, inset?: number): void;
201
217
  /**
202
218
  * Drops every measurement, for a host that swapped its data.
203
219
  *
@@ -74,7 +74,7 @@ export class VirtualizeController {
74
74
  this.host.requestUpdate();
75
75
  }
76
76
  uniform(box, total, size, overscan) {
77
- const first = Math.max(0, Math.floor(box.scrollTop / size) - overscan);
77
+ const first = Math.max(0, Math.min(Math.floor(box.scrollTop / size), total) - overscan);
78
78
  const fits = Math.ceil(box.clientHeight / size) + overscan * 2;
79
79
  const last = Math.min(total, first + fits);
80
80
  return { after: Math.max(0, (total - last) * size), before: first * size, first, last };
@@ -146,6 +146,26 @@ export class VirtualizeController {
146
146
  };
147
147
  this.owed += room;
148
148
  }
149
+ sizeOf(index) {
150
+ return this.measuring() ? this.heights.at(index) : Math.max(1, this.options.estimate());
151
+ }
152
+ reveal(index, inset = 0) {
153
+ const box = this.options.container();
154
+ if (!box) {
155
+ return;
156
+ }
157
+ const total = Math.max(0, this.options.count());
158
+ const top = this.measuring()
159
+ ? this.heights.before(index, total)
160
+ : index * Math.max(1, this.options.estimate());
161
+ const bottom = top + this.sizeOf(index) + inset;
162
+ if (top < box.scrollTop) {
163
+ box.scrollTop = top;
164
+ }
165
+ else if (bottom > box.scrollTop + box.clientHeight) {
166
+ box.scrollTop = bottom - box.clientHeight;
167
+ }
168
+ }
149
169
  forget() {
150
170
  this.heights.forget();
151
171
  }
@@ -161,7 +181,9 @@ export class VirtualizeController {
161
181
  'the last one. That happens when the scrolling box has no height of its own: its ' +
162
182
  '`clientHeight` is then a function of the rows just drawn, so measuring moves what is ' +
163
183
  'being measured and the two never agree. Give the container a block size and an ' +
164
- 'overflow, which is what the styled tier does and what a headless consumer has to.',
184
+ 'overflow, which is what the styled tier does and what a headless consumer has to. ' +
185
+ 'It also happens when the rows report no height, such as a row drawn with ' +
186
+ '`display: contents`, which is a fault in the component that draws them.',
165
187
  tag: typeof host.localName === 'string' ? host.localName : undefined,
166
188
  });
167
189
  }