@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.
- package/custom-elements.json +10436 -3804
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +65 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +77 -16
- package/dist/controllers/arrival-controller/arrival-controller.d.ts +12 -0
- package/dist/controllers/arrival-controller/arrival-controller.js +10 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +99 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +107 -2
- package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
- package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
- package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
- package/dist/controllers/copy-controller/copy-controller.js +16 -9
- package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
- package/dist/controllers/delay-controller/delay-controller.js +54 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +40 -3
- package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
- package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +79 -1
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +80 -10
- package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
- package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +11 -0
- package/dist/controllers/popover-controller/popover-controller.js +6 -0
- package/dist/controllers/source-controller/source-controller.d.ts +38 -0
- package/dist/controllers/source-controller/source-controller.js +71 -2
- package/dist/controllers/status-controller/status-controller.d.ts +47 -0
- package/dist/controllers/status-controller/status-controller.js +34 -0
- package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
- package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +16 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +24 -2
- package/dist/events/events.d.ts +137 -4
- package/dist/events/events.js +96 -0
- package/dist/helix-element/react-bridge.js +6 -4
- package/dist/helix-form-element/helix-form-element.d.ts +131 -2
- package/dist/helix-form-element/helix-form-element.js +216 -6
- package/dist/index.d.ts +30 -9
- package/dist/index.js +29 -8
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +15 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +9 -1
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +4 -2
- package/dist/mixins/mixin-collection/mixin-collection.js +8 -6
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +19 -3
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +43 -0
- package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
- package/dist/mixins/mixin-field/mixin-field.js +46 -8
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +23 -8
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +25 -1
- package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +29 -5
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +94 -1
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
- package/dist/utils/arrow/arrow.d.ts +15 -0
- package/dist/utils/arrow/arrow.js +2 -0
- package/dist/utils/audit-name/audit-name.d.ts +28 -0
- package/dist/utils/audit-name/audit-name.js +95 -0
- package/dist/utils/calendar/calendar.d.ts +117 -0
- package/dist/utils/calendar/calendar.js +96 -0
- package/dist/utils/chord/chord.d.ts +43 -0
- package/dist/utils/chord/chord.js +53 -0
- package/dist/utils/clock/clock.d.ts +87 -0
- package/dist/utils/clock/clock.js +68 -0
- package/dist/utils/color-name/color-name.d.ts +28 -0
- package/dist/utils/color-name/color-name.js +150 -0
- package/dist/utils/count-label/count-label.d.ts +13 -0
- package/dist/utils/count-label/count-label.js +5 -0
- package/dist/utils/css-color/css-color.d.ts +12 -0
- package/dist/utils/css-color/css-color.js +2 -2
- package/dist/utils/date-segments/date-segments.d.ts +122 -0
- package/dist/utils/date-segments/date-segments.js +206 -0
- package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
- package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -18
- package/dist/utils/draw-tooltip/draw-tooltip.js +16 -5
- package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
- package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
- package/dist/utils/follow-link/follow-link.d.ts +46 -0
- package/dist/utils/follow-link/follow-link.js +37 -0
- package/dist/utils/heading-in/heading-in.d.ts +19 -0
- package/dist/utils/heading-in/heading-in.js +14 -0
- package/dist/utils/number-format/number-format.d.ts +23 -0
- package/dist/utils/number-format/number-format.js +55 -0
- package/dist/utils/platform/platform.d.ts +23 -0
- package/dist/utils/platform/platform.js +11 -0
- package/dist/utils/read-color/read-color.d.ts +14 -0
- package/dist/utils/read-color/read-color.js +13 -0
- package/dist/utils/read-series/read-series.d.ts +34 -0
- package/dist/utils/read-series/read-series.js +32 -4
- package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
- package/dist/utils/reflect-elements/reflect-elements.js +40 -0
- package/dist/utils/relative-time/relative-time.d.ts +13 -1
- package/dist/utils/relative-time/relative-time.js +7 -4
- package/dist/utils/report-runtime/report-runtime.d.ts +12 -0
- package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
- package/dist/utils/roving-stop/roving-stop.js +3 -0
- package/dist/utils/scroll-lock/scroll-lock.d.ts +16 -0
- package/dist/utils/scroll-lock/scroll-lock.js +30 -0
- package/dist/utils/swipe/swipe.d.ts +63 -0
- package/dist/utils/swipe/swipe.js +48 -0
- package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
- package/dist/utils/tab-stops/tab-stops.js +22 -0
- package/dist/utils/value-as/value-as.d.ts +20 -0
- package/dist/utils/value-as/value-as.js +16 -0
- package/dist/utils/write-time/write-time.d.ts +11 -1
- package/dist/utils/write-time/write-time.js +7 -7
- package/dist/utils/written-list/written-list.d.ts +29 -2
- package/dist/utils/written-list/written-list.js +11 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
|
|
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
|
}
|