@fusion.dev/helix-core 0.0.0-dev
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/LICENSE +21 -0
- package/custom-elements.json +8005 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
- package/dist/controllers/command-controller/command-controller.d.ts +83 -0
- package/dist/controllers/command-controller/command-controller.js +93 -0
- package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
- package/dist/controllers/copy-controller/copy-controller.js +128 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
- package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
- package/dist/controllers/floating-controller/floating-controller.js +227 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
- package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
- package/dist/controllers/popover-controller/popover-controller.js +150 -0
- package/dist/controllers/source-controller/source-controller.d.ts +55 -0
- package/dist/controllers/source-controller/source-controller.js +94 -0
- package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
- package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
- package/dist/events/element-events.d.ts +4 -0
- package/dist/events/element-events.js +34 -0
- package/dist/events/events.d.ts +743 -0
- package/dist/events/events.js +799 -0
- package/dist/helix-element/helix-element.d.ts +201 -0
- package/dist/helix-element/helix-element.js +695 -0
- package/dist/helix-form-element/helix-form-element.d.ts +152 -0
- package/dist/helix-form-element/helix-form-element.js +194 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +54 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
- package/dist/mixins/mixin-collection/index.d.ts +1 -0
- package/dist/mixins/mixin-collection/index.js +1 -0
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
- package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
- package/dist/mixins/mixin-field/mixin-field.js +423 -0
- package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
- package/dist/utils/anchor-element/anchor-element.js +39 -0
- package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
- package/dist/utils/capture-pointer/capture-pointer.js +30 -0
- package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
- package/dist/utils/closest-instance/closest-instance.js +34 -0
- package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
- package/dist/utils/create-sheet/create-sheet.js +16 -0
- package/dist/utils/custom-element/custom-element.d.ts +26 -0
- package/dist/utils/custom-element/custom-element.js +114 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
- package/dist/utils/draw-icon/draw-icon.js +39 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
- package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
- package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
- package/dist/utils/elapsed-time/elapsed-time.js +117 -0
- package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
- package/dist/utils/focusable-elements/focusable-elements.js +88 -0
- package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
- package/dist/utils/format-numbers/format-numbers.js +29 -0
- package/dist/utils/host-aria/host-aria.d.ts +38 -0
- package/dist/utils/host-aria/host-aria.js +41 -0
- package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
- package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
- package/dist/utils/line-path/line-path.d.ts +23 -0
- package/dist/utils/line-path/line-path.js +107 -0
- package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
- package/dist/utils/on-unless-false/on-unless-false.js +32 -0
- package/dist/utils/read-series/read-series.d.ts +78 -0
- package/dist/utils/read-series/read-series.js +187 -0
- package/dist/utils/relative-time/relative-time.d.ts +24 -0
- package/dist/utils/relative-time/relative-time.js +84 -0
- package/dist/utils/request-change/request-change.d.ts +48 -0
- package/dist/utils/request-change/request-change.js +7 -0
- package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
- package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
- package/dist/utils/restore-value/restore-value.d.ts +32 -0
- package/dist/utils/restore-value/restore-value.js +84 -0
- package/dist/utils/scale/scale.d.ts +23 -0
- package/dist/utils/scale/scale.js +32 -0
- package/dist/utils/scale/ticks.d.ts +27 -0
- package/dist/utils/scale/ticks.js +52 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
- package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
- package/dist/utils/slot-filled/slot-filled.js +26 -0
- package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
- package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
- package/dist/utils/ticker/ticker.d.ts +8 -0
- package/dist/utils/ticker/ticker.js +55 -0
- package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
- package/dist/utils/tone-marks/tone-marks.js +36 -0
- package/dist/utils/value-as/value-as.d.ts +64 -0
- package/dist/utils/value-as/value-as.js +105 -0
- package/package.json +58 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A timestamp read the way a feed reads one: short, and a date once short stops
|
|
3
|
+
* meaning anything.
|
|
4
|
+
*
|
|
5
|
+
* **The compact form is ambiguous past a week, and that is why it stops.**
|
|
6
|
+
* `Intl.NumberFormat` with `unitDisplay: 'narrow'` is the only locale-aware way
|
|
7
|
+
* to get "42m" rather than "42 minutes ago", and measured across locales it
|
|
8
|
+
* renders both `minute` and `month` as `m` in en-US: "3m" is three minutes or
|
|
9
|
+
* three months and a reader cannot tell which. `Intl.RelativeTimeFormat` in its
|
|
10
|
+
* narrow style disambiguates the month as "3mo" and keeps an affix that cannot
|
|
11
|
+
* be removed across locales, German prefixing "vor" and Japanese suffixing 前.
|
|
12
|
+
*
|
|
13
|
+
* So the compact form covers the range where it is unambiguous, the units up to
|
|
14
|
+
* a week, and past that this returns a date. Nothing is guessed and nothing is
|
|
15
|
+
* hand-written: the short units come from `Intl.NumberFormat`, the dates from
|
|
16
|
+
* `Intl.DateTimeFormat`, and both answer to the caller's locale.
|
|
17
|
+
*
|
|
18
|
+
* Cached per locale for the reason `relativeTime` and `formatNumbers` are: the
|
|
19
|
+
* constructors are expensive, the calls are cheap, and a feed of fifty items
|
|
20
|
+
* formats fifty times per update.
|
|
21
|
+
*/
|
|
22
|
+
const held = new Map();
|
|
23
|
+
const dated = new Map();
|
|
24
|
+
/** The units the compact form covers, largest last. `week` is deliberately not
|
|
25
|
+
among them: past six days this returns a date instead, which is both shorter
|
|
26
|
+
to read and the only reading that cannot be mistaken for minutes. */
|
|
27
|
+
const UNITS = [
|
|
28
|
+
{ ms: 60_000, unit: 'minute' },
|
|
29
|
+
{ ms: 3_600_000, unit: 'hour' },
|
|
30
|
+
{ ms: 86_400_000, unit: 'day' },
|
|
31
|
+
];
|
|
32
|
+
/** Where the compact form gives out, in milliseconds. Seven days. */
|
|
33
|
+
const HORIZON = 604_800_000;
|
|
34
|
+
const compact = (locale) => {
|
|
35
|
+
let found = held.get(locale);
|
|
36
|
+
if (!found) {
|
|
37
|
+
found = UNITS.map(({ unit }) => new Intl.NumberFormat(locale || undefined, {
|
|
38
|
+
style: 'unit',
|
|
39
|
+
unit,
|
|
40
|
+
unitDisplay: 'narrow',
|
|
41
|
+
}));
|
|
42
|
+
held.set(locale, found);
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
};
|
|
46
|
+
/** Same day and month for a timestamp inside the year, month and year outside
|
|
47
|
+
it: "12 Mar" says everything while the year is understood, and "Mar 2023" is
|
|
48
|
+
what a reader wants once it is not. */
|
|
49
|
+
const calendar = (locale) => {
|
|
50
|
+
let found = dated.get(locale);
|
|
51
|
+
if (!found) {
|
|
52
|
+
found = [
|
|
53
|
+
new Intl.DateTimeFormat(locale || undefined, { day: 'numeric', month: 'short' }),
|
|
54
|
+
new Intl.DateTimeFormat(locale || undefined, { month: 'short', year: 'numeric' }),
|
|
55
|
+
];
|
|
56
|
+
dated.set(locale, found);
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A formatter for one locale, kept.
|
|
62
|
+
*
|
|
63
|
+
* `now` is a parameter rather than read from the clock, for the reason
|
|
64
|
+
* `relativeTime` gives: a function that reads `Date.now()` internally can only
|
|
65
|
+
* be tested by waiting or by replacing a global, and both produce specs that
|
|
66
|
+
* fail on a slow machine.
|
|
67
|
+
*/
|
|
68
|
+
export const elapsedTime = (locale) => {
|
|
69
|
+
const short = compact(locale);
|
|
70
|
+
const dates = calendar(locale);
|
|
71
|
+
/* "now" rather than a count of seconds, and taken from `Intl` rather than
|
|
72
|
+
written here so it is a word in the reader's language. Under a minute a feed
|
|
73
|
+
that reports 3, then 4, then 5 seconds is a feed moving under somebody
|
|
74
|
+
trying to read it, and nobody acts differently at seven seconds than four. */
|
|
75
|
+
const zero = new Intl.RelativeTimeFormat(locale || undefined, { numeric: 'auto' });
|
|
76
|
+
return (from, now) => {
|
|
77
|
+
const elapsed = now.getTime() - from.getTime();
|
|
78
|
+
if (!Number.isFinite(elapsed)) {
|
|
79
|
+
return { next: Number.POSITIVE_INFINITY, text: '' };
|
|
80
|
+
}
|
|
81
|
+
const size = Math.abs(elapsed);
|
|
82
|
+
if (size < 60_000) {
|
|
83
|
+
/* Floored like every other branch. At 59,999 milliseconds the remainder is
|
|
84
|
+
one, and a caller handed a one is re-armed inside the callback that
|
|
85
|
+
fired it. `relativeTime` shipped without this floor on the same branch
|
|
86
|
+
and its own `Relative.next` claimed to have it; only the shared ticker's
|
|
87
|
+
own `Math.max(1000, ...)` was standing between that and a busy loop. */
|
|
88
|
+
return { next: Math.max(1000, 60_000 - size), text: zero.format(0, 'second') };
|
|
89
|
+
}
|
|
90
|
+
/* A date, and it will not change again. `Infinity` is the whole point of
|
|
91
|
+
saying so: the caller schedules nothing, so a feed of old notifications
|
|
92
|
+
left open overnight wakes for none of them. */
|
|
93
|
+
if (size >= HORIZON) {
|
|
94
|
+
const sameYear = from.getFullYear() === now.getFullYear();
|
|
95
|
+
const format = sameYear ? dates[0] : dates[1];
|
|
96
|
+
return { next: Number.POSITIVE_INFINITY, text: format.format(from) };
|
|
97
|
+
}
|
|
98
|
+
let index = 0;
|
|
99
|
+
for (let at = 0; at < UNITS.length; at += 1) {
|
|
100
|
+
if (size >= UNITS[at].ms) {
|
|
101
|
+
index = at;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const chosen = UNITS[index];
|
|
105
|
+
const count = Math.trunc(size / chosen.ms);
|
|
106
|
+
/* Time until the count ticks over, which is the remainder of the current
|
|
107
|
+
unit rather than the whole of one: read at 90 seconds an item is "1m" for
|
|
108
|
+
another 30 seconds, not another 60. Capped where `relativeTime` caps it,
|
|
109
|
+
and floored at a second so nobody is handed a zero and schedules a busy
|
|
110
|
+
loop. */
|
|
111
|
+
const remaining = Math.min(chosen.ms - (size % chosen.ms), HORIZON - size);
|
|
112
|
+
return {
|
|
113
|
+
next: Math.max(1000, Math.min(remaining, 3_600_000)),
|
|
114
|
+
text: short[index].format(count),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything inside `root` the reader can reach with Tab, in document order.
|
|
3
|
+
*
|
|
4
|
+
* Separate from the focus trap because finding is the half most callers want:
|
|
5
|
+
* roving tabindex, arrow-key navigation and "focus the first thing" all need
|
|
6
|
+
* this list and none of them traps anything.
|
|
7
|
+
*
|
|
8
|
+
* Visibility is tested with `getClientRects()` rather than `offsetParent`,
|
|
9
|
+
* which is null for anything inside a `position: fixed` container. A drawer is
|
|
10
|
+
* exactly that, so the `offsetParent` test excluded every control in the thing
|
|
11
|
+
* being trapped.
|
|
12
|
+
*/
|
|
13
|
+
export declare const focusableElements: (root: HTMLElement) => HTMLElement[];
|
|
14
|
+
/**
|
|
15
|
+
* The truly focused element, following shadow roots down.
|
|
16
|
+
*
|
|
17
|
+
* `document.activeElement` stops at the outermost host, so a button inside two
|
|
18
|
+
* shadow roots reports as the component rather than as itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare const deepActiveElement: () => Element | null;
|
|
21
|
+
/**
|
|
22
|
+
* Whether `node` sits inside `root`, following shadow boundaries upward.
|
|
23
|
+
*
|
|
24
|
+
* `Node.contains` walks `parentNode` and a shadow root's `parentNode` is null,
|
|
25
|
+
* so it answers false for anything inside a component. Paired with
|
|
26
|
+
* `deepActiveElement`, which returns exactly such a node, that pairing is a trap
|
|
27
|
+
* of its own: the two look like they belong together and one of them crosses.
|
|
28
|
+
*
|
|
29
|
+
* `FocusTrapController` had precisely that, `contains(deepActiveElement())`,
|
|
30
|
+
* and it is why every dialog and drawer in the library leaked. Focus on a
|
|
31
|
+
* `helix-button` reports as the `<button>` in its shadow root, which is in no
|
|
32
|
+
* light-DOM container anywhere, so the trap read every reader as already
|
|
33
|
+
* outside it.
|
|
34
|
+
*
|
|
35
|
+
* `host` is reached without naming `ShadowRoot`, which the server has no global
|
|
36
|
+
* for. This runs from a keydown handler and would never execute there, but a
|
|
37
|
+
* bare reference is the kind that surfaces as a crash in Lit's result collector
|
|
38
|
+
* the first time something calls it early.
|
|
39
|
+
*/
|
|
40
|
+
export declare const deepContains: (root: Node, node: Node | null) => boolean;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elements the platform will focus, without naming a single component.
|
|
3
|
+
*
|
|
4
|
+
* Custom elements are covered separately below: one with `delegatesFocus` is
|
|
5
|
+
* focusable as a host, and that is readable from outside through `shadowRoot`.
|
|
6
|
+
* Listing tag names instead would mean a utility in `core` that only worked for
|
|
7
|
+
* the components `core` happens to know about.
|
|
8
|
+
*/
|
|
9
|
+
const NATIVE_FOCUSABLE = [
|
|
10
|
+
'a[href]',
|
|
11
|
+
'button:not([disabled])',
|
|
12
|
+
'input:not([disabled])',
|
|
13
|
+
'select:not([disabled])',
|
|
14
|
+
'textarea:not([disabled])',
|
|
15
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
16
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
17
|
+
].join(',');
|
|
18
|
+
/**
|
|
19
|
+
* Everything inside `root` the reader can reach with Tab, in document order.
|
|
20
|
+
*
|
|
21
|
+
* Separate from the focus trap because finding is the half most callers want:
|
|
22
|
+
* roving tabindex, arrow-key navigation and "focus the first thing" all need
|
|
23
|
+
* this list and none of them traps anything.
|
|
24
|
+
*
|
|
25
|
+
* Visibility is tested with `getClientRects()` rather than `offsetParent`,
|
|
26
|
+
* which is null for anything inside a `position: fixed` container. A drawer is
|
|
27
|
+
* exactly that, so the `offsetParent` test excluded every control in the thing
|
|
28
|
+
* being trapped.
|
|
29
|
+
*/
|
|
30
|
+
export const focusableElements = (root) =>
|
|
31
|
+
/* One pass asking both questions, rather than two passes concatenated.
|
|
32
|
+
*
|
|
33
|
+
* Two lists joined end to end is not document order, and this said "in
|
|
34
|
+
* document order" while returning every native element followed by every
|
|
35
|
+
* delegating host: measured, `b d a c` for a tree holding `a b c d`. The
|
|
36
|
+
* trap reads `items[0]` and `items.at(-1)` as its boundaries, so a surface
|
|
37
|
+
* whose last control is a plain `<button>` and whose first is a Helix
|
|
38
|
+
* component had a last that was not last, and forward Tab off the real end
|
|
39
|
+
* did not wrap. Focus left the dialog.
|
|
40
|
+
*
|
|
41
|
+
* It is invisible in a set that is all one kind, which is what every example
|
|
42
|
+
* and every spec had. A single filtered walk cannot express the fault, and
|
|
43
|
+
* dedup comes free with it: a delegating host that also matches the native
|
|
44
|
+
* selector is visited once because it is one node. */
|
|
45
|
+
[...root.querySelectorAll('*')].filter((node) => (node.matches(NATIVE_FOCUSABLE) || node.shadowRoot?.delegatesFocus === true) &&
|
|
46
|
+
node.getClientRects().length > 0);
|
|
47
|
+
/**
|
|
48
|
+
* The truly focused element, following shadow roots down.
|
|
49
|
+
*
|
|
50
|
+
* `document.activeElement` stops at the outermost host, so a button inside two
|
|
51
|
+
* shadow roots reports as the component rather than as itself.
|
|
52
|
+
*/
|
|
53
|
+
export const deepActiveElement = () => {
|
|
54
|
+
let active = document.activeElement;
|
|
55
|
+
while (active?.shadowRoot?.activeElement) {
|
|
56
|
+
active = active.shadowRoot.activeElement;
|
|
57
|
+
}
|
|
58
|
+
return active;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Whether `node` sits inside `root`, following shadow boundaries upward.
|
|
62
|
+
*
|
|
63
|
+
* `Node.contains` walks `parentNode` and a shadow root's `parentNode` is null,
|
|
64
|
+
* so it answers false for anything inside a component. Paired with
|
|
65
|
+
* `deepActiveElement`, which returns exactly such a node, that pairing is a trap
|
|
66
|
+
* of its own: the two look like they belong together and one of them crosses.
|
|
67
|
+
*
|
|
68
|
+
* `FocusTrapController` had precisely that, `contains(deepActiveElement())`,
|
|
69
|
+
* and it is why every dialog and drawer in the library leaked. Focus on a
|
|
70
|
+
* `helix-button` reports as the `<button>` in its shadow root, which is in no
|
|
71
|
+
* light-DOM container anywhere, so the trap read every reader as already
|
|
72
|
+
* outside it.
|
|
73
|
+
*
|
|
74
|
+
* `host` is reached without naming `ShadowRoot`, which the server has no global
|
|
75
|
+
* for. This runs from a keydown handler and would never execute there, but a
|
|
76
|
+
* bare reference is the kind that surfaces as a crash in Lit's result collector
|
|
77
|
+
* the first time something calls it early.
|
|
78
|
+
*/
|
|
79
|
+
export const deepContains = (root, node) => {
|
|
80
|
+
let current = node;
|
|
81
|
+
while (current) {
|
|
82
|
+
if (current === root) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
current = current.parentNode ?? current.host ?? null;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const formatNumbers: (locale: string) => ((value: number) => string);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A number formatter for a locale, built once and kept.
|
|
3
|
+
*
|
|
4
|
+
* `Intl.NumberFormat` is expensive to construct and cheap to call, and a
|
|
5
|
+
* pagination renders its count on every update, so building one per render is
|
|
6
|
+
* the whole cost of the component. Three files in one family had worked that
|
|
7
|
+
* out separately and each kept its own two-line cache, which is three places to
|
|
8
|
+
* find the day the caching rule changes and three chances for one of them to be
|
|
9
|
+
* subtly different.
|
|
10
|
+
*
|
|
11
|
+
* Keyed on the locale, so a component that never changes locale builds one
|
|
12
|
+
* formatter for its life and one that does builds a second and drops the first.
|
|
13
|
+
* Shared across components rather than per instance: a table of a hundred rows
|
|
14
|
+
* asking for the same locale gets the same object, which is what a cache is
|
|
15
|
+
* for and what a private field per element cannot do.
|
|
16
|
+
*
|
|
17
|
+
* An empty string means the runtime's own, which is what every caller was
|
|
18
|
+
* already passing `|| undefined` to say.
|
|
19
|
+
*/
|
|
20
|
+
const held = new Map();
|
|
21
|
+
export const formatNumbers = (locale) => {
|
|
22
|
+
let format = held.get(locale);
|
|
23
|
+
if (!format) {
|
|
24
|
+
format = new Intl.NumberFormat(locale || undefined);
|
|
25
|
+
held.set(locale, format);
|
|
26
|
+
}
|
|
27
|
+
const found = format;
|
|
28
|
+
return (value) => found.format(value);
|
|
29
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** What a host can say about itself. Anything omitted is left alone. */
|
|
2
|
+
export interface HostAria {
|
|
3
|
+
/** Elements describing it. Cleared with an empty array. */
|
|
4
|
+
describedBy?: Element[];
|
|
5
|
+
/** The accessible name, as a string. `''` clears it. */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Elements naming it, which beat `label` where both are set. */
|
|
8
|
+
labelledBy?: Element[];
|
|
9
|
+
/** The role, which is what the host is. `''` clears it. */
|
|
10
|
+
role?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Writes a host's semantics onto its `ElementInternals`.
|
|
14
|
+
*
|
|
15
|
+
* Onto internals rather than onto the host, which is the whole point: an
|
|
16
|
+
* attribute written at upgrade is markup a React consumer's server output never
|
|
17
|
+
* had, and hydration reports the difference. Internals carry the same meaning
|
|
18
|
+
* to the accessibility tree and leave the element as the consumer wrote it.
|
|
19
|
+
*
|
|
20
|
+
* **Empty means absent, and that is the trap this exists for.** Assigning `''`
|
|
21
|
+
* to `ariaLabel` is not the same as not naming the element: it is a name of no
|
|
22
|
+
* characters, which a screen reader announces as an unnamed control while every
|
|
23
|
+
* check for "is it labeled" passes. Every caller had written
|
|
24
|
+
* `this.label === '' ? null : this.label` and one of them would eventually not.
|
|
25
|
+
*
|
|
26
|
+
* **Element references are feature-tested rather than assumed.** They are the
|
|
27
|
+
* only way a host in the light DOM can point at a node in its own shadow root,
|
|
28
|
+
* an IDREF cannot cross that boundary, and the plain `element.aria*Elements`
|
|
29
|
+
* reflection drops the reference where `ElementInternals` keeps it. Older
|
|
30
|
+
* engines have neither, and assigning to a property they do not implement is a
|
|
31
|
+
* silent no-op rather than an error, so the guard is what makes the absence
|
|
32
|
+
* visible to anything reading this.
|
|
33
|
+
*
|
|
34
|
+
* Omitted keys are left alone, so a component that sets its role once in
|
|
35
|
+
* `willUpdate` and its references later in `updated` does not have to repeat
|
|
36
|
+
* itself or risk clearing what the other call wrote.
|
|
37
|
+
*/
|
|
38
|
+
export declare const hostAria: (internals: ElementInternals, semantics: HostAria) => void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes a host's semantics onto its `ElementInternals`.
|
|
3
|
+
*
|
|
4
|
+
* Onto internals rather than onto the host, which is the whole point: an
|
|
5
|
+
* attribute written at upgrade is markup a React consumer's server output never
|
|
6
|
+
* had, and hydration reports the difference. Internals carry the same meaning
|
|
7
|
+
* to the accessibility tree and leave the element as the consumer wrote it.
|
|
8
|
+
*
|
|
9
|
+
* **Empty means absent, and that is the trap this exists for.** Assigning `''`
|
|
10
|
+
* to `ariaLabel` is not the same as not naming the element: it is a name of no
|
|
11
|
+
* characters, which a screen reader announces as an unnamed control while every
|
|
12
|
+
* check for "is it labeled" passes. Every caller had written
|
|
13
|
+
* `this.label === '' ? null : this.label` and one of them would eventually not.
|
|
14
|
+
*
|
|
15
|
+
* **Element references are feature-tested rather than assumed.** They are the
|
|
16
|
+
* only way a host in the light DOM can point at a node in its own shadow root,
|
|
17
|
+
* an IDREF cannot cross that boundary, and the plain `element.aria*Elements`
|
|
18
|
+
* reflection drops the reference where `ElementInternals` keeps it. Older
|
|
19
|
+
* engines have neither, and assigning to a property they do not implement is a
|
|
20
|
+
* silent no-op rather than an error, so the guard is what makes the absence
|
|
21
|
+
* visible to anything reading this.
|
|
22
|
+
*
|
|
23
|
+
* Omitted keys are left alone, so a component that sets its role once in
|
|
24
|
+
* `willUpdate` and its references later in `updated` does not have to repeat
|
|
25
|
+
* itself or risk clearing what the other call wrote.
|
|
26
|
+
*/
|
|
27
|
+
export const hostAria = (internals, semantics) => {
|
|
28
|
+
if (semantics.role !== undefined) {
|
|
29
|
+
internals.role = semantics.role === '' ? null : semantics.role;
|
|
30
|
+
}
|
|
31
|
+
if (semantics.label !== undefined) {
|
|
32
|
+
internals.ariaLabel = semantics.label === '' ? null : semantics.label;
|
|
33
|
+
}
|
|
34
|
+
if (semantics.labelledBy !== undefined && 'ariaLabelledByElements' in internals) {
|
|
35
|
+
internals.ariaLabelledByElements = semantics.labelledBy.length > 0 ? semantics.labelledBy : null;
|
|
36
|
+
}
|
|
37
|
+
if (semantics.describedBy !== undefined && 'ariaDescribedByElements' in internals) {
|
|
38
|
+
internals.ariaDescribedByElements =
|
|
39
|
+
semantics.describedBy.length > 0 ? semantics.describedBy : null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The least a member must be for a group to disable it. */
|
|
2
|
+
export interface DisableableItem extends HTMLElement {
|
|
3
|
+
/** Whether the item is out of play, from either source. */
|
|
4
|
+
disabled: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Disables a group's members without forgetting what they said themselves.
|
|
8
|
+
*
|
|
9
|
+
* A group and its members are two sources for one fact: a member is out of play
|
|
10
|
+
* if either says so. Written the obvious way that is
|
|
11
|
+
* `item.disabled = group || item.disabled`, and it latches. The first pass with
|
|
12
|
+
* the group disabled writes `true` onto every member; the next pass reads its
|
|
13
|
+
* own writing back as the member's own answer; and enabling the group leaves
|
|
14
|
+
* every member disabled for good. Nothing about it is visible until something
|
|
15
|
+
* re-enables a group, which is rare enough to ship and miserable to find.
|
|
16
|
+
*
|
|
17
|
+
* So what a member said itself is recorded before it is first overridden, and
|
|
18
|
+
* given back when the group lets go. Restoring is the half that matters: a
|
|
19
|
+
* version that only recorded would read the overridden value as the member's
|
|
20
|
+
* own the moment the group was enabled, which is the same latch by a longer
|
|
21
|
+
* route.
|
|
22
|
+
*
|
|
23
|
+
* Once given back, the record is dropped, so a consumer disabling a member
|
|
24
|
+
* afterwards is its own answer again and is left alone.
|
|
25
|
+
*/
|
|
26
|
+
export declare const inheritDisabled: (disabled: boolean, items: DisableableItem[]) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Forgets what a group recorded about an item.
|
|
29
|
+
*
|
|
30
|
+
* For an item leaving a group, which would otherwise carry the group's last
|
|
31
|
+
* word into wherever it goes next.
|
|
32
|
+
*/
|
|
33
|
+
export declare const forgetDisabled: (item: Element) => void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each item declared for itself, before any group wrote to it.
|
|
3
|
+
*
|
|
4
|
+
* A `WeakMap` because the alternative is reading the attribute back, and the
|
|
5
|
+
* attribute is the thing being written: `disabled` reflects, so the second pass
|
|
6
|
+
* would read what the first pass wrote and the item would never come back.
|
|
7
|
+
*/
|
|
8
|
+
const own = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* Disables a group's members without forgetting what they said themselves.
|
|
11
|
+
*
|
|
12
|
+
* A group and its members are two sources for one fact: a member is out of play
|
|
13
|
+
* if either says so. Written the obvious way that is
|
|
14
|
+
* `item.disabled = group || item.disabled`, and it latches. The first pass with
|
|
15
|
+
* the group disabled writes `true` onto every member; the next pass reads its
|
|
16
|
+
* own writing back as the member's own answer; and enabling the group leaves
|
|
17
|
+
* every member disabled for good. Nothing about it is visible until something
|
|
18
|
+
* re-enables a group, which is rare enough to ship and miserable to find.
|
|
19
|
+
*
|
|
20
|
+
* So what a member said itself is recorded before it is first overridden, and
|
|
21
|
+
* given back when the group lets go. Restoring is the half that matters: a
|
|
22
|
+
* version that only recorded would read the overridden value as the member's
|
|
23
|
+
* own the moment the group was enabled, which is the same latch by a longer
|
|
24
|
+
* route.
|
|
25
|
+
*
|
|
26
|
+
* Once given back, the record is dropped, so a consumer disabling a member
|
|
27
|
+
* afterwards is its own answer again and is left alone.
|
|
28
|
+
*/
|
|
29
|
+
export const inheritDisabled = (disabled, items) => {
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
if (disabled) {
|
|
32
|
+
if (!own.has(item)) {
|
|
33
|
+
own.set(item, item.disabled);
|
|
34
|
+
}
|
|
35
|
+
item.disabled = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
/* Only where this held it down. An item nothing overrode is the consumer's
|
|
39
|
+
business and is not touched, which is what keeps a group that was never
|
|
40
|
+
disabled from writing to its members at all. */
|
|
41
|
+
if (own.has(item)) {
|
|
42
|
+
item.disabled = own.get(item) ?? false;
|
|
43
|
+
own.delete(item);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Forgets what a group recorded about an item.
|
|
49
|
+
*
|
|
50
|
+
* For an item leaving a group, which would otherwise carry the group's last
|
|
51
|
+
* word into wherever it goes next.
|
|
52
|
+
*/
|
|
53
|
+
export const forgetDisabled = (item) => {
|
|
54
|
+
own.delete(item);
|
|
55
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** How a line gets from one point to the next. */
|
|
2
|
+
export type Curve = 'smooth' | 'step' | 'straight';
|
|
3
|
+
/** A point in a drawing's own coordinates. */
|
|
4
|
+
export type Point = readonly [number, number];
|
|
5
|
+
/**
|
|
6
|
+
* Builds an SVG path through a set of points.
|
|
7
|
+
*
|
|
8
|
+
* Shared by every chart that draws a line, so the choice of curve is one
|
|
9
|
+
* implementation rather than one per chart. Pure: it takes points and returns a
|
|
10
|
+
* string, and knows nothing about elements or lifecycles.
|
|
11
|
+
*
|
|
12
|
+
* `straight` joins the readings and claims nothing between them, which is why
|
|
13
|
+
* it is the default. `smooth` is easier to follow across a long series and
|
|
14
|
+
* invents the values in between, so it is a reading aid rather than a neutral
|
|
15
|
+
* one. `step` claims the opposite, that a reading holds until the next one, and
|
|
16
|
+
* changes at the midpoint rather than at either end, since a chart's x axis is
|
|
17
|
+
* an order and neither neighbor has a better claim on the moment it moved.
|
|
18
|
+
*
|
|
19
|
+
* Fewer than two points is an empty string rather than a partial path: one
|
|
20
|
+
* reading is not a line, and `M0,0` alone draws nothing while still being a
|
|
21
|
+
* path element that looks like it should.
|
|
22
|
+
*/
|
|
23
|
+
export declare const linePath: (points: readonly Point[], curve?: Curve) => string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Two decimals, which is more than a 100 unit view box can show. */
|
|
2
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
3
|
+
/**
|
|
4
|
+
* Tangents that cannot overshoot, by Fritsch and Carlson.
|
|
5
|
+
*
|
|
6
|
+
* **The reason `smooth` is monotone cubic and not a Catmull-Rom spline, which
|
|
7
|
+
* is the shorter code.** A spline through the points is free to bulge past them
|
|
8
|
+
* between two of them, so a series whose lowest reading is 40 draws a curve
|
|
9
|
+
* that dips to 32 on the way there. Every value on that curve is a claim about
|
|
10
|
+
* a measurement, and the ones between the readings are the chart's own
|
|
11
|
+
* invention either way; a curve that stays inside the data at least invents
|
|
12
|
+
* nothing the data contradicts.
|
|
13
|
+
*
|
|
14
|
+
* Two rules do it. A tangent at a turning point is flat, so the curve arrives
|
|
15
|
+
* and leaves level rather than carrying on past the peak; and where a tangent
|
|
16
|
+
* is steep enough relative to its neighbours to bulge anyway, the pair is
|
|
17
|
+
* scaled back until it cannot.
|
|
18
|
+
*/
|
|
19
|
+
const tangents = (xs, ys) => {
|
|
20
|
+
const count = xs.length;
|
|
21
|
+
const secant = [];
|
|
22
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
23
|
+
secant.push((ys[index + 1] - ys[index]) /
|
|
24
|
+
(xs[index + 1] - xs[index]));
|
|
25
|
+
}
|
|
26
|
+
const slope = new Array(count);
|
|
27
|
+
slope[0] = secant[0];
|
|
28
|
+
slope[count - 1] = secant[count - 2];
|
|
29
|
+
for (let index = 1; index < count - 1; index += 1) {
|
|
30
|
+
const before = secant[index - 1];
|
|
31
|
+
const after = secant[index];
|
|
32
|
+
// Flat at a turning point, which is what keeps the curve off the outside.
|
|
33
|
+
slope[index] = before * after <= 0 ? 0 : (before + after) / 2;
|
|
34
|
+
}
|
|
35
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
36
|
+
const run = secant[index];
|
|
37
|
+
if (run === 0) {
|
|
38
|
+
slope[index] = 0;
|
|
39
|
+
slope[index + 1] = 0;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const from = slope[index] / run;
|
|
43
|
+
const to = slope[index + 1] / run;
|
|
44
|
+
const reach = Math.hypot(from, to);
|
|
45
|
+
if (reach > 3) {
|
|
46
|
+
const scale = 3 / reach;
|
|
47
|
+
slope[index] = scale * from * run;
|
|
48
|
+
slope[index + 1] = scale * to * run;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return slope;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Builds an SVG path through a set of points.
|
|
55
|
+
*
|
|
56
|
+
* Shared by every chart that draws a line, so the choice of curve is one
|
|
57
|
+
* implementation rather than one per chart. Pure: it takes points and returns a
|
|
58
|
+
* string, and knows nothing about elements or lifecycles.
|
|
59
|
+
*
|
|
60
|
+
* `straight` joins the readings and claims nothing between them, which is why
|
|
61
|
+
* it is the default. `smooth` is easier to follow across a long series and
|
|
62
|
+
* invents the values in between, so it is a reading aid rather than a neutral
|
|
63
|
+
* one. `step` claims the opposite, that a reading holds until the next one, and
|
|
64
|
+
* changes at the midpoint rather than at either end, since a chart's x axis is
|
|
65
|
+
* an order and neither neighbor has a better claim on the moment it moved.
|
|
66
|
+
*
|
|
67
|
+
* Fewer than two points is an empty string rather than a partial path: one
|
|
68
|
+
* reading is not a line, and `M0,0` alone draws nothing while still being a
|
|
69
|
+
* path element that looks like it should.
|
|
70
|
+
*/
|
|
71
|
+
export const linePath = (points, curve = 'straight') => {
|
|
72
|
+
if (points.length < 2) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
const [first] = points;
|
|
76
|
+
const move = `M${round(first[0])},${round(first[1])}`;
|
|
77
|
+
if (curve === 'step') {
|
|
78
|
+
return (move +
|
|
79
|
+
points
|
|
80
|
+
.slice(1)
|
|
81
|
+
.map(([x, y], index) => {
|
|
82
|
+
const middle = round(((points[index][0] ?? 0) + x) / 2);
|
|
83
|
+
return `H${middle}V${round(y)}H${round(x)}`;
|
|
84
|
+
})
|
|
85
|
+
.join(''));
|
|
86
|
+
}
|
|
87
|
+
if (curve === 'smooth') {
|
|
88
|
+
const xs = points.map(([x]) => x);
|
|
89
|
+
const ys = points.map(([, y]) => y);
|
|
90
|
+
const slope = tangents(xs, ys);
|
|
91
|
+
let drawing = move;
|
|
92
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
93
|
+
const reach = (xs[index + 1] - xs[index]) / 3;
|
|
94
|
+
const fromX = round(xs[index] + reach);
|
|
95
|
+
const fromY = round(ys[index] + slope[index] * reach);
|
|
96
|
+
const toX = round(xs[index + 1] - reach);
|
|
97
|
+
const toY = round(ys[index + 1] - slope[index + 1] * reach);
|
|
98
|
+
drawing += `C${fromX},${fromY} ${toX},${toY} ${round(xs[index + 1])},${round(ys[index + 1])}`;
|
|
99
|
+
}
|
|
100
|
+
return drawing;
|
|
101
|
+
}
|
|
102
|
+
return (move +
|
|
103
|
+
points
|
|
104
|
+
.slice(1)
|
|
105
|
+
.map(([x, y]) => `L${round(x)},${round(y)}`)
|
|
106
|
+
.join(''));
|
|
107
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ComplexAttributeConverter } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* For a boolean property that is on unless a consumer says otherwise.
|
|
4
|
+
*
|
|
5
|
+
* **Lit's boolean converter cannot express false, and that is the whole
|
|
6
|
+
* problem.** It answers `value !== null`, so an attribute is true by being
|
|
7
|
+
* there: `modal` is true, `modal=""` is true, and `modal="false"` is true as
|
|
8
|
+
* well, because it is present. A property defaulting to `true` therefore has no
|
|
9
|
+
* attribute that turns it off at all, and a consumer who writes the obvious
|
|
10
|
+
* thing gets the opposite of what they asked for, silently.
|
|
11
|
+
*
|
|
12
|
+
* Found on `Drawer.modal`, where it mattered: a tour cannot point inside a
|
|
13
|
+
* modal, so `modal="false"` is exactly what a consumer reaches for, and it left
|
|
14
|
+
* the drawer modal. Two others have the same shape, `Video.playsInline` and the
|
|
15
|
+
* sandbox source's `tabIndents`.
|
|
16
|
+
*
|
|
17
|
+
* So: absent is the default, present is on, and the one string that means off
|
|
18
|
+
* is the one a consumer would write. `"false"` only, and not `"0"` or `"no"`,
|
|
19
|
+
* because a converter that accepts a vocabulary invites a consumer to guess at
|
|
20
|
+
* it, and the one they guess wrong is the one that silently means on.
|
|
21
|
+
*
|
|
22
|
+
* **The other answer is to invert the property, and it is worse.** `Drawer`
|
|
23
|
+
* would take `modeless`, absent meaning modal, which satisfies the rule that a
|
|
24
|
+
* reflected property takes no default. It also renames the thing every consumer
|
|
25
|
+
* already writes, to a word nobody reaches for, so that markup can express a
|
|
26
|
+
* state that is rare. The name stays and the attribute learns to say no.
|
|
27
|
+
*/
|
|
28
|
+
export declare const onUnlessFalse: ComplexAttributeConverter<boolean>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For a boolean property that is on unless a consumer says otherwise.
|
|
3
|
+
*
|
|
4
|
+
* **Lit's boolean converter cannot express false, and that is the whole
|
|
5
|
+
* problem.** It answers `value !== null`, so an attribute is true by being
|
|
6
|
+
* there: `modal` is true, `modal=""` is true, and `modal="false"` is true as
|
|
7
|
+
* well, because it is present. A property defaulting to `true` therefore has no
|
|
8
|
+
* attribute that turns it off at all, and a consumer who writes the obvious
|
|
9
|
+
* thing gets the opposite of what they asked for, silently.
|
|
10
|
+
*
|
|
11
|
+
* Found on `Drawer.modal`, where it mattered: a tour cannot point inside a
|
|
12
|
+
* modal, so `modal="false"` is exactly what a consumer reaches for, and it left
|
|
13
|
+
* the drawer modal. Two others have the same shape, `Video.playsInline` and the
|
|
14
|
+
* sandbox source's `tabIndents`.
|
|
15
|
+
*
|
|
16
|
+
* So: absent is the default, present is on, and the one string that means off
|
|
17
|
+
* is the one a consumer would write. `"false"` only, and not `"0"` or `"no"`,
|
|
18
|
+
* because a converter that accepts a vocabulary invites a consumer to guess at
|
|
19
|
+
* it, and the one they guess wrong is the one that silently means on.
|
|
20
|
+
*
|
|
21
|
+
* **The other answer is to invert the property, and it is worse.** `Drawer`
|
|
22
|
+
* would take `modeless`, absent meaning modal, which satisfies the rule that a
|
|
23
|
+
* reflected property takes no default. It also renames the thing every consumer
|
|
24
|
+
* already writes, to a word nobody reaches for, so that markup can express a
|
|
25
|
+
* state that is rare. The name stays and the attribute learns to say no.
|
|
26
|
+
*/
|
|
27
|
+
export const onUnlessFalse = {
|
|
28
|
+
fromAttribute: (value) => value !== null && value !== 'false',
|
|
29
|
+
/* Written out so it survives being reflected later. `''` rather than
|
|
30
|
+
`'true'`, which is what a boolean attribute looks like everywhere else. */
|
|
31
|
+
toAttribute: (value) => (value ? '' : 'false'),
|
|
32
|
+
};
|