@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
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
2
|
/** What the controller needs to know: which element inside is the control. */
|
|
3
3
|
export interface AriaForwardOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether a host `aria-label` names the control too.
|
|
6
|
+
*
|
|
7
|
+
* For a control named by its content, a button or a switch, which has no
|
|
8
|
+
* `label` to render it into. A field renders its `aria-label` as a clipped
|
|
9
|
+
* label already, so it leaves this off. A function is asked on every
|
|
10
|
+
* forward, for a subclass that names the control itself.
|
|
11
|
+
*/
|
|
12
|
+
label?: boolean | (() => boolean);
|
|
13
|
+
/**
|
|
14
|
+
* Elements read after whatever names the control from outside, the host's
|
|
15
|
+
* `aria-labelledby` or its `<label>`.
|
|
16
|
+
*
|
|
17
|
+
* For a control whose name is the label and then what it shows, as a
|
|
18
|
+
* native `<select>` is named. Added only where the host gives a name, so a
|
|
19
|
+
* control named by its own template keeps its own.
|
|
20
|
+
*/
|
|
21
|
+
labelledByAfter?: () => Element[];
|
|
22
|
+
/**
|
|
23
|
+
* Whether the control has a name of its own, where its own labels do not
|
|
24
|
+
* say.
|
|
25
|
+
*
|
|
26
|
+
* A file input is labeled by the trigger that opens its picker, "Choose
|
|
27
|
+
* files", which is an action rather than a name, so a page's `<label>`
|
|
28
|
+
* should still name it. Absent, the control's own `aria-label`,
|
|
29
|
+
* `aria-labelledby` and labels with words in them decide.
|
|
30
|
+
*/
|
|
31
|
+
named?: () => boolean;
|
|
4
32
|
/** The focusable thing in the shadow root, or null before it renders. */
|
|
5
33
|
target: () => HTMLElement | null;
|
|
34
|
+
/**
|
|
35
|
+
* How to name the control, said in a warning when it has no accessible
|
|
36
|
+
* name once it has rendered. Absent, the name is not checked.
|
|
37
|
+
*
|
|
38
|
+
* For a control named by its content or a host attribute, as a button is. A
|
|
39
|
+
* field is checked by `MixinField`, which knows its label.
|
|
40
|
+
*/
|
|
41
|
+
unnamed?: string;
|
|
6
42
|
}
|
|
7
43
|
/**
|
|
8
44
|
* Carries a host's ARIA references down to the control inside it.
|
|
@@ -42,6 +78,13 @@ export interface AriaForwardOptions {
|
|
|
42
78
|
* a framework rendering a tooltip after this element upgrades, or a consumer
|
|
43
79
|
* pointing `aria-describedby` at a different id, changes an attribute and
|
|
44
80
|
* nothing schedules a Lit update.
|
|
81
|
+
*
|
|
82
|
+
* **A `<label>` that names the host names the control too**, when the control
|
|
83
|
+
* has no name of its own. The platform gives a form-associated host its labels
|
|
84
|
+
* and the host is a generic the reader never lands on, so a `<label for>` or a
|
|
85
|
+
* wrapping `<label>` left the textbox unnamed. The labels travel the same way a
|
|
86
|
+
* forwarded `aria-labelledby` does, as element references, and are read again
|
|
87
|
+
* when focus arrives, since a label can be added without the host hearing.
|
|
45
88
|
*/
|
|
46
89
|
export declare class AriaForwardController implements ReactiveController {
|
|
47
90
|
private readonly host;
|
|
@@ -51,7 +94,27 @@ export declare class AriaForwardController implements ReactiveController {
|
|
|
51
94
|
private readonly forwarded;
|
|
52
95
|
/** The control the record above is about, so a replacement resets it. */
|
|
53
96
|
private recorded;
|
|
97
|
+
/** Whether a host `aria-label` is carried down. See the option. */
|
|
98
|
+
private readonly asked;
|
|
99
|
+
private get label();
|
|
100
|
+
/** The component's own answer to whether the control is named. See the option. */
|
|
101
|
+
private readonly named;
|
|
102
|
+
/** What is read after the host's names. See the option. */
|
|
103
|
+
private readonly after;
|
|
104
|
+
/** How to name the control, where its name is checked. See the option. */
|
|
105
|
+
private readonly unnamed;
|
|
54
106
|
constructor(host: ReactiveControllerHost & HTMLElement, options: AriaForwardOptions);
|
|
107
|
+
/**
|
|
108
|
+
* Whether the control has a name that did not come from this controller: an
|
|
109
|
+
* `aria-label`, an `aria-labelledby` of its own, or a label of its own with
|
|
110
|
+
* words in it.
|
|
111
|
+
*/
|
|
112
|
+
private namedByItself;
|
|
113
|
+
/**
|
|
114
|
+
* The labels the platform associated with the host, for a control with no
|
|
115
|
+
* name of its own, and none otherwise.
|
|
116
|
+
*/
|
|
117
|
+
private hostLabels;
|
|
55
118
|
/**
|
|
56
119
|
* Resolves the host's references and hands them to the control.
|
|
57
120
|
*
|
|
@@ -65,5 +128,7 @@ export declare class AriaForwardController implements ReactiveController {
|
|
|
65
128
|
forward(): void;
|
|
66
129
|
hostConnected(): void;
|
|
67
130
|
hostDisconnected(): void;
|
|
131
|
+
/** Forwards again as focus arrives, for a label added since the last update. */
|
|
132
|
+
private readonly refresh;
|
|
68
133
|
hostUpdated(): void;
|
|
69
134
|
}
|
|
@@ -1,16 +1,63 @@
|
|
|
1
|
+
import { auditName } from '../../utils/audit-name/audit-name.js';
|
|
1
2
|
import { elementById } from '../../utils/element-by-id/element-by-id.js';
|
|
3
|
+
const shownText = (node) => {
|
|
4
|
+
if (node instanceof HTMLSlotElement) {
|
|
5
|
+
return node.assignedNodes({ flatten: true }).map(shownText).join('');
|
|
6
|
+
}
|
|
7
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
8
|
+
return node.textContent ?? '';
|
|
9
|
+
}
|
|
10
|
+
if (node instanceof HTMLElement && node.hidden) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
return [...node.childNodes].map(shownText).join('');
|
|
14
|
+
};
|
|
2
15
|
const RELATIONSHIPS = [
|
|
3
16
|
['aria-describedby', 'ariaDescribedByElements'],
|
|
4
17
|
['aria-labelledby', 'ariaLabelledByElements'],
|
|
5
18
|
];
|
|
6
|
-
const STATES = [
|
|
19
|
+
const STATES = [
|
|
20
|
+
'aria-expanded',
|
|
21
|
+
'aria-haspopup',
|
|
22
|
+
'aria-controls',
|
|
23
|
+
'aria-invalid',
|
|
24
|
+
'aria-pressed',
|
|
25
|
+
];
|
|
7
26
|
export class AriaForwardController {
|
|
27
|
+
get label() {
|
|
28
|
+
return typeof this.asked === 'function' ? this.asked() : this.asked;
|
|
29
|
+
}
|
|
8
30
|
constructor(host, options) {
|
|
9
31
|
this.forwarded = new Set();
|
|
32
|
+
this.refresh = () => {
|
|
33
|
+
this.forward();
|
|
34
|
+
};
|
|
10
35
|
this.host = host;
|
|
11
36
|
this.target = options.target;
|
|
37
|
+
this.asked = options.label ?? false;
|
|
38
|
+
this.named = options.named;
|
|
39
|
+
this.after = options.labelledByAfter;
|
|
40
|
+
this.unnamed = options.unnamed;
|
|
12
41
|
host.addController(this);
|
|
13
42
|
}
|
|
43
|
+
namedByItself(target) {
|
|
44
|
+
if (target.getAttribute('aria-label')) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (!this.forwarded.has('aria-labelledby') && target.hasAttribute('aria-labelledby')) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const own = target.labels;
|
|
51
|
+
return [...(own ?? [])].some((label) => shownText(label).trim() !== '');
|
|
52
|
+
}
|
|
53
|
+
hostLabels(target) {
|
|
54
|
+
const labels = this.host.labels;
|
|
55
|
+
const named = this.named ? this.named() : this.namedByItself(target);
|
|
56
|
+
if (!labels || labels.length === 0 || named) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return [...labels].filter((label) => label instanceof HTMLElement);
|
|
60
|
+
}
|
|
14
61
|
forward() {
|
|
15
62
|
const target = this.target();
|
|
16
63
|
if (!target) {
|
|
@@ -20,17 +67,34 @@ export class AriaForwardController {
|
|
|
20
67
|
this.forwarded.clear();
|
|
21
68
|
this.recorded = target;
|
|
22
69
|
}
|
|
70
|
+
for (const attribute of this.label ? [...STATES, 'aria-label'] : STATES) {
|
|
71
|
+
const value = this.host.getAttribute(attribute);
|
|
72
|
+
if (value === null) {
|
|
73
|
+
if (this.forwarded.has(attribute)) {
|
|
74
|
+
target.removeAttribute(attribute);
|
|
75
|
+
this.forwarded.delete(attribute);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
target.setAttribute(attribute, value);
|
|
80
|
+
this.forwarded.add(attribute);
|
|
81
|
+
}
|
|
23
82
|
for (const [attribute, property] of RELATIONSHIPS) {
|
|
24
83
|
if (!(property in target)) {
|
|
25
84
|
continue;
|
|
26
85
|
}
|
|
27
86
|
const ids = (this.host.getAttribute(attribute) ?? '').split(/\s+/).filter(Boolean);
|
|
28
|
-
const
|
|
87
|
+
const referenced = ids
|
|
29
88
|
.map((id) => elementById(this.host, id))
|
|
30
89
|
.filter((element) => element !== null);
|
|
90
|
+
const elements = referenced.length === 0 && attribute === 'aria-labelledby'
|
|
91
|
+
? this.hostLabels(target)
|
|
92
|
+
: referenced;
|
|
31
93
|
const reflect = target;
|
|
32
94
|
if (elements.length > 0) {
|
|
33
|
-
|
|
95
|
+
const after = attribute === 'aria-labelledby' ? (this.after?.() ?? []) : [];
|
|
96
|
+
const named = elements;
|
|
97
|
+
reflect[property] = [...named, ...after.filter((one) => !named.includes(one))];
|
|
34
98
|
this.forwarded.add(attribute);
|
|
35
99
|
continue;
|
|
36
100
|
}
|
|
@@ -39,30 +103,27 @@ export class AriaForwardController {
|
|
|
39
103
|
this.forwarded.delete(attribute);
|
|
40
104
|
}
|
|
41
105
|
}
|
|
42
|
-
for (const attribute of STATES) {
|
|
43
|
-
const value = this.host.getAttribute(attribute);
|
|
44
|
-
if (value === null) {
|
|
45
|
-
if (this.forwarded.has(attribute)) {
|
|
46
|
-
target.removeAttribute(attribute);
|
|
47
|
-
this.forwarded.delete(attribute);
|
|
48
|
-
}
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
target.setAttribute(attribute, value);
|
|
52
|
-
this.forwarded.add(attribute);
|
|
53
|
-
}
|
|
54
106
|
}
|
|
55
107
|
hostConnected() {
|
|
56
108
|
this.observer = new MutationObserver(() => this.forward());
|
|
57
109
|
this.observer.observe(this.host, {
|
|
58
|
-
attributeFilter: [
|
|
110
|
+
attributeFilter: [
|
|
111
|
+
...RELATIONSHIPS.map(([attribute]) => attribute),
|
|
112
|
+
...STATES,
|
|
113
|
+
...(this.asked === false ? [] : ['aria-label']),
|
|
114
|
+
],
|
|
59
115
|
});
|
|
116
|
+
this.host.addEventListener('focusin', this.refresh);
|
|
60
117
|
}
|
|
61
118
|
hostDisconnected() {
|
|
62
119
|
this.observer?.disconnect();
|
|
63
120
|
this.observer = undefined;
|
|
121
|
+
this.host.removeEventListener('focusin', this.refresh);
|
|
64
122
|
}
|
|
65
123
|
hostUpdated() {
|
|
66
124
|
this.forward();
|
|
125
|
+
if (this.unnamed !== undefined) {
|
|
126
|
+
auditName(this.host, this.target, this.unnamed);
|
|
127
|
+
}
|
|
67
128
|
}
|
|
68
129
|
}
|
|
@@ -5,6 +5,11 @@ export interface ArrivalOptions {
|
|
|
5
5
|
text: () => string;
|
|
6
6
|
/** Whether the element is arriving now, rather than with the page. */
|
|
7
7
|
arriving: () => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* How long the region keeps the words once it has said them, in
|
|
10
|
+
* milliseconds. Defaults to seven seconds.
|
|
11
|
+
*/
|
|
12
|
+
hold?: number;
|
|
8
13
|
}
|
|
9
14
|
/**
|
|
10
15
|
* The words of an element that is announced when it appears, for a region that
|
|
@@ -24,6 +29,12 @@ export interface ArrivalOptions {
|
|
|
24
29
|
* second time on the headless tier, which has no sheet to clip the region, and
|
|
25
30
|
* a served page changed its picture on hydrating.
|
|
26
31
|
*
|
|
32
|
+
* **And emptied again once it has been heard.** The words stay in the element
|
|
33
|
+
* itself, so a region that kept its copy put the message in the accessibility
|
|
34
|
+
* tree twice, and a reader moving through the page met it twice. The region
|
|
35
|
+
* keeps the copy for `hold` and then lets it go, which a polite region does not
|
|
36
|
+
* read out, removals being no news.
|
|
37
|
+
*
|
|
27
38
|
* The component renders `said` into its region and calls `say` when what it
|
|
28
39
|
* holds changes, from a `slotchange`.
|
|
29
40
|
*/
|
|
@@ -35,6 +46,7 @@ export declare class ArrivalController implements ReactiveController {
|
|
|
35
46
|
/** The words last accounted for, said or not. */
|
|
36
47
|
private heard;
|
|
37
48
|
private frame;
|
|
49
|
+
private timer;
|
|
38
50
|
constructor(host: ReactiveControllerHost, options: ArrivalOptions);
|
|
39
51
|
hostConnected(): void;
|
|
40
52
|
hostDisconnected(): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const HOLD = 7000;
|
|
1
2
|
export class ArrivalController {
|
|
2
3
|
constructor(host, options) {
|
|
3
4
|
this.host = host;
|
|
@@ -22,6 +23,9 @@ export class ArrivalController {
|
|
|
22
23
|
hostDisconnected() {
|
|
23
24
|
cancelAnimationFrame(this.frame);
|
|
24
25
|
this.frame = 0;
|
|
26
|
+
clearTimeout(this.timer);
|
|
27
|
+
this.timer = undefined;
|
|
28
|
+
this.said = '';
|
|
25
29
|
}
|
|
26
30
|
say() {
|
|
27
31
|
const next = this.options.text();
|
|
@@ -31,5 +35,11 @@ export class ArrivalController {
|
|
|
31
35
|
this.heard = next;
|
|
32
36
|
this.said = next;
|
|
33
37
|
this.host.requestUpdate();
|
|
38
|
+
clearTimeout(this.timer);
|
|
39
|
+
this.timer = setTimeout(() => {
|
|
40
|
+
this.timer = undefined;
|
|
41
|
+
this.said = '';
|
|
42
|
+
this.host.requestUpdate();
|
|
43
|
+
}, this.options.hold ?? HOLD);
|
|
34
44
|
}
|
|
35
45
|
}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* The id every chart puts on its plot, and the boundary a bubble is kept inside.
|
|
4
|
+
*
|
|
5
|
+
* **A name six templates and two files have to agree on is a name that will
|
|
6
|
+
* stop agreeing**, which is `PALETTE`'s argument and it holds harder here: the
|
|
7
|
+
* failure is silent. `within` resolves by id, so a plot renamed on one side
|
|
8
|
+
* returns null, the boundary falls back to the viewport, and the bubble goes
|
|
9
|
+
* back to hanging past the drawing with nothing reported anywhere. The plot is
|
|
10
|
+
* also what takes focus, and this controller finds it by the same id.
|
|
11
|
+
*
|
|
12
|
+
* Fixed rather than generated: it is scoped to one chart's shadow root, where
|
|
13
|
+
* it is the only one. Declared here, beside the keys that read it, and
|
|
14
|
+
* re-exported from `drawTooltip`'s module, where it was first declared.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CHART_PLOT = "helix-chart-plot";
|
|
2
17
|
/** What the controller needs of its host, which is a Lit element and a count. */
|
|
3
18
|
export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
|
|
4
19
|
/**
|
|
@@ -9,6 +24,16 @@ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
|
|
|
9
24
|
* Where the chart drew itself, which is where the plot is looked for.
|
|
10
25
|
*/
|
|
11
26
|
readonly renderRoot: DocumentFragment | HTMLElement;
|
|
27
|
+
/**
|
|
28
|
+
* The positions of the cells in a row that hold numbers, which the series
|
|
29
|
+
* keys move through. Absent, a reading is one series.
|
|
30
|
+
*/
|
|
31
|
+
readonly plottedColumns?: number[];
|
|
32
|
+
/**
|
|
33
|
+
* The palette slot for a cell, or `undefined` for a column no series was
|
|
34
|
+
* drawn from, such as a scatter's measure across.
|
|
35
|
+
*/
|
|
36
|
+
seriesOf?(cell: number): number | undefined;
|
|
12
37
|
}
|
|
13
38
|
/**
|
|
14
39
|
* Which reading a pointer is nearest, for a chart that draws no markers.
|
|
@@ -33,6 +58,21 @@ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
|
|
|
33
58
|
* lifts. The table underneath is the readable version and is what a phone
|
|
34
59
|
* should be given, so this listens for `pointermove` and ignores anything whose
|
|
35
60
|
* `pointerType` is not fine.
|
|
61
|
+
*
|
|
62
|
+
* **The keyboard moves the same reading, which is what makes it the reading
|
|
63
|
+
* and not the pointer's.** A chart's plot takes focus (`tabindex="0"` on the
|
|
64
|
+
* element carrying `CHART_PLOT`), and while it has it the arrow keys walk the
|
|
65
|
+
* readings, Home and End go to the first and the last, and the other pair of
|
|
66
|
+
* arrows walks the series within a reading. Escape puts the bubble away, as it
|
|
67
|
+
* does a tooltip. The tooltip follows because it already follows `at`, and the
|
|
68
|
+
* chart's readout, a polite live region `drawTooltip` renders, says the
|
|
69
|
+
* reading in words. It speaks only for a key: a pointer crossing a chart is a
|
|
70
|
+
* sighted reader looking, and a region speaking on every slot it crossed would
|
|
71
|
+
* talk over everything else on the page.
|
|
72
|
+
*
|
|
73
|
+
* No peer charting library ships this, which is the reason it is spelled out
|
|
74
|
+
* rather than assumed: the table beside the chart has every number, and this
|
|
75
|
+
* is for a reader who wants the picture's own order, a reading at a time.
|
|
36
76
|
*/
|
|
37
77
|
export declare class ChartPointerController implements ReactiveController {
|
|
38
78
|
private host;
|
|
@@ -40,6 +80,22 @@ export declare class ChartPointerController implements ReactiveController {
|
|
|
40
80
|
private plot;
|
|
41
81
|
/** The reading under the pointer, or -1 when the pointer is elsewhere. */
|
|
42
82
|
at: number;
|
|
83
|
+
/**
|
|
84
|
+
* How many readings stand in one column of the drawing, for a chart laid out
|
|
85
|
+
* as a grid.
|
|
86
|
+
*
|
|
87
|
+
* `1` is a row of readings, which is every chart but a heatmap. A heatmap's
|
|
88
|
+
* calendar week stands seven days to a column, so the up and down arrows move
|
|
89
|
+
* a day and the across pair moves a week, which is where the eye goes.
|
|
90
|
+
*/
|
|
91
|
+
grid: number;
|
|
92
|
+
/** Whether the reading was last moved by a key, which is when the readout speaks. */
|
|
93
|
+
keyed: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* The series the keyboard is reading within a reading, as its place among
|
|
96
|
+
* the plotted columns, or -1 while the whole reading is read.
|
|
97
|
+
*/
|
|
98
|
+
series: number;
|
|
43
99
|
/**
|
|
44
100
|
* Which axis the readings are laid out along.
|
|
45
101
|
*
|
|
@@ -60,6 +116,17 @@ export declare class ChartPointerController implements ReactiveController {
|
|
|
60
116
|
constructor(host: ChartPointerHost);
|
|
61
117
|
hostConnected(): void;
|
|
62
118
|
hostDisconnected(): void;
|
|
119
|
+
/**
|
|
120
|
+
* The cell of the reading the series keys rest on, by its place in the row,
|
|
121
|
+
* or `undefined` while the whole reading is read.
|
|
122
|
+
*
|
|
123
|
+
* The tooltip marks this cell and the readout says it alone. A place in the
|
|
124
|
+
* row and not a series depth, because a row holds columns nothing was drawn
|
|
125
|
+
* from, and the bubble lists every one of them.
|
|
126
|
+
*/
|
|
127
|
+
get column(): number | undefined;
|
|
128
|
+
/** The cells a series key can rest on: every column a series was drawn from. */
|
|
129
|
+
private columns;
|
|
63
130
|
/**
|
|
64
131
|
* Tells the tooltip its anchor moved, which nothing else would.
|
|
65
132
|
*
|
|
@@ -110,8 +177,40 @@ export declare class ChartPointerController implements ReactiveController {
|
|
|
110
177
|
* can change its direction without the chart updating.
|
|
111
178
|
*/
|
|
112
179
|
get mirrored(): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Whether a pointer event may move the reading, which only a mouse or a pen
|
|
182
|
+
* may.
|
|
183
|
+
*
|
|
184
|
+
* Public for a chart that hears the pointer itself, a donut's slices and a
|
|
185
|
+
* heatmap's cells, so the rule a coarse pointer gets is this one everywhere.
|
|
186
|
+
* Heard unfiltered, a touch opened the bubble under the finger and left it
|
|
187
|
+
* there after the finger lifted.
|
|
188
|
+
*/
|
|
189
|
+
accepts(event: Event): boolean;
|
|
113
190
|
/** Puts the pointer on a reading, for a chart that resolves it some other way. */
|
|
114
191
|
set(at: number): void;
|
|
192
|
+
/**
|
|
193
|
+
* Whether an event came from the plot itself, which is the one element in
|
|
194
|
+
* the chart the keys are for.
|
|
195
|
+
*
|
|
196
|
+
* By the id every chart gives its plot, asked of the first node on the
|
|
197
|
+
* event's path: a key pressed in a donut's middle, which a consumer can fill
|
|
198
|
+
* with a link, is that link's and not the chart's.
|
|
199
|
+
*/
|
|
200
|
+
private fromPlot;
|
|
201
|
+
/** Puts the reading away, as Escape and leaving the plot both do. */
|
|
202
|
+
private clear;
|
|
203
|
+
private readonly key;
|
|
204
|
+
/**
|
|
205
|
+
* Shows the first reading when the plot is reached from the keyboard, so the
|
|
206
|
+
* bubble and the readout say where the reader has arrived.
|
|
207
|
+
*
|
|
208
|
+
* A pointer pressing the plot focuses it too, and is left alone: the
|
|
209
|
+
* pointer is already showing a reading, and `:focus-visible` is how the
|
|
210
|
+
* platform says which of the two arrived.
|
|
211
|
+
*/
|
|
212
|
+
private readonly arrive;
|
|
213
|
+
private readonly depart;
|
|
115
214
|
private readonly move;
|
|
116
215
|
private readonly leave;
|
|
117
216
|
}
|
|
@@ -1,11 +1,76 @@
|
|
|
1
1
|
import { isRtl } from '../../utils/is-rtl/is-rtl.js';
|
|
2
|
+
import { stepReading } from './reading-step.js';
|
|
3
|
+
export const CHART_PLOT = 'helix-chart-plot';
|
|
2
4
|
export class ChartPointerController {
|
|
3
5
|
constructor(host) {
|
|
4
6
|
this.moved = false;
|
|
5
7
|
this.at = -1;
|
|
8
|
+
this.grid = 1;
|
|
9
|
+
this.keyed = false;
|
|
10
|
+
this.series = -1;
|
|
6
11
|
this.axis = 'inline';
|
|
12
|
+
this.key = (event) => {
|
|
13
|
+
if (!this.fromPlot(event) || event.altKey || event.ctrlKey || event.metaKey) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const count = this.host.readings;
|
|
17
|
+
if (count === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (event.key === 'Escape') {
|
|
21
|
+
if (this.at >= 0) {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
this.clear();
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const next = stepReading(event.key, { at: this.at, series: this.series }, {
|
|
28
|
+
axis: this.axis,
|
|
29
|
+
count,
|
|
30
|
+
grid: this.grid,
|
|
31
|
+
rtl: isRtl(this.host),
|
|
32
|
+
series: this.columns().length,
|
|
33
|
+
});
|
|
34
|
+
if (next === undefined) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { at, series } = next;
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
if (at === this.at && series === this.series && this.keyed) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.at = at;
|
|
43
|
+
this.series = series;
|
|
44
|
+
this.keyed = true;
|
|
45
|
+
this.moved = true;
|
|
46
|
+
this.host.requestUpdate();
|
|
47
|
+
};
|
|
48
|
+
this.arrive = (event) => {
|
|
49
|
+
if (!this.fromPlot(event) || this.at >= 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const plot = event.composedPath()[0];
|
|
53
|
+
let visible = true;
|
|
54
|
+
try {
|
|
55
|
+
visible = plot.matches(':focus-visible');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
}
|
|
59
|
+
if (!visible || this.host.readings === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.at = 0;
|
|
63
|
+
this.keyed = true;
|
|
64
|
+
this.moved = true;
|
|
65
|
+
this.host.requestUpdate();
|
|
66
|
+
};
|
|
67
|
+
this.depart = (event) => {
|
|
68
|
+
if (this.fromPlot(event) && this.keyed) {
|
|
69
|
+
this.clear();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
7
72
|
this.move = (event) => {
|
|
8
|
-
if (this.axis === 'none' || (event
|
|
73
|
+
if (this.axis === 'none' || !this.accepts(event)) {
|
|
9
74
|
return;
|
|
10
75
|
}
|
|
11
76
|
const box = this.box();
|
|
@@ -23,12 +88,14 @@ export class ChartPointerController {
|
|
|
23
88
|
const next = Math.min(Math.max(slot, 0), count - 1);
|
|
24
89
|
if (next !== this.at) {
|
|
25
90
|
this.at = next;
|
|
91
|
+
this.keyed = false;
|
|
92
|
+
this.series = -1;
|
|
26
93
|
this.moved = true;
|
|
27
94
|
this.host.requestUpdate();
|
|
28
95
|
}
|
|
29
96
|
};
|
|
30
97
|
this.leave = () => {
|
|
31
|
-
if (this.at !== -1) {
|
|
98
|
+
if (this.at !== -1 && !this.keyed) {
|
|
32
99
|
this.at = -1;
|
|
33
100
|
this.host.requestUpdate();
|
|
34
101
|
}
|
|
@@ -39,11 +106,29 @@ export class ChartPointerController {
|
|
|
39
106
|
hostConnected() {
|
|
40
107
|
this.host.addEventListener('pointermove', this.move);
|
|
41
108
|
this.host.addEventListener('pointerleave', this.leave);
|
|
109
|
+
this.host.addEventListener('keydown', this.key);
|
|
110
|
+
this.host.addEventListener('focusin', this.arrive);
|
|
111
|
+
this.host.addEventListener('focusout', this.depart);
|
|
42
112
|
}
|
|
43
113
|
hostDisconnected() {
|
|
44
114
|
this.host.removeEventListener('pointermove', this.move);
|
|
45
115
|
this.host.removeEventListener('pointerleave', this.leave);
|
|
116
|
+
this.host.removeEventListener('keydown', this.key);
|
|
117
|
+
this.host.removeEventListener('focusin', this.arrive);
|
|
118
|
+
this.host.removeEventListener('focusout', this.depart);
|
|
46
119
|
this.at = -1;
|
|
120
|
+
this.series = -1;
|
|
121
|
+
this.keyed = false;
|
|
122
|
+
}
|
|
123
|
+
get column() {
|
|
124
|
+
return this.series < 0 ? undefined : this.columns()[this.series];
|
|
125
|
+
}
|
|
126
|
+
columns() {
|
|
127
|
+
const host = this.host;
|
|
128
|
+
const all = host.plottedColumns ?? [];
|
|
129
|
+
return host.seriesOf === undefined
|
|
130
|
+
? all
|
|
131
|
+
: all.filter((one) => host.seriesOf?.(one) !== undefined);
|
|
47
132
|
}
|
|
48
133
|
hostUpdated() {
|
|
49
134
|
if (!this.moved) {
|
|
@@ -63,11 +148,31 @@ export class ChartPointerController {
|
|
|
63
148
|
get mirrored() {
|
|
64
149
|
return isRtl(this.host);
|
|
65
150
|
}
|
|
151
|
+
accepts(event) {
|
|
152
|
+
const type = event.pointerType;
|
|
153
|
+
return type === 'mouse' || type === 'pen';
|
|
154
|
+
}
|
|
66
155
|
set(at) {
|
|
67
156
|
if (at !== this.at) {
|
|
68
157
|
this.at = at;
|
|
158
|
+
this.keyed = false;
|
|
69
159
|
this.moved = true;
|
|
70
160
|
this.host.requestUpdate();
|
|
71
161
|
}
|
|
72
162
|
}
|
|
163
|
+
fromPlot(event) {
|
|
164
|
+
const target = event.composedPath()[0];
|
|
165
|
+
return (target instanceof Element &&
|
|
166
|
+
target.id === CHART_PLOT &&
|
|
167
|
+
target.getRootNode() === this.host.renderRoot);
|
|
168
|
+
}
|
|
169
|
+
clear() {
|
|
170
|
+
const was = this.at;
|
|
171
|
+
this.at = -1;
|
|
172
|
+
this.series = -1;
|
|
173
|
+
this.keyed = false;
|
|
174
|
+
if (was !== -1) {
|
|
175
|
+
this.host.requestUpdate();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
73
178
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a key moves the reading of a chart, as arithmetic with no element in
|
|
3
|
+
* it.
|
|
4
|
+
*
|
|
5
|
+
* `ChartPointerController` hears the key and asks this where it goes, so the
|
|
6
|
+
* map of keys to readings is one function every chart shares, and a spec can
|
|
7
|
+
* hold it without a page.
|
|
8
|
+
*/
|
|
9
|
+
/** How the readings of a chart are laid out, which decides what each arrow does. */
|
|
10
|
+
export interface ReadingLayout {
|
|
11
|
+
/**
|
|
12
|
+
* The axis the readings run along: `inline` along the line of text, `block`
|
|
13
|
+
* down the box, as a horizontal bar chart runs its categories, and `none`
|
|
14
|
+
* for a ring or a plane, whose readings are walked in the table's order.
|
|
15
|
+
*/
|
|
16
|
+
axis: 'block' | 'inline' | 'none';
|
|
17
|
+
/** How many readings there are. */
|
|
18
|
+
count: number;
|
|
19
|
+
/** How many readings stand in one column, for a grid. `1` is a row of them. */
|
|
20
|
+
grid: number;
|
|
21
|
+
/** Whether the line of text runs right to left, which turns the inline arrows. */
|
|
22
|
+
rtl: boolean;
|
|
23
|
+
/** How many series a reading holds, which the cross pair of arrows walks. */
|
|
24
|
+
series: number;
|
|
25
|
+
}
|
|
26
|
+
/** A reading and the series within it, `-1` for none and for the whole reading. */
|
|
27
|
+
export interface ReadingPlace {
|
|
28
|
+
/** The reading, or `-1` for none. */
|
|
29
|
+
at: number;
|
|
30
|
+
/** The series within it, by its place among the plotted ones, or `-1` for all of them. */
|
|
31
|
+
series: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Which way a key moves along the readings: `1`, `-1`, or `0` for a key that
|
|
35
|
+
* moves something else.
|
|
36
|
+
*
|
|
37
|
+
* The arrows along the line on an inline chart, down the box on a block one,
|
|
38
|
+
* and a whole column across on a grid. The cross pair walks the readings too
|
|
39
|
+
* wherever a reading holds one series, so no arrow is dead.
|
|
40
|
+
*/
|
|
41
|
+
export declare const alongReadings: (key: string, layout: ReadingLayout) => number;
|
|
42
|
+
/**
|
|
43
|
+
* Which way a key moves among the series of a reading: the pair of arrows
|
|
44
|
+
* that does not walk the readings, and only where a reading holds two or more.
|
|
45
|
+
* Down the bubble's list for the arrow pointing down, or along the line.
|
|
46
|
+
*/
|
|
47
|
+
export declare const amongSeries: (key: string, layout: ReadingLayout) => number;
|
|
48
|
+
/**
|
|
49
|
+
* Where a key moves the reading, or `undefined` for a key that moves nothing,
|
|
50
|
+
* which the page keeps.
|
|
51
|
+
*
|
|
52
|
+
* Home and End go to the first and the last. From no reading, an arrow moving
|
|
53
|
+
* on starts at the first and one moving back at the last, so either arrow
|
|
54
|
+
* starts somewhere a reader expects. A series key on no reading starts on the
|
|
55
|
+
* first. Nothing walks off either end.
|
|
56
|
+
*/
|
|
57
|
+
export declare const stepReading: (key: string, from: ReadingPlace, layout: ReadingLayout) => ReadingPlace | undefined;
|