@fusion.dev/helix-core 0.147.0 → 0.149.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.
Files changed (124) hide show
  1. package/custom-elements.json +16160 -12084
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
  5. package/dist/controllers/children-controller/children-controller.d.ts +28 -0
  6. package/dist/controllers/children-controller/children-controller.js +18 -0
  7. package/dist/controllers/countdown-controller/countdown-controller.d.ts +15 -1
  8. package/dist/controllers/countdown-controller/countdown-controller.js +27 -2
  9. package/dist/controllers/floating-controller/floating-controller.d.ts +12 -1
  10. package/dist/controllers/floating-controller/floating-controller.js +2 -1
  11. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +25 -0
  12. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +8 -0
  13. package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
  14. package/dist/controllers/machine-controller/machine-controller.js +78 -0
  15. package/dist/controllers/machine-controller/machine.d.ts +93 -0
  16. package/dist/controllers/machine-controller/machine.js +20 -0
  17. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +10 -0
  18. package/dist/controllers/open-phase-controller/open-phase-controller.js +7 -4
  19. package/dist/controllers/option-window-controller/option-window-controller.d.ts +96 -0
  20. package/dist/controllers/option-window-controller/option-window-controller.js +81 -0
  21. package/dist/controllers/popover-controller/popover-controller.js +1 -1
  22. package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
  23. package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
  24. package/dist/controllers/source-controller/source-controller.js +1 -1
  25. package/dist/controllers/virtualize-controller/heights.js +1 -1
  26. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
  27. package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
  28. package/dist/events/before-reorder.d.ts +14 -0
  29. package/dist/events/before-reorder.js +11 -0
  30. package/dist/events/details.d.ts +28 -0
  31. package/dist/events/events.d.ts +4 -2
  32. package/dist/events/events.js +3 -1
  33. package/dist/events/names.js +1 -1
  34. package/dist/events/reorder.d.ts +18 -12
  35. package/dist/events/reordered.d.ts +4 -4
  36. package/dist/helix-element/helix-element.d.ts +14 -5
  37. package/dist/helix-element/helix-element.js +6 -3
  38. package/dist/helix-element/react-audit-off.d.ts +11 -0
  39. package/dist/helix-element/react-audit-off.js +1 -0
  40. package/dist/helix-element/react-audit.d.ts +20 -0
  41. package/dist/helix-element/react-audit.js +56 -0
  42. package/dist/helix-element/react-bridge.d.ts +2 -22
  43. package/dist/helix-element/react-bridge.js +1 -56
  44. package/dist/index.d.ts +34 -14
  45. package/dist/index.js +32 -12
  46. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
  47. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +6 -0
  48. package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
  49. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
  50. package/dist/mixins/mixin-field/mixin-field.js +1 -1
  51. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
  52. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
  53. package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
  54. package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
  55. package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
  56. package/dist/mixins/mixin-viewport/mixin-viewport.js +23 -41
  57. package/dist/utils/assign-slots/assign-slots.d.ts +18 -0
  58. package/dist/utils/assign-slots/assign-slots.js +14 -0
  59. package/dist/utils/autofill-select/autofill-select.d.ts +40 -0
  60. package/dist/utils/autofill-select/autofill-select.js +14 -0
  61. package/dist/utils/calendar/calendar.d.ts +27 -0
  62. package/dist/utils/calendar/calendar.js +17 -0
  63. package/dist/utils/center-in-view/center-in-view.d.ts +18 -0
  64. package/dist/utils/center-in-view/center-in-view.js +14 -0
  65. package/dist/utils/chord/key-names.d.ts +30 -0
  66. package/dist/utils/chord/key-names.js +52 -0
  67. package/dist/utils/column-step/column-step.d.ts +13 -0
  68. package/dist/utils/column-step/column-step.js +9 -0
  69. package/dist/utils/count-label/count-label.d.ts +7 -4
  70. package/dist/utils/count-label/count-label.js +2 -5
  71. package/dist/utils/data-options/data-options.d.ts +60 -0
  72. package/dist/utils/data-options/data-options.js +46 -0
  73. package/dist/utils/date-segments/date-segments.d.ts +2 -1
  74. package/dist/utils/date-segments/date-segments.js +10 -0
  75. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +7 -2
  76. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -1
  77. package/dist/utils/draw-tooltip/spoken-reading.d.ts +3 -2
  78. package/dist/utils/draw-tooltip/spoken-reading.js +3 -2
  79. package/dist/utils/export-parts/export-parts.d.ts +14 -0
  80. package/dist/utils/export-parts/export-parts.js +1 -0
  81. package/dist/utils/fill-template/fill-template.d.ts +9 -0
  82. package/dist/utils/fill-template/fill-template.js +3 -0
  83. package/dist/utils/filter-marks/filter-marks.d.ts +36 -0
  84. package/dist/utils/filter-marks/filter-marks.js +41 -0
  85. package/dist/utils/focus-target/focus-target.d.ts +12 -0
  86. package/dist/utils/focus-target/focus-target.js +9 -0
  87. package/dist/utils/focusable-elements/focusable-elements.d.ts +17 -0
  88. package/dist/utils/focusable-elements/focusable-elements.js +8 -0
  89. package/dist/utils/grid-move/grid-move.d.ts +56 -0
  90. package/dist/utils/grid-move/grid-move.js +28 -0
  91. package/dist/utils/locale-of/locale-of.d.ts +5 -0
  92. package/dist/utils/locale-of/locale-of.js +8 -2
  93. package/dist/utils/number-format/number-format.d.ts +7 -0
  94. package/dist/utils/number-format/number-format.js +1 -0
  95. package/dist/utils/reorder/drag-preview.d.ts +46 -0
  96. package/dist/utils/reorder/drag-preview.js +58 -0
  97. package/dist/utils/reorder/reorder.d.ts +178 -0
  98. package/dist/utils/reorder/reorder.js +112 -0
  99. package/dist/utils/report-runtime/report-runtime.d.ts +14 -5
  100. package/dist/utils/report-values/report-values.d.ts +16 -4
  101. package/dist/utils/report-values/report-values.js +2 -2
  102. package/dist/utils/request-change/request-change.d.ts +11 -0
  103. package/dist/utils/request-change/request-change.js +10 -0
  104. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +17 -0
  105. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +1 -0
  106. package/dist/utils/slot-filled/slot-filled.d.ts +8 -0
  107. package/dist/utils/slot-filled/slot-filled.js +1 -0
  108. package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
  109. package/dist/utils/slotted-words/slotted-words.js +3 -1
  110. package/dist/utils/toolbar-around/toolbar-around.d.ts +18 -0
  111. package/dist/utils/toolbar-around/toolbar-around.js +5 -0
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/dist/words/index.d.ts +3 -0
  115. package/dist/words/index.js +3 -0
  116. package/dist/words/message.d.ts +47 -0
  117. package/dist/words/message.js +152 -0
  118. package/dist/words/registry.d.ts +89 -0
  119. package/dist/words/registry.js +101 -0
  120. package/dist/words/words-locale.d.ts +34 -0
  121. package/dist/words/words-locale.js +30 -0
  122. package/dist/words/words.d.ts +52 -0
  123. package/dist/words/words.js +100 -0
  124. package/package.json +40 -25
@@ -0,0 +1,60 @@
1
+ /** One option of a list fed as data, as the list reads and marks it. */
2
+ export interface DataOption {
3
+ /** Whether the keyboard is on it. The list writes this. */
4
+ active: boolean;
5
+ /** Whether it can be chosen. */
6
+ disabled: boolean;
7
+ /** Whether a filter hides it. The list writes this. */
8
+ hidden: boolean;
9
+ /** Whether it is chosen. The list writes this. */
10
+ selected: boolean;
11
+ /** The words a reader sees, which the filter and the typeahead match. */
12
+ text: string;
13
+ /** What choosing it puts in the value. */
14
+ value: string;
15
+ }
16
+ /**
17
+ * An option as a page hands it over: its value alone, which is also its words,
18
+ * or a record with a `value`, the `label` a reader sees (the value where it is
19
+ * left out) and `disabled`.
20
+ */
21
+ export type DataOptionItem = number | string | {
22
+ disabled?: boolean;
23
+ label?: string;
24
+ value: number | string;
25
+ };
26
+ /**
27
+ * The options a page handed over as data, as a list reads them, built once per
28
+ * array.
29
+ *
30
+ * Values and words are strings, as an option element's are, so a number and
31
+ * its digits choose the same option.
32
+ */
33
+ export declare const dataOptions: (items: readonly DataOptionItem[]) => DataOption[];
34
+ /**
35
+ * The positions a window draws, in order, with `kept` added where it lies
36
+ * outside the window: before it where it comes first, after it where it comes
37
+ * later. `-1` keeps nothing. Positions past `count` are left out.
38
+ *
39
+ * `kept` is the option the keyboard is on. **It has to be drawn wherever it
40
+ * is**, because `aria-activedescendant` points at an element, and an option
41
+ * that is not drawn is one a screen reader cannot be told about.
42
+ * `VirtualizeController.spacers(kept)` takes its room out of the spacer on its
43
+ * side, so the scrollbar still measures the set.
44
+ */
45
+ export declare const windowOf: (first: number, last: number, count: number, kept: number) => number[];
46
+ /**
47
+ * Draws `rows[index]` for each of `at` into `box`, as elements named `tag`,
48
+ * and returns them in order.
49
+ *
50
+ * **Reused by position.** A scroll moves the window by a row or two at a time,
51
+ * so the elements already drawn take the next rows' words and state rather than
52
+ * being thrown away: a list of ten thousand keeps the twenty or so elements a
53
+ * screen holds.
54
+ *
55
+ * Each element carries its place in the whole set, `aria-posinset` and
56
+ * `aria-setsize`, so a reader on the four-thousandth of ten thousand hears
57
+ * that, and not its place in what happens to be drawn. `disabled` holds every
58
+ * row down, for a list that is disabled itself.
59
+ */
60
+ export declare const drawOptions: (box: Element, tag: string, rows: readonly DataOption[], at: readonly number[], disabled: boolean) => HTMLElement[];
@@ -0,0 +1,46 @@
1
+ export const dataOptions = (items) => items.map((item) => {
2
+ const record = typeof item === 'object' ? item : { value: item };
3
+ return {
4
+ active: false,
5
+ disabled: Boolean(record.disabled),
6
+ hidden: false,
7
+ selected: false,
8
+ text: String(record.label ?? record.value),
9
+ value: String(record.value),
10
+ };
11
+ });
12
+ export const windowOf = (first, last, count, kept) => {
13
+ const at = [];
14
+ for (let index = first; index < Math.min(last, count); index += 1) {
15
+ at.push(index);
16
+ }
17
+ if (kept >= 0 && kept < count && !at.includes(kept)) {
18
+ kept < first ? at.unshift(kept) : at.push(kept);
19
+ }
20
+ return at;
21
+ };
22
+ export const drawOptions = (box, tag, rows, at, disabled) => {
23
+ const drawn = [...box.children];
24
+ while (drawn.length > at.length) {
25
+ drawn.pop()?.remove();
26
+ }
27
+ while (drawn.length < at.length) {
28
+ drawn.push(box.appendChild(box.ownerDocument.createElement(tag)));
29
+ }
30
+ at.forEach((index, i) => {
31
+ const row = rows[index];
32
+ const element = drawn[i];
33
+ Object.assign(element, {
34
+ active: row.active,
35
+ disabled: row.disabled || disabled,
36
+ selected: row.selected,
37
+ value: row.value,
38
+ });
39
+ if (element.textContent !== row.text) {
40
+ element.textContent = row.text;
41
+ }
42
+ element.setAttribute('aria-posinset', String(index + 1));
43
+ element.setAttribute('aria-setsize', String(rows.length));
44
+ });
45
+ return drawn;
46
+ };
@@ -26,7 +26,8 @@ export interface SegmentOptions {
26
26
  * between them, read off `Intl`'s own rendering of a sample.
27
27
  *
28
28
  * `en-US` is month, day, year with slashes; `de-DE` day, month, year with
29
- * dots; `ja-JP` year, month, day. A time follows the clock: a twelve hour
29
+ * dots; `ja-JP` year, month, day; the `iso8601` calendar year, month, day
30
+ * with hyphens. A time follows the clock: a twelve hour
30
31
  * locale ends with the half of the day.
31
32
  */
32
33
  export declare const segmentLayout: (kind: SegmentedKind, locale: string, options?: SegmentOptions) => SegmentSlot[];
@@ -14,6 +14,16 @@ export const segmentLayout = (kind, locale, options = {}) => {
14
14
  ...(options.seconds ? { second: '2-digit' } : {}),
15
15
  ...(options.hourCycle ? { hourCycle: options.hourCycle } : {}),
16
16
  });
17
+ if (kind === 'date' &&
18
+ (/-ca-iso8601/i.test(locale) || format.resolvedOptions().calendar === 'iso8601')) {
19
+ return [
20
+ { kind: 'year' },
21
+ { literal: '-' },
22
+ { kind: 'month' },
23
+ { literal: '-' },
24
+ { kind: 'day' },
25
+ ];
26
+ }
17
27
  const known = new Set(['day', 'dayPeriod', 'hour', 'minute', 'month', 'second', 'year']);
18
28
  const slots = [];
19
29
  for (const part of format.formatToParts(SAMPLE)) {
@@ -44,9 +44,14 @@ export interface TooltipAt {
44
44
  /**
45
45
  * What the keyboard is reading, which `drawTooltip` says aloud and marks.
46
46
  *
47
- * The chart's `pointer` is one, and is what every chart passes.
47
+ * The chart's `pointer` is one, and is what every chart passes. A chart whose
48
+ * readings can be chosen passes one with `state` as well, a word such as
49
+ * "selected" said after where the reading stands.
48
50
  */
49
- export type ReadingCursor = Pick<ChartPointerController, 'column' | 'keyed'> & Partial<Pick<ChartPointerController, 'position' | 'shows'>>;
51
+ export type ReadingCursor = Pick<ChartPointerController, 'column' | 'keyed'> & Partial<Pick<ChartPointerController, 'position' | 'shows'>> & {
52
+ /** A word said after the reading's position, such as "selected", or nothing. */
53
+ state?: string;
54
+ };
50
55
  /**
51
56
  * What a pointer resting on a reading shows, as `Tooltip` pointed at a mark.
52
57
  *
@@ -11,7 +11,7 @@ export const drawTooltip = (rows, at, tag, where, slotOf, reading) => {
11
11
  const live = reading === undefined || rows.length === 0
12
12
  ? nothing
13
13
  : html `<span aria-live="polite" part="live" role="status">${reading.keyed && row
14
- ? spokenReading(row, reading.column, (cell) => reading.shows?.(cell) !== false, reading.position)
14
+ ? spokenReading(row, reading.column, (cell) => reading.shows?.(cell) !== false, reading.position, reading.state)
15
15
  : ''}</span>`;
16
16
  if (!row || rows.length === 0) {
17
17
  return live;
@@ -9,6 +9,7 @@ import type { Row } from '../read-series/read-series.js';
9
9
  * false for, which is a series a legend hid.
10
10
  *
11
11
  * `position` follows in brackets, such as "2 of 12", where the chart gives
12
- * one: last, so the reading is heard before where it stands.
12
+ * one: last, so the reading is heard before where it stands. `state`, a word
13
+ * such as "selected", follows it in the same brackets, "2 of 12, selected".
13
14
  */
14
- export declare const spokenReading: (row: Row, column?: number, shows?: (cell: number) => boolean, position?: string) => string;
15
+ export declare const spokenReading: (row: Row, column?: number, shows?: (cell: number) => boolean, position?: string, state?: string) => string;
@@ -1,5 +1,6 @@
1
- export const spokenReading = (row, column, shows = () => true, position = '') => {
2
- const after = position === '' ? '' : ` (${position})`;
1
+ export const spokenReading = (row, column, shows = () => true, position = '', state = '') => {
2
+ const bracket = [position, state].filter((word) => word !== '').join(', ');
3
+ const after = bracket === '' ? '' : ` (${bracket})`;
3
4
  const cell = column === undefined ? undefined : row.cells[column];
4
5
  if (cell) {
5
6
  return `${`${row.label}, ${cell.name} ${cell.text}`.trim()}${after}`;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * An `exportparts` list that forwards each of an inner component's parts under
3
+ * a prefix: `exportParts(['button', 'text'], 'copy')` is
4
+ * `button: copy-button, text: copy-text`.
5
+ *
6
+ * **One list per inner component, written once beside it**, so the next part
7
+ * Button gains reaches every family that renders a Button. Ten lists written
8
+ * out by hand each had to be found and edited when Button gained `spinner`.
9
+ *
10
+ * The inner component's parts are its own declared list, never ones read from
11
+ * a rendered element: the attribute is written on the server too, before
12
+ * anything has rendered.
13
+ */
14
+ export declare const exportParts: (parts: readonly string[], prefix: string) => string;
@@ -0,0 +1 @@
1
+ export const exportParts = (parts, prefix) => parts.map((part) => `${part}: ${prefix}-${part}`).join(', ');
@@ -11,3 +11,12 @@
11
11
  * shows up on the page as the mistake it is.
12
12
  */
13
13
  export declare const fillTemplate: (template: string, values: Record<string, string | number>) => string;
14
+ /**
15
+ * A name made from a template and the words of what it acts on: `{key}` in
16
+ * the template filled with the words, or the words after the template where it
17
+ * has no `{key}`.
18
+ *
19
+ * The shape of every "Remove {label}" a page can override: written either way,
20
+ * "Remove" alone still names which one, as "Remove Draft".
21
+ */
22
+ export declare const labelWith: (template: string, key: string, words: string) => string;
@@ -1 +1,4 @@
1
1
  export const fillTemplate = (template, values) => Object.entries(values).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, () => String(value)), template);
2
+ export const labelWith = (template, key, words) => template.includes(`{${key}}`)
3
+ ? fillTemplate(template, { [key]: words })
4
+ : `${template} ${words}`.trim();
@@ -0,0 +1,36 @@
1
+ /** What the marks are called in `CSS.highlights`, and in `base.css`. */
2
+ export declare const FILTER_MARK = "helix-filter-match";
3
+ /**
4
+ * Where a needle sits inside an element, as a range over its text nodes, or
5
+ * nothing where it does not.
6
+ *
7
+ * Walked rather than measured off `textContent`, because the words may be split
8
+ * across several nodes and wrapped in elements of the consumer's, and a match
9
+ * can begin in one node and end in another. The offsets are mapped back onto
10
+ * the nodes they came from, so the range covers the real text either way.
11
+ * Matched without regard to case, as a filter matches.
12
+ */
13
+ export declare const rangeOf: (root: Element, needle: string) => Range[];
14
+ /**
15
+ * Marks where `needle` matches in each of `elements`, for `owner`, without
16
+ * touching the elements. An empty needle takes the owner's marks away, which
17
+ * is also what an owner leaving the page calls.
18
+ *
19
+ * **The Custom Highlight API paints a range, and nothing is inserted.** The
20
+ * words are usually the consumer's own light DOM, slotted through as many as
21
+ * two shadow roots, and wrapping the matched part in a `<mark>` would mean
22
+ * rewriting their content: markup they wrote would come back changed. So an
23
+ * option holding a link or an icon is marked as readily as one holding a bare
24
+ * word. Listbox marks what its filter matched this way, and so every family
25
+ * that filters through one: Select, Combobox, Command and Time Picker.
26
+ *
27
+ * **The rule that paints it lives in `base.css`**, as
28
+ * `::highlight(helix-filter-match)`, rather than in a component's styles. A
29
+ * highlight is styled from the tree its text lives in, and that text lives in
30
+ * the consumer's document.
31
+ *
32
+ * Nothing at all where the API is missing, as on a server, in a test
33
+ * environment or in an older browser: the text nodes are not walked for an
34
+ * answer nothing can paint.
35
+ */
36
+ export declare const markMatches: (owner: Element, elements: Iterable<Element>, needle: string) => void;
@@ -0,0 +1,41 @@
1
+ export const FILTER_MARK = 'helix-filter-match';
2
+ const matched = new Map();
3
+ export const rangeOf = (root, needle) => {
4
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
5
+ const nodes = [];
6
+ let whole = '';
7
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
8
+ nodes.push([node, whole.length]);
9
+ whole += node.nodeValue;
10
+ }
11
+ const at = whole.toLowerCase().indexOf(needle.toLowerCase());
12
+ if (at < 0 || needle === '') {
13
+ return [];
14
+ }
15
+ const range = root.ownerDocument.createRange();
16
+ const [start, before] = nodes.findLast(([, from]) => from <= at);
17
+ range.setStart(start, at - before);
18
+ const end = at + needle.length;
19
+ const [last, under] = nodes.findLast(([, from]) => from < end);
20
+ range.setEnd(last, end - under);
21
+ return [range];
22
+ };
23
+ export const markMatches = (owner, elements, needle) => {
24
+ const registry = globalThis.CSS?.highlights;
25
+ if (registry === undefined) {
26
+ return;
27
+ }
28
+ if (needle === '') {
29
+ matched.delete(owner);
30
+ }
31
+ else {
32
+ matched.set(owner, [...elements].flatMap((element) => rangeOf(element, needle)));
33
+ }
34
+ const all = [...matched.values()].flat();
35
+ if (all.length === 0) {
36
+ registry.delete(FILTER_MARK);
37
+ }
38
+ else {
39
+ registry.set(FILTER_MARK, new Highlight(...all));
40
+ }
41
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The element focus lands on when a trigger is focused: the trigger itself,
3
+ * or, for a component that delegates focus, the control inside its shadow root,
4
+ * followed down through any component nested there.
5
+ *
6
+ * **What a description or a name has to be written on**, since a reader hears
7
+ * the element that has focus: a tooltip describing a Helix button describes
8
+ * the `<button>` inside it, and a badge counting on an icon button counts on
9
+ * the same one. Read from outside through `shadowRoot`, so it works for any
10
+ * component with an open root and names none.
11
+ */
12
+ export declare const focusTarget: (trigger: Element) => Element;
@@ -0,0 +1,9 @@
1
+ const CONTROL = 'a[href], button, input, select, textarea, [contenteditable], [tabindex]:not([tabindex="-1"])';
2
+ export const focusTarget = (trigger) => {
3
+ const root = trigger.shadowRoot;
4
+ if (!root?.delegatesFocus) {
5
+ return trigger;
6
+ }
7
+ const inner = root.querySelector(CONTROL);
8
+ return inner ? focusTarget(inner) : trigger;
9
+ };
@@ -1,3 +1,20 @@
1
+ /**
2
+ * What takes a press of its own inside a row or an item that is pressable as a
3
+ * whole: a link, a control, a label, a disclosure's summary, or anything a page
4
+ * gave a `tabindex`.
5
+ */
6
+ export declare const PRESSABLE = "a[href], button, input, label, select, summary, textarea, [tabindex]";
7
+ /**
8
+ * Whether a press landed on a control of the item's own, before `boundary` in
9
+ * its path, so the item leaves the press to that control.
10
+ *
11
+ * **The composed path, so a control inside a component counts**: the `<button>`
12
+ * in a slotted Helix button's shadow root is on the path and matches, where the
13
+ * event's retargeted `target` is only the host. The boundary itself and
14
+ * everything past it is not asked, so a row that is a `[tabindex]` stop does
15
+ * not count as its own control.
16
+ */
17
+ export declare const pressedOwnControl: (event: Event, boundary: EventTarget, pressable?: string) => boolean;
1
18
  /**
2
19
  * Everything inside `root` the reader can reach with Tab, in document order.
3
20
  *
@@ -7,6 +7,14 @@ const NATIVE_FOCUSABLE = [
7
7
  '[contenteditable]:not([contenteditable="false"])',
8
8
  '[tabindex]:not([tabindex="-1"])',
9
9
  ].join(',');
10
+ export const PRESSABLE = 'a[href], button, input, label, select, summary, textarea, [tabindex]';
11
+ export const pressedOwnControl = (event, boundary, pressable = PRESSABLE) => {
12
+ const path = event.composedPath();
13
+ const end = path.indexOf(boundary);
14
+ return path
15
+ .slice(0, end < 0 ? 0 : end)
16
+ .some((node) => node instanceof Element && node.matches(pressable));
17
+ };
10
18
  export const focusableElements = (root) => [...root.querySelectorAll('*')].filter((node) => (node.matches(NATIVE_FOCUSABLE) || node.shadowRoot?.delegatesFocus === true) &&
11
19
  node.getClientRects().length > 0);
12
20
  export const deepActiveElement = () => {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where a key moves focus in a table read as a grid, as arithmetic over places.
3
+ *
4
+ * Pure, so a spec holds the keyboard map without a page, and in core because
5
+ * Table's `grid` and Data Table both read it: each finds its cells and focuses
6
+ * them, and this says which place a key reaches from which.
7
+ */
8
+ /**
9
+ * A cell's place in the grid. The row of headings is row `-1`, the rows count
10
+ * from 0 in the order they are drawn, and a `selectable` table's checkbox
11
+ * column is column 0.
12
+ */
13
+ export interface GridPlace {
14
+ column: number;
15
+ row: number;
16
+ }
17
+ /** The edges of the grid a key cannot move past. */
18
+ export interface GridBounds {
19
+ /** The last row, counted as `GridPlace.row` counts. */
20
+ last: number;
21
+ /** The first row: `-1` where there is a row of headings, and `0` where there is not. */
22
+ top: number;
23
+ /** How many columns the grid has. */
24
+ width: number;
25
+ }
26
+ /** What a key reads besides itself. */
27
+ export interface GridKeys {
28
+ /** Whether Control or Command was held, which takes Home and End to the corners. */
29
+ control: boolean;
30
+ /** How many rows Page Up and Page Down move. */
31
+ page: number;
32
+ /**
33
+ * Which way a horizontal arrow moves along the line: `1` forward, `-1` back,
34
+ * as core `inlineStep` answers, so a right-to-left page is already flipped.
35
+ */
36
+ step: -1 | 0 | 1;
37
+ }
38
+ /**
39
+ * The place a key reaches from `from`, or `undefined` for a key that does not
40
+ * move focus. A key at an edge answers the place it is on, so the caller still
41
+ * takes the key.
42
+ *
43
+ * | Key | Place |
44
+ * |---|---|
45
+ * | Up, Down | One row, the headings included. |
46
+ * | Left, Right | One column along the line, as `step` says. |
47
+ * | Home, End | The first or the last column of the row. |
48
+ * | Control with Home or End | The first column of the first row, or the last of the last. |
49
+ * | Page Up, Page Down | `page` rows. |
50
+ */
51
+ export declare const gridMove: (key: string, from: GridPlace, bounds: GridBounds, keys: GridKeys) => GridPlace | undefined;
52
+ /**
53
+ * How many rows Page Up and Page Down move: the rows of `tall` pixels that fit
54
+ * in `room`, less one, so the row focus left stays in view. At least one.
55
+ */
56
+ export declare const pageRows: (room: number, tall: number) => number;
@@ -0,0 +1,28 @@
1
+ const within = (value, low, high) => Math.max(low, Math.min(high, value));
2
+ export const gridMove = (key, from, bounds, keys) => {
3
+ const { last, top, width } = bounds;
4
+ const edge = Math.max(0, width - 1);
5
+ const row = (to) => ({ column: from.column, row: within(to, top, last) });
6
+ switch (key) {
7
+ case 'ArrowUp':
8
+ return row(from.row - 1);
9
+ case 'ArrowDown':
10
+ return row(from.row + 1);
11
+ case 'ArrowLeft':
12
+ case 'ArrowRight':
13
+ return keys.step === 0
14
+ ? undefined
15
+ : { column: within(from.column + keys.step, 0, edge), row: from.row };
16
+ case 'Home':
17
+ return { column: 0, row: keys.control ? top : from.row };
18
+ case 'End':
19
+ return { column: edge, row: keys.control ? last : from.row };
20
+ case 'PageUp':
21
+ return row(from.row - keys.page);
22
+ case 'PageDown':
23
+ return row(from.row + keys.page);
24
+ default:
25
+ return undefined;
26
+ }
27
+ };
28
+ export const pageRows = (room, tall) => tall > 0 ? Math.max(1, Math.floor(room / tall) - 1) : 1;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * The runtime's own locale: in a browser, the language its interface is set
3
+ * to, which is what every `Intl` call given no locale uses.
4
+ */
5
+ export declare const runtimeLocale: () => string;
1
6
  /**
2
7
  * The locale an element speaks: `own`, the element's own `locale` property,
3
8
  * where one is set, and otherwise the `lang` of the nearest element with one,
@@ -1,9 +1,15 @@
1
1
  let runtime;
2
+ export const runtimeLocale = () => {
3
+ runtime ??= new Intl.Locale(new Intl.DateTimeFormat().resolvedOptions().locale);
4
+ return runtime.toString();
5
+ };
2
6
  const usable = (lang) => {
3
7
  try {
4
8
  const asked = new Intl.Locale(lang);
5
- runtime ??= new Intl.Locale(new Intl.DateTimeFormat().resolvedOptions().locale);
6
- return asked.region === undefined && asked.language === runtime.language ? '' : asked.toString();
9
+ runtimeLocale();
10
+ return asked.region === undefined && asked.language === runtime?.language
11
+ ? ''
12
+ : asked.toString();
7
13
  }
8
14
  catch {
9
15
  return '';
@@ -9,6 +9,13 @@
9
9
  */
10
10
  /** Writes a number in a locale with `Intl.NumberFormat` options. */
11
11
  export declare const formatNumber: (value: number, locale?: string, options?: Intl.NumberFormatOptions) => string;
12
+ /**
13
+ * A share, from 0 to 1, as a locale writes a whole percentage: `40%`, or
14
+ * `40 %` in French. For `aria-valuetext`, since a bare `aria-valuenow` is read
15
+ * as a number with no unit. Splitter's position and Meter's and Progress's
16
+ * readings are written with it.
17
+ */
18
+ export declare const formatPercent: (share: number, locale?: string) => string;
12
19
  /**
13
20
  * A reading in words: `value` written with `options` where they say another
14
21
  * style, and otherwise `share`, from 0 to 1, as a percentage, whole unless
@@ -10,6 +10,7 @@ const formatter = (locale, options) => {
10
10
  };
11
11
  export const formatNumber = (value, locale = '', options) => formatter(locale, options).format(value);
12
12
  const WHOLE_PERCENT = { maximumFractionDigits: 0, style: 'percent' };
13
+ export const formatPercent = (share, locale = '') => formatNumber(share, locale, WHOLE_PERCENT);
13
14
  export const readingText = (value, share, locale = '', options) => options && options.style !== 'percent'
14
15
  ? formatNumber(value, locale, options)
15
16
  : formatNumber(share, locale, options ?? WHOLE_PERCENT);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The copy of an item a pointer drags: what the item draws, through every
3
+ * shadow root and slot in it, rebuilt as plain elements with the styles the
4
+ * browser computed for each written on it.
5
+ *
6
+ * `ReorderController` puts the copy in the host's own shadow root, in the top
7
+ * layer, so the page's markup is never touched and no scrolling box clips it.
8
+ * A copy is inert: it has no custom elements, no slots, and no attributes but
9
+ * a picture's, so nothing in it upgrades, takes the host's children, or joins
10
+ * a radio group. Pseudo-elements are not copied.
11
+ */
12
+ import { nothing, type TemplateResult } from 'lit';
13
+ /**
14
+ * The part a host draws for the drag preview: an empty manual popover,
15
+ * hidden from assistive technology, which `ReorderController` fills with a
16
+ * copy of the item a pointer drags and shows in the top layer. Closed, it
17
+ * takes no room. Drawn while `on`, which is while the host reorders.
18
+ */
19
+ export declare const drawDragPreview: (on: boolean) => TemplateResult | typeof nothing;
20
+ /** A point in the viewport, where the boxes of a copy are measured from. */
21
+ interface Origin {
22
+ x: number;
23
+ y: number;
24
+ }
25
+ /**
26
+ * The box an element draws: its own, or the boxes of what it draws together,
27
+ * for an element that is `display: contents`, such as a row of a grid.
28
+ */
29
+ export declare const boxOf: (element: Element) => DOMRect;
30
+ /**
31
+ * A copy of what a node draws, for a drag preview, with every style the
32
+ * browser computed for each part written on it. The custom properties are
33
+ * left out, having done their work, and so are the logical properties, whose
34
+ * physical twins are written: two of a pair in one style do not agree on
35
+ * which wins.
36
+ *
37
+ * A `placed` node is put where it is with `position: absolute`, measured from
38
+ * `origin`, the padding box it is placed in, rather than laid out again. The
39
+ * root of a preview is placed in the preview, whose padding box is the root's
40
+ * box as `boxOf` measures it, and so is whatever a grid lays out, since a
41
+ * subgrid has no tracks of its own once it is copied out of its grid. An
42
+ * element that is `display: contents`, such as a row whose cells sit in the
43
+ * grid of its table, has each of its cells put where it is.
44
+ */
45
+ export declare const dragPreviewOf: (node: Node, placed: boolean, origin: Origin) => Node[];
46
+ export {};
@@ -0,0 +1,58 @@
1
+ import { html, nothing } from 'lit';
2
+ export const drawDragPreview = (on) => on ? html `<div part="drag-preview" popover="manual" aria-hidden="true" inert></div>` : nothing;
3
+ const drawn = (node) => {
4
+ const assigned = node instanceof HTMLSlotElement ? node.assignedNodes({ flatten: true }) : [];
5
+ return [...(assigned.length ? assigned : (node.shadowRoot ?? node).childNodes)];
6
+ };
7
+ export const boxOf = (element) => {
8
+ if (getComputedStyle(element).display !== 'contents') {
9
+ return element.getBoundingClientRect();
10
+ }
11
+ let [left, top, right, bottom] = [Infinity, Infinity, -Infinity, -Infinity];
12
+ for (const node of drawn(element)) {
13
+ const box = node instanceof Element && boxOf(node);
14
+ if (box && (box.width || box.height)) {
15
+ left = Math.min(left, box.left);
16
+ top = Math.min(top, box.top);
17
+ right = Math.max(right, box.right);
18
+ bottom = Math.max(bottom, box.bottom);
19
+ }
20
+ }
21
+ return left <= right ? new DOMRect(left, top, right - left, bottom - top) : new DOMRect();
22
+ };
23
+ export const dragPreviewOf = (node, placed, origin) => {
24
+ if (node instanceof Text) {
25
+ return [node.cloneNode()];
26
+ }
27
+ const style = node instanceof Element ? getComputedStyle(node) : undefined;
28
+ if (!style || style.display === 'none') {
29
+ return [];
30
+ }
31
+ const element = node;
32
+ const tag = element.localName;
33
+ const whole = /^(img|svg)$/.test(tag);
34
+ const copy = whole
35
+ ? element.cloneNode(true)
36
+ : document.createElement(/-|^slot$/.test(tag) ? 'div' : tag);
37
+ const contents = style.display === 'contents';
38
+ const box = element.getBoundingClientRect();
39
+ let written = '';
40
+ for (let index = 0; index < style.length; index += 1) {
41
+ const name = style.item(index);
42
+ if (!/^--|block|inline|start-|end-/.test(name)) {
43
+ written += `${name}:${style.getPropertyValue(name)};`;
44
+ }
45
+ }
46
+ copy.style.cssText =
47
+ placed && !contents
48
+ ? `${written}position:absolute;inset:auto;left:${box.left - origin.x}px;top:${box.top - origin.y}px;width:${box.width}px;height:${box.height}px;margin:0;box-sizing:border-box;grid-area:auto`
49
+ : written;
50
+ if (!whole) {
51
+ const inner = !contents && (placed || style.position !== 'static')
52
+ ? { x: box.left + element.clientLeft, y: box.top + element.clientTop }
53
+ : origin;
54
+ const grid = contents ? placed : style.display.endsWith('grid');
55
+ copy.append(...drawn(element).flatMap((child) => dragPreviewOf(child, grid, inner)));
56
+ }
57
+ return [copy];
58
+ };