@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.
- package/custom-elements.json +10266 -4161
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +10 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +6 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +89 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +102 -1
- 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 +11 -9
- package/dist/controllers/countdown-controller/countdown-controller.d.ts +55 -0
- package/dist/controllers/countdown-controller/countdown-controller.js +57 -0
- package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
- package/dist/controllers/delay-controller/delay-controller.js +54 -0
- 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 +11 -2
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +4 -1
- 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/source-controller/source-controller.d.ts +16 -0
- package/dist/controllers/source-controller/source-controller.js +44 -1
- 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 +68 -35
- package/dist/controllers/virtualize-controller/virtualize-controller.js +77 -10
- package/dist/events/events.d.ts +221 -0
- package/dist/events/events.js +150 -0
- package/dist/helix-element/helix-element.js +14 -1
- package/dist/helix-element/react-bridge.d.ts +9 -4
- package/dist/helix-element/react-bridge.js +14 -1
- package/dist/index.d.ts +31 -9
- package/dist/index.js +29 -7
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +5 -1
- 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-dismissible/mixin-dismissible.d.ts +2 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +7 -3
- package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
- package/dist/mixins/mixin-field/mixin-field.js +8 -0
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +16 -6
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +19 -1
- package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +5 -4
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +12 -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-icon/draw-icon.d.ts +7 -6
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -21
- package/dist/utils/draw-tooltip/draw-tooltip.js +11 -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/range-add/range-add.d.ts +19 -0
- package/dist/utils/range-add/range-add.js +5 -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 +6 -0
- package/dist/utils/read-series/read-series.js +1 -0
- 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 +5 -1
- package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
- package/dist/utils/roving-stop/roving-stop.js +3 -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 +15 -5
- package/dist/utils/write-time/write-time.js +7 -7
- package/dist/utils/written-list/written-list.d.ts +18 -0
- package/dist/utils/written-list/written-list.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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?:
|
|
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
|
|
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.
|
|
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
|
+
};
|