@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,5 +1,10 @@
|
|
|
1
1
|
/** One point in a series: what it is called, what it is worth, and when. */
|
|
2
2
|
export interface SeriesPoint {
|
|
3
|
+
/**
|
|
4
|
+
* The row the reading is in, counting from zero, which is its place among
|
|
5
|
+
* the rows `readRows` reads and so the category it is drawn at.
|
|
6
|
+
*/
|
|
7
|
+
at: number;
|
|
3
8
|
/**
|
|
4
9
|
* The row's own label, taken from its header cell.
|
|
5
10
|
*/
|
|
@@ -50,8 +55,31 @@ export interface Series {
|
|
|
50
55
|
* the second reads `12abc` as 12: a value that is nearly a number is a mistake
|
|
51
56
|
* worth dropping rather than rounding off, and a dropped point is visible where
|
|
52
57
|
* a silently truncated one is not.
|
|
58
|
+
*
|
|
59
|
+
* **A dropped point keeps its place.** Each point carries `at`, the row it was
|
|
60
|
+
* read from, and a chart draws it at that row's category. Counted by position
|
|
61
|
+
* in `points`, a blank in March slid April's reading under March's label, and a
|
|
62
|
+
* blank in the first column took a category off the axis for every series.
|
|
63
|
+
* `byRow` lays a series out one slot per row with the blanks left empty.
|
|
53
64
|
*/
|
|
54
65
|
export declare const readSeries: (table: Element) => Series[];
|
|
66
|
+
/**
|
|
67
|
+
* A series laid out one slot per row, with nothing in a slot whose cell was
|
|
68
|
+
* blank or did not parse.
|
|
69
|
+
*
|
|
70
|
+
* This is the shape a chart drawing categories wants: slot `i` is the reading at
|
|
71
|
+
* the `i`th row, which is the label under it and the row the tooltip reads.
|
|
72
|
+
*/
|
|
73
|
+
export declare const byRow: (series: Series | undefined, rows: number) => (SeriesPoint | undefined)[];
|
|
74
|
+
/**
|
|
75
|
+
* A series cut into its unbroken runs, where a run ends at a row with no
|
|
76
|
+
* reading.
|
|
77
|
+
*
|
|
78
|
+
* A line drawn through a gap claims a value in the gap. Drawn as runs, the
|
|
79
|
+
* line stops at the last reading before a blank and starts again at the first
|
|
80
|
+
* one after it.
|
|
81
|
+
*/
|
|
82
|
+
export declare const unbroken: (series: Series) => SeriesPoint[][];
|
|
55
83
|
/** One cell of a row, as a reader sees it and as a chart measures it. */
|
|
56
84
|
export interface RowCell {
|
|
57
85
|
/**
|
|
@@ -102,3 +130,9 @@ export interface Row {
|
|
|
102
130
|
* on render, rows only where a pointer has stopped.
|
|
103
131
|
*/
|
|
104
132
|
export declare const readRows: (table: Element) => Row[];
|
|
133
|
+
/**
|
|
134
|
+
* A series name as it is matched: trimmed and in lower case, since a column's
|
|
135
|
+
* heading is matched without regard to case or surrounding space everywhere a
|
|
136
|
+
* name is written, in `lines`, `hidden-series` and `hide()` alike.
|
|
137
|
+
*/
|
|
138
|
+
export declare const seriesKey: (name: string) => string;
|
|
@@ -6,13 +6,13 @@ const isGrid = (element) => element.getAttribute('role') === 'table' ||
|
|
|
6
6
|
/^helix-(headless-)?table$/i.test(element.tagName);
|
|
7
7
|
const readGrid = (grid) => {
|
|
8
8
|
const columns = [...grid.querySelectorAll(COLUMNS)];
|
|
9
|
-
const rows =
|
|
9
|
+
const rows = gridRows(grid);
|
|
10
10
|
const names = columns.slice(1).map((cell) => cell.textContent?.trim() ?? '');
|
|
11
11
|
const cellsOf = (row) => [...row.querySelectorAll(CELLS)];
|
|
12
12
|
const count = Math.max(0, ...rows.map((row) => cellsOf(row).length - 1));
|
|
13
13
|
return Array.from({ length: count }, (_, column) => ({
|
|
14
14
|
name: names[column] ?? grid.getAttribute('label') ?? '',
|
|
15
|
-
points: rows.flatMap((row) => {
|
|
15
|
+
points: rows.flatMap((row, at) => {
|
|
16
16
|
const cells = cellsOf(row);
|
|
17
17
|
const head = cells[0];
|
|
18
18
|
const cell = cells[column + 1];
|
|
@@ -26,6 +26,7 @@ const readGrid = (grid) => {
|
|
|
26
26
|
return Number.isFinite(value) && raw.trim() !== ''
|
|
27
27
|
? [
|
|
28
28
|
{
|
|
29
|
+
at,
|
|
29
30
|
label: head?.textContent?.trim() ?? '',
|
|
30
31
|
value,
|
|
31
32
|
...(tone ? { tone } : {}),
|
|
@@ -36,6 +37,7 @@ const readGrid = (grid) => {
|
|
|
36
37
|
}),
|
|
37
38
|
}));
|
|
38
39
|
};
|
|
40
|
+
const gridRows = (grid) => [...grid.querySelectorAll(ROWS)].filter((row) => row.querySelector(CELLS) !== null);
|
|
39
41
|
const plotted = (series) => series.filter((one) => one.points.length > 0);
|
|
40
42
|
export const readSeries = (table) => {
|
|
41
43
|
if (isGrid(table)) {
|
|
@@ -49,7 +51,7 @@ export const readSeries = (table) => {
|
|
|
49
51
|
const columns = Math.max(0, ...rows.map((row) => row.cells.length - 1));
|
|
50
52
|
return plotted(Array.from({ length: columns }, (_, column) => ({
|
|
51
53
|
name: names[column] ?? element.caption?.textContent?.trim() ?? '',
|
|
52
|
-
points: rows.flatMap((row) => {
|
|
54
|
+
points: rows.flatMap((row, at) => {
|
|
53
55
|
const head = row.cells[0];
|
|
54
56
|
const cell = row.cells[column + 1];
|
|
55
57
|
if (!cell) {
|
|
@@ -62,6 +64,7 @@ export const readSeries = (table) => {
|
|
|
62
64
|
return Number.isFinite(value) && raw.trim() !== ''
|
|
63
65
|
? [
|
|
64
66
|
{
|
|
67
|
+
at,
|
|
65
68
|
label: head?.textContent?.trim() ?? '',
|
|
66
69
|
value,
|
|
67
70
|
...(tone ? { tone } : {}),
|
|
@@ -72,6 +75,30 @@ export const readSeries = (table) => {
|
|
|
72
75
|
}),
|
|
73
76
|
})));
|
|
74
77
|
};
|
|
78
|
+
export const byRow = (series, rows) => {
|
|
79
|
+
const slots = Array.from({ length: rows }, () => undefined);
|
|
80
|
+
for (const point of series?.points ?? []) {
|
|
81
|
+
if (point.at >= 0 && point.at < rows) {
|
|
82
|
+
slots[point.at] = point;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return slots;
|
|
86
|
+
};
|
|
87
|
+
export const unbroken = (series) => {
|
|
88
|
+
const runs = [];
|
|
89
|
+
let previous;
|
|
90
|
+
for (const point of series.points) {
|
|
91
|
+
const run = runs.at(-1);
|
|
92
|
+
if (run && previous && point.at === previous.at + 1) {
|
|
93
|
+
run.push(point);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
runs.push([point]);
|
|
97
|
+
}
|
|
98
|
+
previous = point;
|
|
99
|
+
}
|
|
100
|
+
return runs;
|
|
101
|
+
};
|
|
75
102
|
export const readRows = (table) => {
|
|
76
103
|
const cell = (name, element) => {
|
|
77
104
|
if (!element) {
|
|
@@ -100,7 +127,7 @@ export const readRows = (table) => {
|
|
|
100
127
|
const names = [...table.querySelectorAll(COLUMNS)]
|
|
101
128
|
.slice(1)
|
|
102
129
|
.map((one) => one.textContent?.trim() ?? '');
|
|
103
|
-
return
|
|
130
|
+
return gridRows(table).map((one) => {
|
|
104
131
|
const cells = [...one.querySelectorAll(CELLS)];
|
|
105
132
|
return row(cells[0], cells.slice(1), names);
|
|
106
133
|
});
|
|
@@ -111,3 +138,4 @@ export const readRows = (table) => {
|
|
|
111
138
|
.map((one) => one.textContent?.trim() ?? '');
|
|
112
139
|
return [...(element.tBodies[0]?.rows ?? [])].map((one) => row(one.cells[0], [...one.cells].slice(1), names));
|
|
113
140
|
};
|
|
141
|
+
export const seriesKey = (name) => name.trim().toLowerCase();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The ARIA properties that take elements rather than ids. */
|
|
2
|
+
export type ElementsProperty = 'ariaDescribedByElements' | 'ariaLabelledByElements';
|
|
3
|
+
/**
|
|
4
|
+
* Writes elements onto an ARIA element-reflection property of `target`, or
|
|
5
|
+
* clears it, and says whether the property is now this caller's.
|
|
6
|
+
*
|
|
7
|
+
* Element references, because the words are in the light DOM and the panel in
|
|
8
|
+
* a shadow root, which an IDREF cannot cross. Nothing is written where the
|
|
9
|
+
* engine has no reflection, which keeps the fallback name, and the answer is
|
|
10
|
+
* then `owned` as it was. **An empty list clears the property only where the
|
|
11
|
+
* caller wrote it**, which `owned` says, so a name another hand wrote stays.
|
|
12
|
+
*
|
|
13
|
+
* Dialog and Drawer name their panel by its header this way, Popover by its
|
|
14
|
+
* first heading, Dialog describes itself by its body, and a Tour stop names
|
|
15
|
+
* and describes its panel.
|
|
16
|
+
*/
|
|
17
|
+
export declare const reflectElements: (target: Element | null | undefined, property: ElementsProperty, elements: readonly Element[], owned: boolean) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Adds `by` to the elements that describe `target`, where that is safe, and
|
|
20
|
+
* says whether `target` is now described by it.
|
|
21
|
+
*
|
|
22
|
+
* Through element reflection, and only where no framework is watching:
|
|
23
|
+
*
|
|
24
|
+
* - **Inside a shadow root**, such as the `<button>` a Helix button draws, it
|
|
25
|
+
* is always safe. That markup is the component's own, and no server sent it.
|
|
26
|
+
* - **In the light DOM**, the element is the consumer's, and reflection writes
|
|
27
|
+
* an empty `aria-describedby` onto it. So it is written only when `light` is
|
|
28
|
+
* set, which a caller does once a reader has acted on the page, after any
|
|
29
|
+
* hydration, and never where the consumer wrote `aria-describedby` or
|
|
30
|
+
* `aria-labelledby` themselves, which are theirs to keep.
|
|
31
|
+
*
|
|
32
|
+
* Existing references are kept, so a field's hint still describes it. Tooltip
|
|
33
|
+
* describes its trigger this way.
|
|
34
|
+
*/
|
|
35
|
+
export declare const linkDescription: (target: Element, by: Element, light: boolean) => boolean;
|
|
36
|
+
/** Takes `by` back off the elements that describe `target`, leaving the rest. */
|
|
37
|
+
export declare const unlinkDescription: (target: Element, by: Element) => void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const reflectElements = (target, property, elements, owned) => {
|
|
2
|
+
const element = target;
|
|
3
|
+
if (!element || !(property in element)) {
|
|
4
|
+
return owned;
|
|
5
|
+
}
|
|
6
|
+
if (elements.length > 0) {
|
|
7
|
+
element[property] = elements;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (owned) {
|
|
11
|
+
element[property] = null;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
export const linkDescription = (target, by, light) => {
|
|
16
|
+
const element = target;
|
|
17
|
+
if (!('ariaDescribedByElements' in element)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const current = element.ariaDescribedByElements ?? [];
|
|
21
|
+
if (current.includes(by)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const shadowed = typeof ShadowRoot !== 'undefined' && target.getRootNode() instanceof ShadowRoot;
|
|
25
|
+
if (!shadowed &&
|
|
26
|
+
(!light || target.hasAttribute('aria-describedby') || target.hasAttribute('aria-labelledby'))) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
element.ariaDescribedByElements = [...current, by];
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
export const unlinkDescription = (target, by) => {
|
|
33
|
+
const element = target;
|
|
34
|
+
const current = element.ariaDescribedByElements;
|
|
35
|
+
if (!current?.includes(by)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const rest = current.filter((one) => one !== by);
|
|
39
|
+
element.ariaDescribedByElements = rest.length > 0 ? rest : null;
|
|
40
|
+
};
|
|
@@ -13,6 +13,15 @@ export interface Relative {
|
|
|
13
13
|
/** The reading, in the caller's locale. */
|
|
14
14
|
text: string;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* How a reading is written: `style` is `Intl.RelativeTimeFormat`'s, from
|
|
18
|
+
* "3 hours ago" at `long` to "3h ago" at `narrow` in English, and `numeric`
|
|
19
|
+
* says whether a day back is "yesterday" (`auto`) or "1 day ago" (`always`).
|
|
20
|
+
*/
|
|
21
|
+
export interface RelativeOptions {
|
|
22
|
+
numeric?: 'always' | 'auto';
|
|
23
|
+
style?: 'long' | 'narrow' | 'short';
|
|
24
|
+
}
|
|
16
25
|
/**
|
|
17
26
|
* A formatter for one locale, kept.
|
|
18
27
|
*
|
|
@@ -20,5 +29,8 @@ export interface Relative {
|
|
|
20
29
|
* testable at all: a function that reads `Date.now()` internally can only be
|
|
21
30
|
* tested by waiting or by replacing a global, and both produce specs that fail
|
|
22
31
|
* on a slow machine.
|
|
32
|
+
*
|
|
33
|
+
* Long and `auto` unless `options` says otherwise, which is what a person
|
|
34
|
+
* says aloud and what every caller wanted before Format Date offered the rest.
|
|
23
35
|
*/
|
|
24
|
-
export declare const relativeTime: (locale: string) => ((from: Date, now: Date) => Relative);
|
|
36
|
+
export declare const relativeTime: (locale: string, options?: RelativeOptions) => ((from: Date, now: Date) => Relative);
|
|
@@ -8,11 +8,14 @@ const UNITS = [
|
|
|
8
8
|
{ ms: 2_629_800_000, unit: 'month' },
|
|
9
9
|
{ ms: 31_557_600_000, unit: 'year' },
|
|
10
10
|
];
|
|
11
|
-
export const relativeTime = (locale) => {
|
|
12
|
-
|
|
11
|
+
export const relativeTime = (locale, options = {}) => {
|
|
12
|
+
const numeric = options.numeric ?? 'auto';
|
|
13
|
+
const style = options.style ?? 'long';
|
|
14
|
+
const key = `${locale}|${numeric}|${style}`;
|
|
15
|
+
let format = held.get(key);
|
|
13
16
|
if (!format) {
|
|
14
|
-
format = new Intl.RelativeTimeFormat(locale || undefined, { numeric
|
|
15
|
-
held.set(
|
|
17
|
+
format = new Intl.RelativeTimeFormat(locale || undefined, { numeric, style });
|
|
18
|
+
held.set(key, format);
|
|
16
19
|
}
|
|
17
20
|
const found = format;
|
|
18
21
|
return (from, now) => {
|
|
@@ -26,14 +26,20 @@ export type RuntimeKind =
|
|
|
26
26
|
| 'max-past-fold'
|
|
27
27
|
/** A rating's `min` is no score between 0 and its `max`. */
|
|
28
28
|
| 'min-out-of-range'
|
|
29
|
+
/** A Stat was given one of Trend's directions, `up`, `down` or `neutral`, which lay nothing out. */
|
|
30
|
+
| 'misread-direction'
|
|
29
31
|
/** A React handler prop names an event the element never dispatches itself. */
|
|
30
32
|
| 'misplaced-handler'
|
|
31
33
|
/** The three.js driver could not load its layout, so the notes stay where they started. */
|
|
32
34
|
| 'missing-layout'
|
|
35
|
+
/** A control has no accessible name, so a screen reader announces it by its role alone. */
|
|
36
|
+
| 'missing-name'
|
|
33
37
|
/** A selectable Data Table has no `row-key`, so it knows each row by its place. */
|
|
34
38
|
| 'missing-row-key'
|
|
35
39
|
/** A stencil names parts the shape does not have. */
|
|
36
40
|
| 'missing-stencil-parts'
|
|
41
|
+
/** A Skip Link's `href` names no element in its tree, so a press moves focus nowhere. */
|
|
42
|
+
| 'missing-target'
|
|
37
43
|
/** A tooltip's `for` names no element in its own tree. */
|
|
38
44
|
| 'missing-trigger'
|
|
39
45
|
/** An area chart was handed a negative number, which it draws as none. */
|
|
@@ -42,6 +48,8 @@ export type RuntimeKind =
|
|
|
42
48
|
| 'not-a-graph'
|
|
43
49
|
/** The browser refused picture in picture. */
|
|
44
50
|
| 'picture-in-picture-refused'
|
|
51
|
+
/** A Grid's `min` is no length it can write, so the grid lays out by its column count. */
|
|
52
|
+
| 'refused-length'
|
|
45
53
|
/** A React handler prop is a removed callback, which never ran from JSX. */
|
|
46
54
|
| 'removed-callback'
|
|
47
55
|
/** A React handler prop uses Solid's `on:`, which React reads as another event's name. */
|
|
@@ -56,8 +64,12 @@ export type RuntimeKind =
|
|
|
56
64
|
| 'two-default-operators'
|
|
57
65
|
/** A React handler prop names an event nothing dispatches. */
|
|
58
66
|
| 'unknown-event'
|
|
67
|
+
/** An Icon's `name` is in no icon set it can read, so it draws nothing. */
|
|
68
|
+
| 'unknown-icon'
|
|
59
69
|
/** A tooltip's trigger does not name it in `aria-describedby`, so it is not read out. */
|
|
60
70
|
| 'unlinked-trigger'
|
|
71
|
+
/** A Text rendered as a label wraps or names a control, which its shadow root cannot reach. */
|
|
72
|
+
| 'unreachable-label'
|
|
61
73
|
/** A virtualized list's box has no height of its own, so its window never settles. */
|
|
62
74
|
| 'unsettled-window'
|
|
63
75
|
/** A slotted trigger is a plain element, which cannot be told it opens something. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** How `rovingStop` reads an item. */
|
|
2
|
+
export interface RovingOptions<T> {
|
|
3
|
+
/** Whether an item cannot take the stop. Defaults to none. */
|
|
4
|
+
disabled?: (item: T) => boolean;
|
|
5
|
+
/** Whether an item is chosen, which takes the stop before the first. Defaults to none. */
|
|
6
|
+
selected?: (item: T) => boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The item that holds a group's one tab stop: the one the reader was last on,
|
|
10
|
+
* then the first chosen one, then the first that can be reached.
|
|
11
|
+
*
|
|
12
|
+
* A composite widget is one Tab stop whose items the arrow keys move between,
|
|
13
|
+
* and Tab coming back lands where the reader left. A disabled item never holds
|
|
14
|
+
* it, so a group whose first item is disabled can still be entered from the
|
|
15
|
+
* keyboard. `last` is ignored once it has left `items`.
|
|
16
|
+
*
|
|
17
|
+
* Tag Group, a Toggle Group that presses several, and a Notifications feed
|
|
18
|
+
* choose their stop this way.
|
|
19
|
+
*/
|
|
20
|
+
export declare const rovingStop: <T>(items: readonly T[], last: T | undefined, { disabled, selected }?: RovingOptions<T>) => T | undefined;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const rovingStop = (items, last, { disabled = () => false, selected = () => false } = {}) => (last !== undefined && items.includes(last) && !disabled(last) ? last : undefined) ??
|
|
2
|
+
items.find((item) => selected(item) && !disabled(item)) ??
|
|
3
|
+
items.find((item) => !disabled(item));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stops the page behind a modal surface from scrolling.
|
|
3
|
+
*
|
|
4
|
+
* `showModal()` makes the page inert and leaves it scrollable, so a wheel over
|
|
5
|
+
* the backdrop moves the page under the dialog. `overflow: hidden` on the root
|
|
6
|
+
* stops that, and `scrollbar-gutter: stable` keeps the space a classic
|
|
7
|
+
* scrollbar took, so the page does not shift sideways when it goes. What the
|
|
8
|
+
* root carried before is put back when the last holder lets go.
|
|
9
|
+
*
|
|
10
|
+
* Dialog, Drawer and Lightbox hold it, and Toast reads who does.
|
|
11
|
+
*/
|
|
12
|
+
export declare const lockScroll: (holder: Element) => void;
|
|
13
|
+
/** Lets go of the page, and gives it back once nothing else holds it. */
|
|
14
|
+
export declare const unlockScroll: (holder: Element) => void;
|
|
15
|
+
/** The surfaces holding the page still, in the order they took it. */
|
|
16
|
+
export declare const scrollHolders: () => Element[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SCROLL_LOCK = Symbol.for('fusion.helix.scroll-lock');
|
|
2
|
+
const record = () => globalThis[SCROLL_LOCK];
|
|
3
|
+
export const lockScroll = (holder) => {
|
|
4
|
+
const globals = globalThis;
|
|
5
|
+
const root = document.documentElement;
|
|
6
|
+
globals[SCROLL_LOCK] ??= { gutter: '', holders: new Set(), overflow: '' };
|
|
7
|
+
const lock = globals[SCROLL_LOCK];
|
|
8
|
+
if (lock.holders.has(holder)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (lock.holders.size === 0) {
|
|
12
|
+
lock.overflow = root.style.overflow;
|
|
13
|
+
lock.gutter = root.style.scrollbarGutter;
|
|
14
|
+
if (window.innerWidth > root.clientWidth) {
|
|
15
|
+
root.style.scrollbarGutter = 'stable';
|
|
16
|
+
}
|
|
17
|
+
root.style.overflow = 'hidden';
|
|
18
|
+
}
|
|
19
|
+
lock.holders.add(holder);
|
|
20
|
+
};
|
|
21
|
+
export const unlockScroll = (holder) => {
|
|
22
|
+
const lock = record();
|
|
23
|
+
if (!lock?.holders.delete(holder) || lock.holders.size > 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const root = document.documentElement;
|
|
27
|
+
root.style.overflow = lock.overflow;
|
|
28
|
+
root.style.scrollbarGutter = lock.gutter;
|
|
29
|
+
};
|
|
30
|
+
export const scrollHolders = () => [...(record()?.holders ?? [])];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The edge of the window a surface leaves through when it is swiped away. */
|
|
2
|
+
export type SwipeEdge = 'bottom' | 'left' | 'right' | 'top';
|
|
3
|
+
/** Which way along the screen a swipe is read. */
|
|
4
|
+
export type SwipeAxis = 'x' | 'y';
|
|
5
|
+
/**
|
|
6
|
+
* How fast a release has to be moving, in pixels per millisecond, to count as
|
|
7
|
+
* a fling: the surface then goes on in the direction it was thrown, whatever
|
|
8
|
+
* distance it had covered.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FLING = 0.5;
|
|
11
|
+
/**
|
|
12
|
+
* The edge a surface placed at `placement` leaves through, resolved against the
|
|
13
|
+
* writing direction. `start` and `end` are logical, so on a right-to-left page
|
|
14
|
+
* the end is the left edge.
|
|
15
|
+
*/
|
|
16
|
+
export declare const swipeEdge: (placement: 'bottom' | 'end' | 'start' | 'top', rtl: boolean) => SwipeEdge;
|
|
17
|
+
/** The axis a swipe toward `edge` is read along. */
|
|
18
|
+
export declare const swipeAxis: (edge: SwipeEdge) => SwipeAxis;
|
|
19
|
+
/** How far a movement of `dx` and `dy` went toward `edge`, negative when it went away. */
|
|
20
|
+
export declare const towardEdge: (edge: SwipeEdge, dx: number, dy: number) => number;
|
|
21
|
+
/** A distance toward `edge` as a physical offset, for a `translate`. */
|
|
22
|
+
export declare const edgeOffset: (edge: SwipeEdge, travel: number) => {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The fractions of a surface a reader can leave on screen, read from a list
|
|
28
|
+
* such as `"0.5 1"`. Each is above zero and at most one, and the list is sorted
|
|
29
|
+
* with no repeats. Where nothing usable is written, the one point is `1`: the
|
|
30
|
+
* whole surface.
|
|
31
|
+
*/
|
|
32
|
+
export declare const parseSnapPoints: (text: string | null | undefined) => number[];
|
|
33
|
+
/** What a release asks of `settleSwipe`. */
|
|
34
|
+
export interface SwipeRelease {
|
|
35
|
+
/** The snap points, from `parseSnapPoints`. */
|
|
36
|
+
points: readonly number[];
|
|
37
|
+
/** The surface's size along the axis, in pixels. */
|
|
38
|
+
size: number;
|
|
39
|
+
/** How far the surface sits toward its edge, from fully shown, in pixels. */
|
|
40
|
+
travel: number;
|
|
41
|
+
/** How fast it was moving toward its edge at the release, in pixels per millisecond. */
|
|
42
|
+
velocity: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The fraction of the surface to rest at after a release, and `0` to dismiss it.
|
|
46
|
+
*
|
|
47
|
+
* A fling goes on to the next point in the direction it was thrown, and
|
|
48
|
+
* flinging toward the edge from the smallest point dismisses. Otherwise the
|
|
49
|
+
* surface rests at the nearest point, dismissal counted as the point at zero, so
|
|
50
|
+
* a surface with the one point `1` goes once it is dragged more than halfway.
|
|
51
|
+
*/
|
|
52
|
+
export declare const settleSwipe: ({ points, size, travel, velocity }: SwipeRelease) => number;
|
|
53
|
+
/** How far toward its edge a surface resting at `fraction` sits, in pixels. */
|
|
54
|
+
export declare const snapTravel: (size: number, fraction: number) => number;
|
|
55
|
+
/**
|
|
56
|
+
* Which way a swipe across a row of items moves: `1` to the next, `-1` to the
|
|
57
|
+
* previous, `0` to stay.
|
|
58
|
+
*
|
|
59
|
+
* Dragging the picture toward the start of the line brings the next one in,
|
|
60
|
+
* so the next item is a drag leftward on a left-to-right page and rightward on
|
|
61
|
+
* a right-to-left one. A quarter of the width, or a fling, is enough.
|
|
62
|
+
*/
|
|
63
|
+
export declare const swipeStep: (dx: number, vx: number, width: number, rtl: boolean) => -1 | 0 | 1;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const FLING = 0.5;
|
|
2
|
+
export const swipeEdge = (placement, rtl) => {
|
|
3
|
+
if (placement === 'top' || placement === 'bottom') {
|
|
4
|
+
return placement;
|
|
5
|
+
}
|
|
6
|
+
return (placement === 'end') !== rtl ? 'right' : 'left';
|
|
7
|
+
};
|
|
8
|
+
export const swipeAxis = (edge) => edge === 'left' || edge === 'right' ? 'x' : 'y';
|
|
9
|
+
export const towardEdge = (edge, dx, dy) => ({ bottom: dy, left: -dx, right: dx, top: -dy })[edge];
|
|
10
|
+
export const edgeOffset = (edge, travel) => ({
|
|
11
|
+
x: edge === 'right' ? travel : edge === 'left' ? -travel : 0,
|
|
12
|
+
y: edge === 'bottom' ? travel : edge === 'top' ? -travel : 0,
|
|
13
|
+
});
|
|
14
|
+
export const parseSnapPoints = (text) => {
|
|
15
|
+
const points = [
|
|
16
|
+
...new Set((text ?? '')
|
|
17
|
+
.split(/[\s,]+/)
|
|
18
|
+
.map(Number)
|
|
19
|
+
.filter((one) => Number.isFinite(one) && one > 0 && one <= 1)),
|
|
20
|
+
].sort((a, b) => a - b);
|
|
21
|
+
return points.length > 0 ? points : [1];
|
|
22
|
+
};
|
|
23
|
+
export const settleSwipe = ({ points, size, travel, velocity }) => {
|
|
24
|
+
if (size <= 0) {
|
|
25
|
+
return points[points.length - 1] ?? 1;
|
|
26
|
+
}
|
|
27
|
+
const shown = Math.max(0, Math.min(1, 1 - travel / size));
|
|
28
|
+
const stops = [0, ...points];
|
|
29
|
+
if (velocity >= FLING) {
|
|
30
|
+
return [...stops].reverse().find((one) => one < shown) ?? 0;
|
|
31
|
+
}
|
|
32
|
+
if (velocity <= -FLING) {
|
|
33
|
+
return stops.find((one) => one > shown) ?? stops[stops.length - 1] ?? 1;
|
|
34
|
+
}
|
|
35
|
+
return stops.reduce((best, one) => (Math.abs(one - shown) < Math.abs(best - shown) ? one : best));
|
|
36
|
+
};
|
|
37
|
+
export const snapTravel = (size, fraction) => Math.round((1 - fraction) * size);
|
|
38
|
+
export const swipeStep = (dx, vx, width, rtl) => {
|
|
39
|
+
const along = rtl ? dx : -dx;
|
|
40
|
+
const speed = rtl ? vx : -vx;
|
|
41
|
+
if (speed >= FLING || (speed > -FLING && along > width / 4)) {
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
if (speed <= -FLING || along < -width / 4) {
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every element in `root` that Tab stops at, in the order Tab reaches them,
|
|
3
|
+
* through shadow roots and slots.
|
|
4
|
+
*
|
|
5
|
+
* Through them because a panel's controls are inside the component it holds,
|
|
6
|
+
* and often a button's inside that, so a light-DOM query such as
|
|
7
|
+
* `focusableElements` finds none of them. Walked in the composed tree's order:
|
|
8
|
+
* a host's shadow root in place of its children, and a slot's assigned
|
|
9
|
+
* elements in place of its fallback. A host that delegates its focus is
|
|
10
|
+
* passed through to what it delegates to, and anything `hidden`, `inert`,
|
|
11
|
+
* disabled or without a box is left out.
|
|
12
|
+
*
|
|
13
|
+
* `MixinPickerField` keeps Tab inside a picker's panel with it, reading the
|
|
14
|
+
* first and last of these as the ends of the dialog.
|
|
15
|
+
*/
|
|
16
|
+
export declare const tabStops: (root: Element) => HTMLElement[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const tabStops = (root) => {
|
|
2
|
+
const found = [];
|
|
3
|
+
const visit = (node) => {
|
|
4
|
+
const element = node;
|
|
5
|
+
if (element.hidden || element.inert) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (element.tabIndex >= 0 &&
|
|
9
|
+
!element.shadowRoot?.delegatesFocus &&
|
|
10
|
+
!element.disabled &&
|
|
11
|
+
element.getClientRects().length > 0) {
|
|
12
|
+
found.push(element);
|
|
13
|
+
}
|
|
14
|
+
const assigned = node instanceof HTMLSlotElement ? node.assignedElements() : [];
|
|
15
|
+
const children = assigned.length > 0 ? assigned : [...(node.shadowRoot ?? node).children];
|
|
16
|
+
for (const child of children) {
|
|
17
|
+
visit(child);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
visit(root);
|
|
21
|
+
return found;
|
|
22
|
+
};
|
|
@@ -55,6 +55,26 @@ export declare const valueAsMinutes: (value: string) => number | null;
|
|
|
55
55
|
export declare const minutesAsValue: (minutes: number) => string;
|
|
56
56
|
/** Whether a value is a time these controls can hold, which is minute precision. */
|
|
57
57
|
export declare const isTimeValue: (value: string) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The seconds since midnight a value names, or `null` where it is not a time.
|
|
60
|
+
*
|
|
61
|
+
* **To the second, which is what Time Picker and Time Input hold under
|
|
62
|
+
* `seconds`.** `valueAsMinutes` above stays at minute precision for the
|
|
63
|
+
* controls that never carry one. Both shapes are read whatever a control
|
|
64
|
+
* writes, so a value with seconds handed to a field without them is a time it
|
|
65
|
+
* can judge, off its step, rather than text it cannot read.
|
|
66
|
+
*/
|
|
67
|
+
export declare const valueAsSeconds: (value: string) => number | null;
|
|
68
|
+
/**
|
|
69
|
+
* Whether a value is a time a control holds: `HH:mm`, or either shape where it
|
|
70
|
+
* holds `seconds`.
|
|
71
|
+
*/
|
|
72
|
+
export declare const isClockValue: (value: string, seconds?: boolean) => boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Seconds since midnight as `HH:mm`, or `HH:mm:ss` with `seconds`, wrapping
|
|
75
|
+
* rather than running off the day.
|
|
76
|
+
*/
|
|
77
|
+
export declare const secondsAsValue: (at: number, seconds?: boolean) => string;
|
|
58
78
|
/**
|
|
59
79
|
* The number a value holds, or `NaN`.
|
|
60
80
|
*
|
|
@@ -53,5 +53,21 @@ export const minutesAsValue = (minutes) => {
|
|
|
53
53
|
return `${String(Math.floor(whole / 60)).padStart(2, '0')}:${String(whole % 60).padStart(2, '0')}`;
|
|
54
54
|
};
|
|
55
55
|
export const isTimeValue = (value) => valueAsMinutes(value) !== null;
|
|
56
|
+
const CLOCK = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/;
|
|
57
|
+
const DAY_SECONDS = 86_400;
|
|
58
|
+
export const valueAsSeconds = (value) => {
|
|
59
|
+
const parts = CLOCK.exec(value.trim());
|
|
60
|
+
if (!parts) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return Number(parts[1]) * 3600 + Number(parts[2]) * 60 + Number(parts[3] ?? 0);
|
|
64
|
+
};
|
|
65
|
+
export const isClockValue = (value, seconds = false) => valueAsSeconds(value) !== null && (seconds || value.trim().length === 5);
|
|
66
|
+
export const secondsAsValue = (at, seconds = false) => {
|
|
67
|
+
const whole = ((Math.round(at) % DAY_SECONDS) + DAY_SECONDS) % DAY_SECONDS;
|
|
68
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
69
|
+
const clock = `${pad(Math.floor(whole / 3600))}:${pad(Math.floor(whole / 60) % 60)}`;
|
|
70
|
+
return seconds ? `${clock}:${pad(whole % 60)}` : clock;
|
|
71
|
+
};
|
|
56
72
|
export const valueAsNumber = (value) => value.trim() === '' ? Number.NaN : Number(value);
|
|
57
73
|
export const numberAsValue = (count) => (Number.isNaN(count) ? '' : String(count));
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
* 24 hour regardless.
|
|
10
10
|
*
|
|
11
11
|
* A value this cannot read is handed back unchanged, so a field showing what is
|
|
12
|
-
* being typed shows the letters rather than blanking under the reader.
|
|
12
|
+
* being typed shows the letters rather than blanking under the reader. Minute
|
|
13
|
+
* precision, as `valueAsMinutes` reads: `writeClock` is the same writing to the
|
|
14
|
+
* second.
|
|
13
15
|
*/
|
|
14
16
|
export declare const writeTime: (value: string, locale?: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* A time written the way the locale writes one, with its seconds where asked.
|
|
19
|
+
*
|
|
20
|
+
* Reads both `HH:mm` and `HH:mm:ss`, as `valueAsSeconds` does, which is what
|
|
21
|
+
* Time Picker and Time Input hold; a locale with its clock set by `clockLocale`
|
|
22
|
+
* writes on that clock. A value that is not a time is handed back unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export declare const writeClock: (value: string, locale: string, seconds?: boolean) => string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { valueAsMinutes } from '../value-as/value-as.js';
|
|
2
|
-
export const writeTime = (value, locale) =>
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { valueAsMinutes, valueAsSeconds } from '../value-as/value-as.js';
|
|
2
|
+
export const writeTime = (value, locale) => valueAsMinutes(value) === null ? value : writeClock(value, locale ?? '');
|
|
3
|
+
export const writeClock = (value, locale, seconds = false) => {
|
|
4
|
+
const at = valueAsSeconds(value);
|
|
5
|
+
if (at === null) {
|
|
5
6
|
return value;
|
|
6
7
|
}
|
|
7
|
-
const at = new Date(Date.UTC(2000, 0, 1, Math.floor(minutes / 60), minutes % 60));
|
|
8
8
|
return new Intl.DateTimeFormat(locale || undefined, {
|
|
9
|
-
timeStyle: 'short',
|
|
9
|
+
timeStyle: seconds ? 'medium' : 'short',
|
|
10
10
|
timeZone: 'UTC',
|
|
11
|
-
}).format(at);
|
|
11
|
+
}).format(new Date(at * 1000));
|
|
12
12
|
};
|