@fusion.dev/helix-core 0.143.0 → 0.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +10266 -4161
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +10 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +6 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +89 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +102 -1
- package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
- package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
- package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
- package/dist/controllers/copy-controller/copy-controller.js +11 -9
- package/dist/controllers/countdown-controller/countdown-controller.d.ts +55 -0
- package/dist/controllers/countdown-controller/countdown-controller.js +57 -0
- package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
- package/dist/controllers/delay-controller/delay-controller.js +54 -0
- package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
- package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +11 -2
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +4 -1
- package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
- package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
- package/dist/controllers/source-controller/source-controller.d.ts +16 -0
- package/dist/controllers/source-controller/source-controller.js +44 -1
- package/dist/controllers/status-controller/status-controller.d.ts +47 -0
- package/dist/controllers/status-controller/status-controller.js +34 -0
- package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
- package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +68 -35
- package/dist/controllers/virtualize-controller/virtualize-controller.js +77 -10
- package/dist/events/events.d.ts +221 -0
- package/dist/events/events.js +150 -0
- package/dist/helix-element/helix-element.js +14 -1
- package/dist/helix-element/react-bridge.d.ts +9 -4
- package/dist/helix-element/react-bridge.js +14 -1
- package/dist/index.d.ts +31 -9
- package/dist/index.js +29 -7
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +5 -1
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +7 -3
- package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
- package/dist/mixins/mixin-field/mixin-field.js +8 -0
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +16 -6
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +19 -1
- package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +5 -4
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +12 -1
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
- package/dist/utils/arrow/arrow.d.ts +15 -0
- package/dist/utils/arrow/arrow.js +2 -0
- package/dist/utils/audit-name/audit-name.d.ts +28 -0
- package/dist/utils/audit-name/audit-name.js +95 -0
- package/dist/utils/calendar/calendar.d.ts +117 -0
- package/dist/utils/calendar/calendar.js +96 -0
- package/dist/utils/chord/chord.d.ts +43 -0
- package/dist/utils/chord/chord.js +53 -0
- package/dist/utils/clock/clock.d.ts +87 -0
- package/dist/utils/clock/clock.js +68 -0
- package/dist/utils/color-name/color-name.d.ts +28 -0
- package/dist/utils/color-name/color-name.js +150 -0
- package/dist/utils/count-label/count-label.d.ts +13 -0
- package/dist/utils/count-label/count-label.js +5 -0
- package/dist/utils/css-color/css-color.d.ts +12 -0
- package/dist/utils/css-color/css-color.js +2 -2
- package/dist/utils/date-segments/date-segments.d.ts +122 -0
- package/dist/utils/date-segments/date-segments.js +206 -0
- package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
- package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +7 -6
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -21
- package/dist/utils/draw-tooltip/draw-tooltip.js +11 -5
- package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
- package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
- package/dist/utils/follow-link/follow-link.d.ts +46 -0
- package/dist/utils/follow-link/follow-link.js +37 -0
- package/dist/utils/heading-in/heading-in.d.ts +19 -0
- package/dist/utils/heading-in/heading-in.js +14 -0
- package/dist/utils/number-format/number-format.d.ts +23 -0
- package/dist/utils/number-format/number-format.js +55 -0
- package/dist/utils/platform/platform.d.ts +23 -0
- package/dist/utils/platform/platform.js +11 -0
- package/dist/utils/range-add/range-add.d.ts +19 -0
- package/dist/utils/range-add/range-add.js +5 -0
- package/dist/utils/read-color/read-color.d.ts +14 -0
- package/dist/utils/read-color/read-color.js +13 -0
- package/dist/utils/read-series/read-series.d.ts +6 -0
- package/dist/utils/read-series/read-series.js +1 -0
- package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
- package/dist/utils/reflect-elements/reflect-elements.js +40 -0
- package/dist/utils/relative-time/relative-time.d.ts +13 -1
- package/dist/utils/relative-time/relative-time.js +7 -4
- package/dist/utils/report-runtime/report-runtime.d.ts +5 -1
- package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
- package/dist/utils/roving-stop/roving-stop.js +3 -0
- package/dist/utils/swipe/swipe.d.ts +63 -0
- package/dist/utils/swipe/swipe.js +48 -0
- package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
- package/dist/utils/tab-stops/tab-stops.js +22 -0
- package/dist/utils/value-as/value-as.d.ts +20 -0
- package/dist/utils/value-as/value-as.js +16 -0
- package/dist/utils/write-time/write-time.d.ts +15 -5
- package/dist/utils/write-time/write-time.js +7 -7
- package/dist/utils/written-list/written-list.d.ts +18 -0
- package/dist/utils/written-list/written-list.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const HEADING = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
2
|
+
export const headingIn = (elements) => {
|
|
3
|
+
for (const one of elements) {
|
|
4
|
+
const found = one.matches(HEADING) ? one : one.querySelector(HEADING);
|
|
5
|
+
if (found) {
|
|
6
|
+
return found;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
11
|
+
export const headingsIn = (elements) => {
|
|
12
|
+
const heading = headingIn(elements);
|
|
13
|
+
return heading ? [heading] : [...elements];
|
|
14
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes and reads numbers in a locale with `Intl.NumberFormat` options, for a
|
|
3
|
+
* control that shows a number and takes one typed: a currency, a percent, a
|
|
4
|
+
* unit, or grouped thousands. React Aria's NumberField and Slider take the same
|
|
5
|
+
* `formatOptions`.
|
|
6
|
+
*
|
|
7
|
+
* **Formatters are kept**, by locale and options, since one is expensive to
|
|
8
|
+
* build and cheap to call, and a control formats on every update.
|
|
9
|
+
*/
|
|
10
|
+
/** Writes a number in a locale with `Intl.NumberFormat` options. */
|
|
11
|
+
export declare const formatNumber: (value: number, locale?: string, options?: Intl.NumberFormatOptions) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Reads a number written in a locale with the same options it is shown with,
|
|
14
|
+
* or `null` where the text is no number.
|
|
15
|
+
*
|
|
16
|
+
* The locale's group separators are dropped and its decimal separator read as
|
|
17
|
+
* a point, so `1.234,5` in `de-DE` is 1234.5. A currency sign, a percent sign
|
|
18
|
+
* and a unit the options write are dropped too, and a percent is read back as
|
|
19
|
+
* the fraction it shows: `25 %` is 0.25. A point is read as a decimal point in
|
|
20
|
+
* a locale that groups with something else, which is what a decimal keypad
|
|
21
|
+
* types.
|
|
22
|
+
*/
|
|
23
|
+
export declare const parseNumber: (text: string, locale?: string, options?: Intl.NumberFormatOptions) => number | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const held = new Map();
|
|
2
|
+
const formatter = (locale, options) => {
|
|
3
|
+
const key = `${locale}|${JSON.stringify(options ?? {})}`;
|
|
4
|
+
let found = held.get(key);
|
|
5
|
+
if (!found) {
|
|
6
|
+
found = new Intl.NumberFormat(locale || undefined, options);
|
|
7
|
+
held.set(key, found);
|
|
8
|
+
}
|
|
9
|
+
return found;
|
|
10
|
+
};
|
|
11
|
+
export const formatNumber = (value, locale = '', options) => formatter(locale, options).format(value);
|
|
12
|
+
const grouped = (text, separator, decimal) => {
|
|
13
|
+
const whole = (text.split(decimal)[0] ?? '').replace(/^[+-]/u, '');
|
|
14
|
+
return new RegExp(`^\\d{1,3}(${literal(separator)}\\d{3})+$`, 'u').test(whole);
|
|
15
|
+
};
|
|
16
|
+
const literal = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
17
|
+
export const parseNumber = (text, locale = '', options) => {
|
|
18
|
+
const parts = formatter(locale, options).formatToParts(-12345.6);
|
|
19
|
+
const of = (type) => parts.filter((part) => part.type === type).map((part) => part.value);
|
|
20
|
+
const group = of('group')[0] ?? ',';
|
|
21
|
+
const decimal = of('decimal')[0] ?? '.';
|
|
22
|
+
const dropped = [...of('currency'), ...of('percentSign'), ...of('unit'), ...of('literal')]
|
|
23
|
+
.map((one) => one.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const percent = options?.style === 'percent' && /[%\u066a\u2030]/u.test(text);
|
|
26
|
+
let read = text.replace(/[\s\u00a0\u202f]/gu, '');
|
|
27
|
+
for (const one of dropped) {
|
|
28
|
+
read = read.replace(new RegExp(literal(one), 'gu'), '');
|
|
29
|
+
}
|
|
30
|
+
read = read.replace(/[\u2212\u2012\u2013]/gu, '-');
|
|
31
|
+
const separator = group.trim() || group;
|
|
32
|
+
if (read.includes(separator) && !grouped(read, separator, decimal)) {
|
|
33
|
+
if (group !== '.' || read.includes(decimal)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (group !== '.' || decimal === '.') {
|
|
38
|
+
read = read.replace(new RegExp(literal(separator), 'gu'), '');
|
|
39
|
+
}
|
|
40
|
+
else if (read.includes(decimal)) {
|
|
41
|
+
read = read.replaceAll(group, '');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
read = read.replaceAll(group, '.');
|
|
45
|
+
}
|
|
46
|
+
read = read.replace(decimal, '.');
|
|
47
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)$/u.test(read)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const parsed = Number(read);
|
|
51
|
+
if (!Number.isFinite(parsed)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return percent ? parsed / 100 : parsed;
|
|
55
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** What `onApple` and `appleTouch` read of the browser: the parts of `navigator` they need. */
|
|
2
|
+
export interface Platform {
|
|
3
|
+
readonly maxTouchPoints?: number;
|
|
4
|
+
readonly platform?: string;
|
|
5
|
+
readonly userAgent?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether the browser is iOS or iPadOS, where VoiceOver reads a
|
|
9
|
+
* `role="spinbutton"` text field as a slider a reader cannot type in. React
|
|
10
|
+
* Aria's NumberField drops the role there for the same reason. An iPad asks
|
|
11
|
+
* for the desktop site and reports itself as a Mac, so a Mac with a touch
|
|
12
|
+
* screen counts too.
|
|
13
|
+
*/
|
|
14
|
+
export declare const appleTouch: (platform: Platform | undefined) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the keyboard in front of the reader is an Apple one, which labels
|
|
17
|
+
* its modifiers with glyphs and whose platform modifier is Command.
|
|
18
|
+
*
|
|
19
|
+
* From the user agent, as every application asks it; `false` on a server,
|
|
20
|
+
* which has no keyboard to ask. Kbd draws `⌘K` or `Ctrl+K` from it and reads
|
|
21
|
+
* `mod` as the key it names.
|
|
22
|
+
*/
|
|
23
|
+
export declare const onApple: (platform?: Platform | undefined) => boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const appleTouch = (platform) => {
|
|
2
|
+
if (!platform) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const agent = platform.userAgent ?? '';
|
|
6
|
+
if (/iPhone|iPad|iPod/u.test(agent)) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
return platform.platform === 'MacIntel' && (platform.maxTouchPoints ?? 0) > 1;
|
|
10
|
+
};
|
|
11
|
+
export const onApple = (platform = typeof navigator === 'undefined' ? undefined : navigator) => platform !== undefined && /mac|iphone|ipad|ipod/i.test(platform.userAgent ?? '');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a range adds to a selection of many, as arithmetic over the items on
|
|
3
|
+
* show.
|
|
4
|
+
*
|
|
5
|
+
* Pure and in core, because Listbox (Shift with Home or End) and Data Table
|
|
6
|
+
* (Shift with an arrow, Home or End, over rows) take in a range the same way,
|
|
7
|
+
* and a spec can hold it without a page.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The selection after Shift with Home or End: every option shown from the
|
|
11
|
+
* highlight to the end the key went to, added after what was selected, or
|
|
12
|
+
* `null` where nothing new is taken in.
|
|
13
|
+
*
|
|
14
|
+
* `from` is where the highlight was, as an index into `shown`, or `-1` for no
|
|
15
|
+
* highlight, which takes in the end alone. `to` is the end the key went to.
|
|
16
|
+
* APG's multiple-selection listbox, and the same rule over a data table's
|
|
17
|
+
* rows.
|
|
18
|
+
*/
|
|
19
|
+
export declare const rangeAdd: (values: readonly string[], shown: readonly string[], from: number, to: number) => string[] | null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const rangeAdd = (values, shown, from, to) => {
|
|
2
|
+
const [start, end] = from < 0 ? [to, to] : [Math.min(from, to), Math.max(from, to)];
|
|
3
|
+
const added = shown.slice(start, end + 1).filter((value) => !values.includes(value));
|
|
4
|
+
return added.length > 0 ? [...values, ...added] : null;
|
|
5
|
+
};
|
|
@@ -34,3 +34,17 @@ export type ColorBytes = [number, number, number];
|
|
|
34
34
|
* @param value A resolved color, as `getComputedStyle` reports one.
|
|
35
35
|
*/
|
|
36
36
|
export declare const readColor: (value: string) => ColorBytes | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* A computed color as `rgb()`, or `rgba()` where it is see-through, which
|
|
39
|
+
* every SVG reader can draw.
|
|
40
|
+
*
|
|
41
|
+
* A theme's colors compute to `oklch()` or `color()`, which a browser draws
|
|
42
|
+
* and an image editor opening a saved file may not, so the color is painted
|
|
43
|
+
* onto one fresh pixel and read back, its alpha with it. Unlike `readColor`,
|
|
44
|
+
* which refuses what it cannot tell apart from its ground, this keeps what the
|
|
45
|
+
* pixel says: a computed style is always a color the engine parsed. A page with
|
|
46
|
+
* no canvas keeps the color as it is.
|
|
47
|
+
*
|
|
48
|
+
* @param color A resolved color, as `getComputedStyle` reports one.
|
|
49
|
+
*/
|
|
50
|
+
export declare const rgbOf: (color: string) => string;
|
|
@@ -34,3 +34,16 @@ export const readColor = (value) => {
|
|
|
34
34
|
known.set(value, read);
|
|
35
35
|
return read ? [...read] : undefined;
|
|
36
36
|
};
|
|
37
|
+
export const rgbOf = (color) => {
|
|
38
|
+
const pixel = typeof document === 'undefined' ? null : document.createElement('canvas');
|
|
39
|
+
const context = pixel?.getContext?.('2d', { willReadFrequently: true });
|
|
40
|
+
if (!context) {
|
|
41
|
+
return color;
|
|
42
|
+
}
|
|
43
|
+
context.fillStyle = color;
|
|
44
|
+
context.fillRect(0, 0, 1, 1);
|
|
45
|
+
const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
|
|
46
|
+
return alpha === 255
|
|
47
|
+
? `rgb(${red}, ${green}, ${blue})`
|
|
48
|
+
: `rgba(${red}, ${green}, ${blue}, ${(alpha ?? 0) / 255})`;
|
|
49
|
+
};
|
|
@@ -130,3 +130,9 @@ export interface Row {
|
|
|
130
130
|
* on render, rows only where a pointer has stopped.
|
|
131
131
|
*/
|
|
132
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;
|
|
@@ -138,3 +138,4 @@ export const readRows = (table) => {
|
|
|
138
138
|
.map((one) => one.textContent?.trim() ?? '');
|
|
139
139
|
return [...(element.tBodies[0]?.rows ?? [])].map((one) => row(one.cells[0], [...one.cells].slice(1), names));
|
|
140
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 = 'ariaControlsElements' | '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, a Tour stop names and
|
|
15
|
+
* describes its panel, and a Tab points its control at the panel it shows.
|
|
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) => {
|
|
@@ -30,8 +30,12 @@ export type RuntimeKind =
|
|
|
30
30
|
| 'misread-direction'
|
|
31
31
|
/** A React handler prop names an event the element never dispatches itself. */
|
|
32
32
|
| 'misplaced-handler'
|
|
33
|
+
/** An Image has no `alt`, so a screen reader announces its file name. */
|
|
34
|
+
| 'missing-alt'
|
|
33
35
|
/** The three.js driver could not load its layout, so the notes stay where they started. */
|
|
34
36
|
| 'missing-layout'
|
|
37
|
+
/** A control has no accessible name, so a screen reader announces it by its role alone. */
|
|
38
|
+
| 'missing-name'
|
|
35
39
|
/** A selectable Data Table has no `row-key`, so it knows each row by its place. */
|
|
36
40
|
| 'missing-row-key'
|
|
37
41
|
/** A stencil names parts the shape does not have. */
|
|
@@ -66,7 +70,7 @@ export type RuntimeKind =
|
|
|
66
70
|
| 'unknown-icon'
|
|
67
71
|
/** A tooltip's trigger does not name it in `aria-describedby`, so it is not read out. */
|
|
68
72
|
| 'unlinked-trigger'
|
|
69
|
-
/** A Text rendered as a label
|
|
73
|
+
/** A Text is rendered as a label, a deprecated value whose shadow root reaches no control. */
|
|
70
74
|
| 'unreachable-label'
|
|
71
75
|
/** A virtualized list's box has no height of its own, so its window never settles. */
|
|
72
76
|
| 'unsettled-window'
|
|
@@ -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,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));
|
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `Intl` decides between a twelve and a twenty-four hour clock, because that is
|
|
5
5
|
* a property of the locale rather than of any component: `en-US` gets `2:30 PM`
|
|
6
|
-
* and `en-GB` gets `14:30` without either being configured.
|
|
7
|
-
* force one
|
|
8
|
-
*
|
|
9
|
-
* 24 hour regardless.
|
|
6
|
+
* and `en-GB` gets `14:30` without either being configured. This function takes
|
|
7
|
+
* no flag to force one. A family that offers the choice, such as Time Picker
|
|
8
|
+
* and Time Input with `hour-cycle`, passes it in the locale, as its `-u-hc-`
|
|
9
|
+
* extension (`en-US-u-hc-h23`). The value is 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
|
};
|