@fusion.dev/helix-core 0.1.0 → 0.3.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 +4 -4
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +0 -105
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +0 -71
- package/dist/controllers/command-controller/command-controller.js +0 -62
- package/dist/controllers/copy-controller/copy-controller.js +0 -63
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +0 -40
- package/dist/controllers/dismiss-controller/dismiss-controller.js +0 -124
- package/dist/controllers/drilldown-controller/drilldown-controller.js +0 -87
- package/dist/controllers/floating-controller/floating-controller.js +0 -152
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +0 -35
- package/dist/controllers/indicator-controller/indicator-controller.js +0 -82
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +0 -46
- package/dist/controllers/popover-controller/popover-controller.js +0 -72
- package/dist/controllers/source-controller/source-controller.js +0 -38
- package/dist/controllers/token-color-controller/token-color-controller.js +0 -51
- package/dist/controllers/virtualize-controller/virtualize-controller.js +0 -128
- package/dist/events/element-events.js +0 -22
- package/dist/events/events.js +0 -386
- package/dist/helix-element/helix-element.d.ts +10 -17
- package/dist/helix-element/helix-element.js +0 -505
- package/dist/helix-form-element/helix-form-element.js +0 -140
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +0 -77
- package/dist/mixins/mixin-collection/mixin-collection.js +0 -83
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +0 -82
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +0 -78
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +0 -93
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +0 -87
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +0 -38
- package/dist/mixins/mixin-field/mixin-field.js +0 -277
- package/dist/utils/anchor-element/anchor-element.js +0 -23
- package/dist/utils/capture-pointer/capture-pointer.js +0 -18
- package/dist/utils/closest-instance/closest-instance.js +0 -20
- package/dist/utils/create-sheet/create-sheet.js +0 -14
- package/dist/utils/custom-element/custom-element.js +0 -83
- package/dist/utils/draw-icon/draw-icon.js +0 -28
- package/dist/utils/draw-tooltip/draw-tooltip.js +1 -50
- package/dist/utils/elapsed-time/elapsed-time.js +0 -51
- package/dist/utils/focusable-elements/focusable-elements.js +1 -59
- package/dist/utils/format-numbers/format-numbers.js +0 -16
- package/dist/utils/host-aria/host-aria.js +0 -26
- package/dist/utils/inherit-disabled/inherit-disabled.js +0 -36
- package/dist/utils/line-path/line-path.js +0 -36
- package/dist/utils/on-unless-false/on-unless-false.js +0 -23
- package/dist/utils/read-series/read-series.js +0 -71
- package/dist/utils/relative-time/relative-time.js +0 -43
- package/dist/utils/resolve-active-item/resolve-active-item.js +0 -20
- package/dist/utils/restore-value/restore-value.js +0 -54
- package/dist/utils/scale/scale.js +0 -19
- package/dist/utils/scale/ticks.js +0 -36
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +0 -53
- package/dist/utils/slot-filled/slot-filled.js +0 -23
- package/dist/utils/tell-disclosure/tell-disclosure.js +0 -53
- package/dist/utils/ticker/ticker.js +0 -22
- package/dist/utils/tone-marks/tone-marks.js +0 -24
- package/dist/utils/value-as/value-as.js +0 -56
- package/package.json +5 -5
|
@@ -2,43 +2,6 @@ import { nothing } from 'lit';
|
|
|
2
2
|
import { styleMap } from 'lit/directives/style-map.js';
|
|
3
3
|
import { html } from 'lit/static-html.js';
|
|
4
4
|
import { seriesIndex } from '../../mixins/mixin-chart-source/mixin-chart-source.js';
|
|
5
|
-
/**
|
|
6
|
-
* What a pointer resting on a reading shows, as `Tooltip` pointed at a mark.
|
|
7
|
-
*
|
|
8
|
-
* **A chart does not draw its own tooltip, and the top layer is why.** `Tooltip`
|
|
9
|
-
* renders `popover="manual"` through `PopoverController`, so it clears every
|
|
10
|
-
* stacking context and every `overflow` on the page. A box drawn by the chart is
|
|
11
|
-
* `position: absolute` inside the plot and is confined to the chart's own
|
|
12
|
-
* context, so it is clipped by, or layered under, whatever sits beside the
|
|
13
|
-
* chart. Escaping that means reimplementing `PopoverController`, which is
|
|
14
|
-
* `Tooltip` with extra steps and a second copy of the hard part.
|
|
15
|
-
*
|
|
16
|
-
* **`for` was already the anchor and needed nothing added.** It resolves through
|
|
17
|
-
* `getRootNode().getElementById()`, and a chart's shadow root *is* a
|
|
18
|
-
* `ShadowRoot`, so a chart names an element inside itself and `Tooltip` finds
|
|
19
|
-
* it. What the chart provides is an invisible mark at the reading's point.
|
|
20
|
-
*
|
|
21
|
-
* **An HTML mark rather than the `<rect>` or `<circle>` already there.**
|
|
22
|
-
* `FloatingController` types its anchor as `HTMLElement` and an SVG element is
|
|
23
|
-
* not one; and a slice's `<circle>` has the bounding box of the whole ring
|
|
24
|
-
* rather than of its arc, so anchoring to it would point at the ring's center
|
|
25
|
-
* for every slice.
|
|
26
|
-
*
|
|
27
|
-
* **And the arrow now follows from placement rather than from a compass.** The
|
|
28
|
-
* chart says which side it prefers and `FloatingController` moves it if that
|
|
29
|
-
* side does not fit, which is how a slice at any bearing gets a bubble that
|
|
30
|
-
* points at it. Four hard-coded sides could not express a bearing, which is what
|
|
31
|
-
* made the donut's arrow miss its own ring.
|
|
32
|
-
*
|
|
33
|
-
* **It shows the table's text and not the chart's numbers.** `$128k` is what the
|
|
34
|
-
* cell says and `128000` is what the drawing measured. Every cell of the row is
|
|
35
|
-
* here, including columns nothing was plotted from, because a note beside the
|
|
36
|
-
* figures is most of the reason to want a tooltip.
|
|
37
|
-
*
|
|
38
|
-
* **Hidden from assistive technology, on purpose.** The table is the accessible
|
|
39
|
-
* representation and the drawing is `aria-hidden`; a reader on a keyboard gets
|
|
40
|
-
* the table, which they can reach, rather than a tooltip which they cannot.
|
|
41
|
-
*/
|
|
42
5
|
export const drawTooltip = (rows, at, tag, where) => {
|
|
43
6
|
const row = rows[at];
|
|
44
7
|
if (!row || rows.length === 0) {
|
|
@@ -50,10 +13,6 @@ export const drawTooltip = (rows, at, tag, where) => {
|
|
|
50
13
|
x: `${at * slot + slot / 2}%`,
|
|
51
14
|
y: '0%',
|
|
52
15
|
};
|
|
53
|
-
/* The mark and the bubble are siblings, and the id is fixed rather than
|
|
54
|
-
generated: it is scoped to this chart's shadow root, where it is the only
|
|
55
|
-
one, so nothing can collide with it and nothing has to be threaded through
|
|
56
|
-
a render to stay in step. */
|
|
57
16
|
return html `<div
|
|
58
17
|
id="helix-chart-mark"
|
|
59
18
|
part="mark"
|
|
@@ -69,15 +28,7 @@ export const drawTooltip = (rows, at, tag, where) => {
|
|
|
69
28
|
>
|
|
70
29
|
<div aria-hidden="true" slot="content">
|
|
71
30
|
<span part="tooltip-label">${row.label}</span>
|
|
72
|
-
${row.cells.map((cell, index) =>
|
|
73
|
-
/* A palette color only where something was plotted in it.
|
|
74
|
-
*
|
|
75
|
-
* A cell with no number is a column no chart drew: a note, a status,
|
|
76
|
-
* a name. Given `data-series` anyway it takes a series color and the
|
|
77
|
-
* stylesheet draws a dot, so a row reading `Note Outage` arrived
|
|
78
|
-
* wearing series three's green while the legend beside the chart said
|
|
79
|
-
* series three was something else. */
|
|
80
|
-
html `<span
|
|
31
|
+
${row.cells.map((cell, index) => html `<span
|
|
81
32
|
data-series=${cell.value === undefined ? nothing : seriesIndex(index)}
|
|
82
33
|
part="tooltip-cell"
|
|
83
34
|
>
|
|
@@ -1,35 +1,10 @@
|
|
|
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
1
|
const held = new Map();
|
|
23
2
|
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
3
|
const UNITS = [
|
|
28
4
|
{ ms: 60_000, unit: 'minute' },
|
|
29
5
|
{ ms: 3_600_000, unit: 'hour' },
|
|
30
6
|
{ ms: 86_400_000, unit: 'day' },
|
|
31
7
|
];
|
|
32
|
-
/** Where the compact form gives out, in milliseconds. Seven days. */
|
|
33
8
|
const HORIZON = 604_800_000;
|
|
34
9
|
const compact = (locale) => {
|
|
35
10
|
let found = held.get(locale);
|
|
@@ -43,9 +18,6 @@ const compact = (locale) => {
|
|
|
43
18
|
}
|
|
44
19
|
return found;
|
|
45
20
|
};
|
|
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
21
|
const calendar = (locale) => {
|
|
50
22
|
let found = dated.get(locale);
|
|
51
23
|
if (!found) {
|
|
@@ -57,21 +29,9 @@ const calendar = (locale) => {
|
|
|
57
29
|
}
|
|
58
30
|
return found;
|
|
59
31
|
};
|
|
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
32
|
export const elapsedTime = (locale) => {
|
|
69
33
|
const short = compact(locale);
|
|
70
34
|
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
35
|
const zero = new Intl.RelativeTimeFormat(locale || undefined, { numeric: 'auto' });
|
|
76
36
|
return (from, now) => {
|
|
77
37
|
const elapsed = now.getTime() - from.getTime();
|
|
@@ -80,14 +40,8 @@ export const elapsedTime = (locale) => {
|
|
|
80
40
|
}
|
|
81
41
|
const size = Math.abs(elapsed);
|
|
82
42
|
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. */
|
|
86
43
|
return { next: Math.max(1000, 60_000 - size), text: zero.format(0, 'second') };
|
|
87
44
|
}
|
|
88
|
-
/* A date, and it will not change again. `Infinity` is the whole point of
|
|
89
|
-
saying so: the caller schedules nothing, so a feed of old notifications
|
|
90
|
-
left open overnight wakes for none of them. */
|
|
91
45
|
if (size >= HORIZON) {
|
|
92
46
|
const sameYear = from.getFullYear() === now.getFullYear();
|
|
93
47
|
const format = sameYear ? dates[0] : dates[1];
|
|
@@ -101,11 +55,6 @@ export const elapsedTime = (locale) => {
|
|
|
101
55
|
}
|
|
102
56
|
const chosen = UNITS[index];
|
|
103
57
|
const count = Math.trunc(size / chosen.ms);
|
|
104
|
-
/* Time until the count ticks over, which is the remainder of the current
|
|
105
|
-
unit rather than the whole of one: read at 90 seconds an item is "1m" for
|
|
106
|
-
another 30 seconds, not another 60. Capped where `relativeTime` caps it,
|
|
107
|
-
and floored at a second so nobody is handed a zero and schedules a busy
|
|
108
|
-
loop. */
|
|
109
58
|
const remaining = Math.min(chosen.ms - (size % chosen.ms), HORIZON - size);
|
|
110
59
|
return {
|
|
111
60
|
next: Math.max(1000, Math.min(remaining, 3_600_000)),
|
|
@@ -1,11 +1,3 @@
|
|
|
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
1
|
const NATIVE_FOCUSABLE = [
|
|
10
2
|
'a[href]',
|
|
11
3
|
'button:not([disabled])',
|
|
@@ -15,40 +7,8 @@ const NATIVE_FOCUSABLE = [
|
|
|
15
7
|
'[contenteditable]:not([contenteditable="false"])',
|
|
16
8
|
'[tabindex]:not([tabindex="-1"])',
|
|
17
9
|
].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: every native element
|
|
34
|
-
* followed by every delegating host comes out `b d a c` for a tree holding
|
|
35
|
-
* `a b c d`, measured. The trap reads `items[0]` and `items.at(-1)` as its
|
|
36
|
-
* boundaries, so a surface whose last control is a plain `<button>` and whose
|
|
37
|
-
* first is a Helix component has a last that is not last, forward Tab off the
|
|
38
|
-
* real end does not wrap, and focus leaves the dialog.
|
|
39
|
-
*
|
|
40
|
-
* It is invisible in a set that is all one kind, which is what every example
|
|
41
|
-
* and every spec had. A single filtered walk cannot express the fault, and
|
|
42
|
-
* dedup comes free with it: a delegating host that also matches the native
|
|
43
|
-
* selector is visited once because it is one node. */
|
|
44
|
-
[...root.querySelectorAll('*')].filter((node) => (node.matches(NATIVE_FOCUSABLE) || node.shadowRoot?.delegatesFocus === true) &&
|
|
10
|
+
export const focusableElements = (root) => [...root.querySelectorAll('*')].filter((node) => (node.matches(NATIVE_FOCUSABLE) || node.shadowRoot?.delegatesFocus === true) &&
|
|
45
11
|
node.getClientRects().length > 0);
|
|
46
|
-
/**
|
|
47
|
-
* The truly focused element, following shadow roots down.
|
|
48
|
-
*
|
|
49
|
-
* `document.activeElement` stops at the outermost host, so a button inside two
|
|
50
|
-
* shadow roots reports as the component rather than as itself.
|
|
51
|
-
*/
|
|
52
12
|
export const deepActiveElement = () => {
|
|
53
13
|
let active = document.activeElement;
|
|
54
14
|
while (active?.shadowRoot?.activeElement) {
|
|
@@ -56,24 +16,6 @@ export const deepActiveElement = () => {
|
|
|
56
16
|
}
|
|
57
17
|
return active;
|
|
58
18
|
};
|
|
59
|
-
/**
|
|
60
|
-
* Whether `node` sits inside `root`, following shadow boundaries upward.
|
|
61
|
-
*
|
|
62
|
-
* `Node.contains` walks `parentNode` and a shadow root's `parentNode` is null,
|
|
63
|
-
* so it answers false for anything inside a component. Paired with
|
|
64
|
-
* `deepActiveElement`, which returns exactly such a node, that pairing is a trap
|
|
65
|
-
* of its own: the two look like they belong together and one of them crosses.
|
|
66
|
-
*
|
|
67
|
-
* `contains(deepActiveElement())` is the shape to avoid: focus on a
|
|
68
|
-
* `helix-button` reports as the `<button>` in its shadow root, which is in no
|
|
69
|
-
* light-DOM container anywhere, so a trap written that way reads every reader
|
|
70
|
-
* as already outside it.
|
|
71
|
-
*
|
|
72
|
-
* `host` is reached without naming `ShadowRoot`, which the server has no global
|
|
73
|
-
* for. This runs from a keydown handler and would never execute there, but a
|
|
74
|
-
* bare reference is the kind that surfaces as a crash in Lit's result collector
|
|
75
|
-
* the first time something calls it early.
|
|
76
|
-
*/
|
|
77
19
|
export const deepContains = (root, node) => {
|
|
78
20
|
let current = node;
|
|
79
21
|
while (current) {
|
|
@@ -1,19 +1,3 @@
|
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
* Keyed on the locale, so a component that never changes locale builds one
|
|
9
|
-
* formatter for its life and one that does builds a second and drops the first.
|
|
10
|
-
* Shared across components rather than per instance: a table of a hundred rows
|
|
11
|
-
* asking for the same locale gets the same object, which is what a cache is
|
|
12
|
-
* for and what a private field per element cannot do.
|
|
13
|
-
*
|
|
14
|
-
* An empty string means the runtime's own, which is what every caller was
|
|
15
|
-
* already passing `|| undefined` to say.
|
|
16
|
-
*/
|
|
17
1
|
const held = new Map();
|
|
18
2
|
export const formatNumbers = (locale) => {
|
|
19
3
|
let format = held.get(locale);
|
|
@@ -1,29 +1,3 @@
|
|
|
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. Clearing is `null`, and this is the one
|
|
13
|
-
* place that has to remember it.
|
|
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
1
|
export const hostAria = (internals, semantics) => {
|
|
28
2
|
if (semantics.role !== undefined) {
|
|
29
3
|
internals.role = semantics.role === '' ? null : semantics.role;
|
|
@@ -1,31 +1,4 @@
|
|
|
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
1
|
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
2
|
export const inheritDisabled = (disabled, items) => {
|
|
30
3
|
for (const item of items) {
|
|
31
4
|
if (disabled) {
|
|
@@ -35,21 +8,12 @@ export const inheritDisabled = (disabled, items) => {
|
|
|
35
8
|
item.disabled = true;
|
|
36
9
|
continue;
|
|
37
10
|
}
|
|
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
11
|
if (own.has(item)) {
|
|
42
12
|
item.disabled = own.get(item) ?? false;
|
|
43
13
|
own.delete(item);
|
|
44
14
|
}
|
|
45
15
|
}
|
|
46
16
|
};
|
|
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
17
|
export const forgetDisabled = (item) => {
|
|
54
18
|
own.delete(item);
|
|
55
19
|
};
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
/** Two decimals, which is more than a 100 unit view box can show. */
|
|
2
1
|
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 neighbors to bulge anyway, the pair is
|
|
17
|
-
* scaled back until it cannot.
|
|
18
|
-
*/
|
|
19
2
|
const tangents = (xs, ys) => {
|
|
20
3
|
const count = xs.length;
|
|
21
4
|
const secant = [];
|
|
@@ -29,7 +12,6 @@ const tangents = (xs, ys) => {
|
|
|
29
12
|
for (let index = 1; index < count - 1; index += 1) {
|
|
30
13
|
const before = secant[index - 1];
|
|
31
14
|
const after = secant[index];
|
|
32
|
-
// Flat at a turning point, which is what keeps the curve off the outside.
|
|
33
15
|
slope[index] = before * after <= 0 ? 0 : (before + after) / 2;
|
|
34
16
|
}
|
|
35
17
|
for (let index = 0; index < count - 1; index += 1) {
|
|
@@ -50,24 +32,6 @@ const tangents = (xs, ys) => {
|
|
|
50
32
|
}
|
|
51
33
|
return slope;
|
|
52
34
|
};
|
|
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
35
|
export const linePath = (points, curve = 'straight') => {
|
|
72
36
|
if (points.length < 2) {
|
|
73
37
|
return '';
|
|
@@ -1,27 +1,4 @@
|
|
|
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
|
-
* So: absent is the default, present is on, and the one string that means off
|
|
12
|
-
* is the one a consumer would write. `"false"` only, and not `"0"` or `"no"`,
|
|
13
|
-
* because a converter that accepts a vocabulary invites a consumer to guess at
|
|
14
|
-
* it, and the one they guess wrong is the one that silently means on.
|
|
15
|
-
*
|
|
16
|
-
* **The other answer is to invert the property, and it is worse.** `Drawer`
|
|
17
|
-
* would take `modeless`, absent meaning modal, which satisfies the rule that a
|
|
18
|
-
* reflected property takes no default. It also renames the thing every consumer
|
|
19
|
-
* already writes, to a word nobody reaches for, so that markup can express a
|
|
20
|
-
* state that is rare. The name stays and the attribute learns to say no.
|
|
21
|
-
*/
|
|
22
1
|
export const onUnlessFalse = {
|
|
23
2
|
fromAttribute: (value) => value !== null && value !== 'false',
|
|
24
|
-
/* Written out so it survives being reflected later. `''` rather than
|
|
25
|
-
`'true'`, which is what a boolean attribute looks like everywhere else. */
|
|
26
3
|
toAttribute: (value) => (value ? '' : 'false'),
|
|
27
4
|
};
|
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
/* A grid's own rows and cells, by role and by tag.
|
|
2
|
-
*
|
|
3
|
-
* **Both, because `helix-table` sets its roles through `ElementInternals`.**
|
|
4
|
-
* That writes no attribute at all, which is the whole point of it: the
|
|
5
|
-
* semantics reach the accessibility tree and never the DOM. So a selector of
|
|
6
|
-
* `[role="row"]` matches a hand-rolled grid and matches nothing at all in this
|
|
7
|
-
* library's own table, and a selector of the tag matches ours and nothing
|
|
8
|
-
* anybody else builds. Neither alone is the answer. */
|
|
9
1
|
const ROWS = 'helix-table-row, helix-headless-table-row, [role="row"]';
|
|
10
2
|
const CELLS = 'helix-table-cell, helix-headless-table-cell, [role="cell"], [role="gridcell"], [role="rowheader"]';
|
|
11
3
|
const COLUMNS = 'helix-table-column, helix-headless-table-column, [role="columnheader"]';
|
|
12
|
-
/** Whether an element is a grid rather than a document table. */
|
|
13
4
|
const isGrid = (element) => element.getAttribute('role') === 'table' ||
|
|
14
5
|
element.getAttribute('role') === 'grid' ||
|
|
15
6
|
/^helix-(headless-)?table$/i.test(element.tagName);
|
|
16
|
-
/**
|
|
17
|
-
* Reads a grid built from roles rather than from table markup.
|
|
18
|
-
*
|
|
19
|
-
* The shape is the same question asked of different elements: a first row of
|
|
20
|
-
* column headers naming the series, a first cell per row labeling it, and
|
|
21
|
-
* numbers in the rest. `helix-table` puts its columns beside its rows rather
|
|
22
|
-
* than inside a header row, so the columns are looked for in the whole grid and
|
|
23
|
-
* the rows are read separately.
|
|
24
|
-
*
|
|
25
|
-
* **A paginated grid reads as the page it is showing.** That is worth knowing
|
|
26
|
-
* before pointing a chart at one: a table element holds every row it has, and a
|
|
27
|
-
* grid may be holding twenty of two thousand. The chart is not wrong, the
|
|
28
|
-
* question was.
|
|
29
|
-
*/
|
|
30
7
|
const readGrid = (grid) => {
|
|
31
8
|
const columns = [...grid.querySelectorAll(COLUMNS)];
|
|
32
9
|
const rows = [...grid.querySelectorAll(ROWS)];
|
|
@@ -59,36 +36,7 @@ const readGrid = (grid) => {
|
|
|
59
36
|
}),
|
|
60
37
|
}));
|
|
61
38
|
};
|
|
62
|
-
/* A column with no numbers in it is not a series.
|
|
63
|
-
*
|
|
64
|
-
* Both readers build one entry per data column and then drop the rows whose
|
|
65
|
-
* cells will not parse, so a column of notes arrives here as a series with a
|
|
66
|
-
* name and no points. Nothing draws it, so it is invisible until `Legend` names
|
|
67
|
-
* it anyway: a swatch in the palette against a column no chart plotted, and
|
|
68
|
-
* every series after it shifted a color. */
|
|
69
39
|
const plotted = (series) => series.filter((one) => one.points.length > 0);
|
|
70
|
-
/**
|
|
71
|
-
* Reads a table as series, one per data column.
|
|
72
|
-
*
|
|
73
|
-
* **The table is the chart's accessible representation, not a fallback for
|
|
74
|
-
* one.** Every chart here takes its numbers from real markup rather than from a
|
|
75
|
-
* property, so the page is readable with no script at all, sortable by anything
|
|
76
|
-
* that sorts tables, and correct in a screen reader without a parallel
|
|
77
|
-
* description that has to be kept in step by hand. A `values` array would be
|
|
78
|
-
* easier to pass and would make the accessible version the thing nobody
|
|
79
|
-
* maintains.
|
|
80
|
-
*
|
|
81
|
-
* The shape is the ordinary one: a header row naming the series, a first column
|
|
82
|
-
* of row headers, and numbers in the rest. A single-column table has one series
|
|
83
|
-
* and takes its name from the caption, since a header row of one is usually
|
|
84
|
-
* absent.
|
|
85
|
-
*
|
|
86
|
-
* `data-value` wins over the cell's text, because `$1.2k` is what a reader
|
|
87
|
-
* should see and is not a number. `Number()` rather than `parseFloat`, because
|
|
88
|
-
* the second reads `12abc` as 12: a value that is nearly a number is a mistake
|
|
89
|
-
* worth dropping rather than rounding off, and a dropped point is visible where
|
|
90
|
-
* a silently truncated one is not.
|
|
91
|
-
*/
|
|
92
40
|
export const readSeries = (table) => {
|
|
93
41
|
if (isGrid(table)) {
|
|
94
42
|
return plotted(readGrid(table));
|
|
@@ -124,25 +72,6 @@ export const readSeries = (table) => {
|
|
|
124
72
|
}),
|
|
125
73
|
})));
|
|
126
74
|
};
|
|
127
|
-
/**
|
|
128
|
-
* Reads a table as rows rather than as columns.
|
|
129
|
-
*
|
|
130
|
-
* **Everything `readSeries` throws away on purpose, a tooltip needs.** That
|
|
131
|
-
* function answers "what should I draw", so it keeps the parsed number and
|
|
132
|
-
* discards the cell's text, and it drops a column whose cells are not numbers
|
|
133
|
-
* at all. A chart drawing `128000` and a tooltip saying `128000` would be the
|
|
134
|
-
* chart overruling the table, which already decided that number reads `$128k`;
|
|
135
|
-
* and a column of notes beside the figures is exactly the extra a reader wants
|
|
136
|
-
* when their pointer is resting on a reading.
|
|
137
|
-
*
|
|
138
|
-
* So this keeps both, and keeps the columns nothing plotted. A chart of two
|
|
139
|
-
* series over a table of four columns still has four columns to show.
|
|
140
|
-
*
|
|
141
|
-
* The traversal is the same shape as `readSeries` and deliberately separate.
|
|
142
|
-
* Folding them into one pass returning both would make every caller who wants
|
|
143
|
-
* one build the other, and the two are asked for at different moments: series
|
|
144
|
-
* on render, rows only where a pointer has stopped.
|
|
145
|
-
*/
|
|
146
75
|
export const readRows = (table) => {
|
|
147
76
|
const cell = (name, element) => {
|
|
148
77
|
if (!element) {
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A timestamp read the way a person reads one, and the moment it stops being true.
|
|
3
|
-
*
|
|
4
|
-
* **The second half is the point.** "2 minutes ago" is correct for sixty seconds
|
|
5
|
-
* and wrong for the rest of the session, and a feed a user leaves open is
|
|
6
|
-
* exactly where that shows: every item drifts, quietly, and the panel ends up
|
|
7
|
-
* describing when the page loaded rather than when anything happened. So this
|
|
8
|
-
* returns the text and the milliseconds until that text changes, which is what
|
|
9
|
-
* lets a caller schedule one update instead of polling and lets a page hold one
|
|
10
|
-
* timer instead of one per item.
|
|
11
|
-
*
|
|
12
|
-
* `Intl.RelativeTimeFormat` with `numeric: 'auto'` is what produces "yesterday"
|
|
13
|
-
* rather than "1 day ago", which is what a reader would say. Cached per locale
|
|
14
|
-
* for the reason `formatNumbers` is: the constructor is expensive, the call is
|
|
15
|
-
* cheap, and a feed of fifty items formats fifty times per update.
|
|
16
|
-
*/
|
|
17
1
|
const held = new Map();
|
|
18
|
-
/** How long each unit lasts, in milliseconds, largest last. */
|
|
19
2
|
const UNITS = [
|
|
20
3
|
{ ms: 1000, unit: 'second' },
|
|
21
4
|
{ ms: 60_000, unit: 'minute' },
|
|
@@ -25,14 +8,6 @@ const UNITS = [
|
|
|
25
8
|
{ ms: 2_629_800_000, unit: 'month' },
|
|
26
9
|
{ ms: 31_557_600_000, unit: 'year' },
|
|
27
10
|
];
|
|
28
|
-
/**
|
|
29
|
-
* A formatter for one locale, kept.
|
|
30
|
-
*
|
|
31
|
-
* `now` is a parameter rather than read from the clock, which is what makes this
|
|
32
|
-
* testable at all: a function that reads `Date.now()` internally can only be
|
|
33
|
-
* tested by waiting or by replacing a global, and both produce specs that fail
|
|
34
|
-
* on a slow machine.
|
|
35
|
-
*/
|
|
36
11
|
export const relativeTime = (locale) => {
|
|
37
12
|
let format = held.get(locale);
|
|
38
13
|
if (!format) {
|
|
@@ -41,27 +16,12 @@ export const relativeTime = (locale) => {
|
|
|
41
16
|
}
|
|
42
17
|
const found = format;
|
|
43
18
|
return (from, now) => {
|
|
44
|
-
/* Signed from the timestamp's point of view, not the reader's:
|
|
45
|
-
`Intl.RelativeTimeFormat` takes a negative count for the past, so
|
|
46
|
-
`format(-2, 'minute')` is "2 minutes ago" and `format(2, 'minute')` is "in
|
|
47
|
-
2 minutes". Computed as `now - from` the sign is backwards and every past
|
|
48
|
-
item reads as the future, which is the shape of it that looks like a
|
|
49
|
-
working feed until you read one. */
|
|
50
19
|
const elapsed = from.getTime() - now.getTime();
|
|
51
20
|
if (!Number.isFinite(elapsed)) {
|
|
52
21
|
return { next: Number.POSITIVE_INFINITY, text: '' };
|
|
53
22
|
}
|
|
54
|
-
/* Under a minute is "now" rather than a count of seconds. A feed that
|
|
55
|
-
reports 3, then 4, then 5 seconds ago is a feed that moves under the
|
|
56
|
-
reader while they are trying to read it, and the precision is worth
|
|
57
|
-
nothing: nobody acts differently at seven seconds than at four. */
|
|
58
23
|
const size = Math.abs(elapsed);
|
|
59
24
|
if (size < 60_000) {
|
|
60
|
-
/* Floored, as `Relative.next` says every branch is: at 59,999
|
|
61
|
-
milliseconds the remainder is one, so a caller scheduling for itself is
|
|
62
|
-
re-armed inside the callback that fired it. The shared ticker's own
|
|
63
|
-
`Math.max(1000, ...)` hides this from the two readers that go through
|
|
64
|
-
it. */
|
|
65
25
|
return { next: Math.max(1000, 60_000 - size), text: found.format(0, 'second') };
|
|
66
26
|
}
|
|
67
27
|
let chosen = UNITS[1];
|
|
@@ -71,9 +31,6 @@ export const relativeTime = (locale) => {
|
|
|
71
31
|
}
|
|
72
32
|
}
|
|
73
33
|
const count = Math.trunc(elapsed / chosen.ms);
|
|
74
|
-
/* Time until the count ticks over, which is the remainder of the current
|
|
75
|
-
unit rather than the whole of one: an item read at 90 seconds is "1 minute
|
|
76
|
-
ago" for another 30 seconds, not another 60. */
|
|
77
34
|
const remaining = chosen.ms - (size % chosen.ms);
|
|
78
35
|
return {
|
|
79
36
|
next: Math.max(1000, Math.min(remaining, 3_600_000)),
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which item a value names, or the first that could stand in for it.
|
|
3
|
-
*
|
|
4
|
-
* `items.find(match) ?? items[0]` was written by hand in four components and it
|
|
5
|
-
* is not the obvious line it looks like. It is what makes a set whose `value`
|
|
6
|
-
* names nothing still show something, which is every set before its consumer
|
|
7
|
-
* has chosen and every set whose current item has just been removed.
|
|
8
|
-
*
|
|
9
|
-
* The fallback skips disabled items and the match does not. A consumer who
|
|
10
|
-
* names a disabled item meant it, and quietly moving to another would be the
|
|
11
|
-
* component overruling them; but falling back *onto* a disabled item is the
|
|
12
|
-
* component choosing, and it should not choose something out of play. Taking
|
|
13
|
-
* `items[0]` either way opens a set on a disabled first item and shows it as
|
|
14
|
-
* current.
|
|
15
|
-
*
|
|
16
|
-
* A plain function rather than only a method on `MixinControllerActive`,
|
|
17
|
-
* because the components that need it are split across two mixins: a set whose
|
|
18
|
-
* value is submitted takes `MixinControllerValue` and cannot also take the
|
|
19
|
-
* active one without two properties called `value`.
|
|
20
|
-
*/
|
|
21
1
|
export const resolveActiveItem = (value, items) => items.find((item) => item.value === value) ?? items.find((item) => !item.disabled);
|