@fusion.dev/helix-core 0.146.0 → 0.148.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 (132) hide show
  1. package/custom-elements.json +3129 -282
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -3
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +6 -0
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +13 -0
  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/copy-controller/copy-controller.d.ts +4 -1
  8. package/dist/controllers/copy-controller/copy-controller.js +2 -1
  9. package/dist/controllers/countdown-controller/countdown-controller.d.ts +19 -2
  10. package/dist/controllers/countdown-controller/countdown-controller.js +30 -3
  11. package/dist/controllers/floating-controller/floating-controller.d.ts +12 -1
  12. package/dist/controllers/floating-controller/floating-controller.js +2 -1
  13. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +30 -0
  14. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +8 -0
  15. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +10 -0
  16. package/dist/controllers/open-phase-controller/open-phase-controller.js +7 -4
  17. package/dist/controllers/option-window-controller/option-window-controller.d.ts +96 -0
  18. package/dist/controllers/option-window-controller/option-window-controller.js +81 -0
  19. package/dist/controllers/popover-controller/popover-controller.js +1 -1
  20. package/dist/controllers/source-controller/source-controller.js +2 -0
  21. package/dist/controllers/swipe-controller/swipe-controller.d.ts +10 -1
  22. package/dist/controllers/swipe-controller/swipe-controller.js +1 -1
  23. package/dist/controllers/virtualize-controller/virtualize-controller.js +8 -6
  24. package/dist/events/band.d.ts +13 -0
  25. package/dist/events/band.js +11 -0
  26. package/dist/events/details.d.ts +5 -0
  27. package/dist/events/events.d.ts +4 -2
  28. package/dist/events/events.js +3 -1
  29. package/dist/events/names.d.ts +3 -0
  30. package/dist/events/names.js +13 -88
  31. package/dist/helix-element/helix-element.d.ts +14 -5
  32. package/dist/helix-element/helix-element.js +13 -45
  33. package/dist/helix-element/react-bridge.d.ts +13 -0
  34. package/dist/helix-element/react-bridge.js +1 -1
  35. package/dist/index.d.ts +40 -17
  36. package/dist/index.js +38 -15
  37. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +12 -0
  38. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +4 -0
  39. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +25 -4
  40. package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.d.ts +42 -0
  41. package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.js +46 -0
  42. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +2 -11
  43. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +1 -6
  44. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +1 -1
  45. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +10 -6
  46. package/dist/mixins/mixin-viewport/mixin-viewport.js +8 -7
  47. package/dist/utils/assign-slots/assign-slots.d.ts +18 -0
  48. package/dist/utils/assign-slots/assign-slots.js +14 -0
  49. package/dist/utils/audit-name/audit-name.d.ts +7 -2
  50. package/dist/utils/audit-name/audit-name.js +12 -3
  51. package/dist/utils/audit-source/audit-source-off.d.ts +10 -0
  52. package/dist/utils/audit-source/audit-source-off.js +1 -0
  53. package/dist/utils/audit-source/audit-source.d.ts +19 -0
  54. package/dist/utils/audit-source/audit-source.js +27 -0
  55. package/dist/utils/autofill-select/autofill-select.d.ts +40 -0
  56. package/dist/utils/autofill-select/autofill-select.js +14 -0
  57. package/dist/utils/call-listener/call-listener.d.ts +16 -0
  58. package/dist/utils/call-listener/call-listener.js +17 -0
  59. package/dist/utils/center-in-view/center-in-view.d.ts +18 -0
  60. package/dist/utils/center-in-view/center-in-view.js +14 -0
  61. package/dist/utils/chord/chord.d.ts +14 -0
  62. package/dist/utils/chord/chord.js +1 -0
  63. package/dist/utils/chord/key-names.d.ts +30 -0
  64. package/dist/utils/chord/key-names.js +52 -0
  65. package/dist/utils/clamp/clamp.d.ts +7 -0
  66. package/dist/utils/clamp/clamp.js +1 -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/css-color/css-color.js +1 -1
  70. package/dist/utils/data-options/data-options.d.ts +60 -0
  71. package/dist/utils/data-options/data-options.js +46 -0
  72. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +7 -2
  73. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -1
  74. package/dist/utils/draw-tooltip/spoken-reading.d.ts +3 -2
  75. package/dist/utils/draw-tooltip/spoken-reading.js +3 -2
  76. package/dist/utils/elapsed-time/elapsed-time.d.ts +1 -1
  77. package/dist/utils/elapsed-time/elapsed-time.js +2 -5
  78. package/dist/utils/export-parts/export-parts.d.ts +14 -0
  79. package/dist/utils/export-parts/export-parts.js +1 -0
  80. package/dist/utils/fill-template/fill-template.d.ts +9 -0
  81. package/dist/utils/fill-template/fill-template.js +3 -0
  82. package/dist/utils/filter-marks/filter-marks.d.ts +36 -0
  83. package/dist/utils/filter-marks/filter-marks.js +41 -0
  84. package/dist/utils/focus-target/focus-target.d.ts +12 -0
  85. package/dist/utils/focus-target/focus-target.js +9 -0
  86. package/dist/utils/focusable-elements/focusable-elements.d.ts +17 -0
  87. package/dist/utils/focusable-elements/focusable-elements.js +8 -0
  88. package/dist/utils/grid-move/grid-move.d.ts +56 -0
  89. package/dist/utils/grid-move/grid-move.js +28 -0
  90. package/dist/utils/host-words/host-words.d.ts +16 -0
  91. package/dist/utils/host-words/host-words.js +13 -0
  92. package/dist/utils/load-more/load-more.d.ts +25 -0
  93. package/dist/utils/load-more/load-more.js +1 -0
  94. package/dist/utils/locale-of/locale-of.d.ts +18 -0
  95. package/dist/utils/locale-of/locale-of.js +26 -0
  96. package/dist/utils/nearest/nearest.d.ts +9 -0
  97. package/dist/utils/nearest/nearest.js +9 -0
  98. package/dist/utils/number-format/number-format.d.ts +16 -0
  99. package/dist/utils/number-format/number-format.js +5 -0
  100. package/dist/utils/own-rule/own-rule.d.ts +16 -0
  101. package/dist/utils/own-rule/own-rule.js +7 -0
  102. package/dist/utils/platform/platform.d.ts +8 -0
  103. package/dist/utils/platform/platform.js +1 -0
  104. package/dist/utils/read-series/read-series.d.ts +14 -0
  105. package/dist/utils/read-series/read-series.js +15 -0
  106. package/dist/utils/relative-time/relative-time.d.ts +8 -0
  107. package/dist/utils/relative-time/relative-time.js +5 -2
  108. package/dist/utils/report-render/report-render.js +2 -14
  109. package/dist/utils/report-runtime/report-runtime.d.ts +11 -1
  110. package/dist/utils/report-runtime/report-runtime.js +2 -14
  111. package/dist/utils/report-usage/report-usage.js +2 -14
  112. package/dist/utils/report-values/report-values.d.ts +16 -4
  113. package/dist/utils/report-values/report-values.js +2 -2
  114. package/dist/utils/request-change/request-change.d.ts +11 -0
  115. package/dist/utils/request-change/request-change.js +10 -0
  116. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +50 -0
  117. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +14 -3
  118. package/dist/utils/slot-filled/slot-filled.d.ts +8 -0
  119. package/dist/utils/slot-filled/slot-filled.js +1 -0
  120. package/dist/utils/slotted-glyph/slotted-glyph.d.ts +33 -0
  121. package/dist/utils/slotted-glyph/slotted-glyph.js +27 -0
  122. package/dist/utils/toolbar-around/toolbar-around.d.ts +18 -0
  123. package/dist/utils/toolbar-around/toolbar-around.js +5 -0
  124. package/dist/utils/typeahead/typeahead.d.ts +8 -0
  125. package/dist/utils/typeahead/typeahead.js +1 -0
  126. package/dist/utils/verdict-flags/verdict-flags.d.ts +13 -0
  127. package/dist/utils/verdict-flags/verdict-flags.js +6 -0
  128. package/dist/utils/written-list/written-list.d.ts +31 -0
  129. package/dist/utils/written-list/written-list.js +16 -0
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/package.json +15 -4
@@ -0,0 +1,27 @@
1
+ import { reportRuntime } from '../report-runtime/report-runtime.js';
2
+ const audited = new WeakSet();
3
+ export const auditSource = (host, table) => {
4
+ if (host.for === '' || audited.has(host) || typeof document === 'undefined') {
5
+ return;
6
+ }
7
+ audited.add(host);
8
+ const read = () => {
9
+ setTimeout(() => {
10
+ if (host.isConnected && host.for !== '' && !table()) {
11
+ reportRuntime({
12
+ kind: 'missing-table',
13
+ message: `<${host.localName} for="${host.for}"> names no table, so it draws nothing. ` +
14
+ 'Point `for` at the id of a <table>, an element with role="table" or "grid", or a ' +
15
+ 'helix-table, in the same tree.',
16
+ tag: host.localName,
17
+ });
18
+ }
19
+ }, 0);
20
+ };
21
+ if (document.readyState === 'complete') {
22
+ read();
23
+ }
24
+ else {
25
+ window.addEventListener('load', read, { once: true });
26
+ }
27
+ };
@@ -0,0 +1,40 @@
1
+ import { nothing, type TemplateResult } from 'lit';
2
+ /** What the hidden native select offers a browser's autofill, and what it does with the answer. */
3
+ export interface DrawAutofillOptions {
4
+ /** The autofill token, such as `country` or `cc-exp-month`. Empty draws nothing. */
5
+ autocomplete: string;
6
+ /** Whether the select is disabled, which takes it out of autofill too. */
7
+ disabled: boolean;
8
+ /** Whether more than one option can be chosen. */
9
+ multiple: boolean;
10
+ /** Takes what the browser filled in, as a value: several are joined by commas. */
11
+ onFill: (value: string) => void;
12
+ /** The options, each with its value and its words, in order. */
13
+ options: readonly {
14
+ text: string;
15
+ value: string;
16
+ }[];
17
+ /** The value now, several joined by commas. */
18
+ value: string;
19
+ }
20
+ /**
21
+ * A native `<select>` that a browser's autofill can fill, holding the same
22
+ * options as the custom select beside it. Select, Combobox and Listbox draw it
23
+ * under `autocomplete`.
24
+ *
25
+ * **A browser fills native fields only.** It reads `autocomplete`, picks an
26
+ * option by its value or its words, and sends `change`, which `onFill` turns
27
+ * into the custom select's value. Chromium fills a select inside a shadow root
28
+ * when its host is inside the form being filled.
29
+ *
30
+ * **The select's own `input` stops here.** It is composed, so it crossed the
31
+ * shadow root and reached the host before `change` did, with the host's value
32
+ * still the old one: measured in Chromium, a fill sent a page's `input`
33
+ * listener two events, the first carrying the old value. The host sends its
34
+ * own `input` and `change` once the value is taken.
35
+ *
36
+ * **Hidden from everyone but the browser**: clipped to a pixel, `aria-hidden`,
37
+ * and out of the tab order, with no `name`, so the form still receives the
38
+ * value once, from the custom select itself.
39
+ */
40
+ export declare const drawAutofill: ({ autocomplete, disabled, multiple, onFill, options, value, }: DrawAutofillOptions) => TemplateResult | typeof nothing;
@@ -0,0 +1,14 @@
1
+ import { html, nothing } from 'lit';
2
+ const HIDDEN = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none';
3
+ export const drawAutofill = ({ autocomplete, disabled, multiple, onFill, options, value, }) => {
4
+ if (autocomplete === '') {
5
+ return nothing;
6
+ }
7
+ const chosen = value.split(',');
8
+ const fill = (event) => onFill([...event.currentTarget.selectedOptions]
9
+ .map((option) => option.value)
10
+ .filter(Boolean)
11
+ .join(','));
12
+ const stop = (event) => event.stopPropagation();
13
+ return html `<select aria-hidden="true" autocomplete=${autocomplete} style=${HIDDEN} tabindex="-1" ?disabled=${disabled} ?multiple=${multiple} @change=${fill} @input=${stop}><option value=""></option>${options.map((option) => html `<option value=${option.value} ?selected=${chosen.includes(option.value)}>${option.text}</option>`)}</select>`;
14
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What the three diagnostics slots share: a listener on `globalThis` under a
3
+ * registered symbol, called where there is one, whose failure is printed once.
4
+ *
5
+ * `reportRuntime`, `reportUsage` and `reportRender` each wrote this out, with
6
+ * a flag and a sentence of its own, and every element carries all three. Kept
7
+ * as one function, the sentence is one template and the flags one set.
8
+ */
9
+ /**
10
+ * Calls the function in `slot` with `args`, and answers what it returned.
11
+ *
12
+ * **A listener that throws never breaks the caller.** The error is printed
13
+ * once per `kind`, saying what happened instead (`after`), and the answer is
14
+ * `undefined`, as it is where nothing listens.
15
+ */
16
+ export declare const callListener: (slot: symbol, kind: string, after: string, ...args: unknown[]) => unknown;
@@ -0,0 +1,17 @@
1
+ const told = new Set();
2
+ export const callListener = (slot, kind, after, ...args) => {
3
+ const listener = globalThis[slot];
4
+ if (typeof listener !== 'function') {
5
+ return undefined;
6
+ }
7
+ try {
8
+ return listener(...args);
9
+ }
10
+ catch (error) {
11
+ if (!told.has(kind)) {
12
+ told.add(kind);
13
+ console.error(`helix: a ${kind} listener threw, and ${after}. Later failures are not printed.`, error);
14
+ }
15
+ return undefined;
16
+ }
17
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Scrolls `box` to put `item`, one of the things it scrolls, in its middle.
3
+ *
4
+ * For a list read as a dial rather than scanned, as Time Picker's wheels read
5
+ * a Listbox. A delta from the two boxes rather than `offsetTop`: a slotted
6
+ * item's `offsetParent` is whatever is positioned above it, which is not the
7
+ * box and is not guaranteed to be anything.
8
+ *
9
+ * **Twice, and the second pass is not superstition.** The first delta is
10
+ * computed against whatever layout is current when this is called, and inside
11
+ * a panel that has just been revealed it is a frame behind: measured, one call
12
+ * landed a whole row short, 672 where 704 was wanted, and calling it again
13
+ * from a settled layout put it exactly on center. Reading a rect forces
14
+ * layout, so the second pass sees the result of the first and corrects the
15
+ * remainder. It stops as soon as there is nothing left to correct, and where
16
+ * either box has no height, as on a page not laid out.
17
+ */
18
+ export declare const centerInView: (box: HTMLElement, item: Element) => void;
@@ -0,0 +1,14 @@
1
+ export const centerInView = (box, item) => {
2
+ for (let pass = 0; pass < 2; pass += 1) {
3
+ const outer = box.getBoundingClientRect();
4
+ const inner = item.getBoundingClientRect();
5
+ if (outer.height === 0 || inner.height === 0) {
6
+ return;
7
+ }
8
+ const off = inner.top + inner.height / 2 - (outer.top + outer.height / 2);
9
+ if (Math.abs(off) < 1) {
10
+ return;
11
+ }
12
+ box.scrollTop += off;
13
+ }
14
+ };
@@ -41,3 +41,17 @@ export interface ChordPress {
41
41
  * should go to whatever did want it.
42
42
  */
43
43
  export declare const matchesChord: (press: ChordPress, keys: string, apple: boolean) => boolean;
44
+ /**
45
+ * Whether a key press is Control+A, or Command+A on an Apple keyboard: the
46
+ * press that selects every item of a multiple selection.
47
+ *
48
+ * List, Tree, Listbox, Data Table and Virtual List each wrote the test. Either
49
+ * modifier counts on any platform, as they did, and Alt held is another
50
+ * shortcut.
51
+ */
52
+ export declare const selectsAll: (press: {
53
+ readonly altKey: boolean;
54
+ readonly ctrlKey: boolean;
55
+ readonly key: string;
56
+ readonly metaKey: boolean;
57
+ }) => boolean;
@@ -51,3 +51,4 @@ export const matchesChord = (press, keys, apple) => {
51
51
  (ctrl ? press.ctrlKey : !press.ctrlKey) &&
52
52
  (meta ? press.metaKey : !press.metaKey));
53
53
  };
54
+ export const selectsAll = (press) => (press.ctrlKey || press.metaKey) && !press.altKey && press.key.toLowerCase() === 'a';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The words and glyphs a chord is drawn with, per platform.
3
+ *
4
+ * In core beside `ariaChord`, which says a chord to a reader as this draws it
5
+ * for the eye: Kbd draws a chord with it, and Speed Dial draws an action's
6
+ * shortcut with it where nothing is slotted, without importing Kbd.
7
+ */
8
+ /** A chord's keys as they are shown, and what is shown between two of them. */
9
+ export interface KeyNames {
10
+ /** Each key's label, in the order the chord names them. */
11
+ keys: string[];
12
+ /** What goes between two keys: `+`, a hyphen, or nothing. */
13
+ separator: string;
14
+ }
15
+ /**
16
+ * The labels of a chord's keys on a platform, with `own` labels in place of
17
+ * the platform's, and what goes between two keys.
18
+ *
19
+ * `parts` are the chord's names as `chordParts` splits them. A name `own`
20
+ * holds takes that label, a name the platform knows takes its glyph or word,
21
+ * and any other single character is shown in capitals.
22
+ *
23
+ * **On an Apple keyboard glyphs run together and words are joined with a
24
+ * hyphen**, as Apple writes `Command-K`. A label of more than one character
25
+ * from `own` is a word, so `{"mod": "Command"}` shows `Command-K` there, where
26
+ * nothing between the two ran them into `CommandK`. The platform's own `Esc`
27
+ * and `Space` keep running on from a glyph, as `⌘Esc` always has. Everywhere
28
+ * else keys are joined with `+`.
29
+ */
30
+ export declare const keyNames: (parts: readonly string[], apple: boolean, own?: Readonly<Record<string, string>>) => KeyNames;
@@ -0,0 +1,52 @@
1
+ const APPLE = {
2
+ alt: '⌥',
3
+ backspace: '⌫',
4
+ cmd: '⌘',
5
+ ctrl: '⌃',
6
+ del: '⌦',
7
+ down: '↓',
8
+ enter: '↵',
9
+ esc: 'Esc',
10
+ left: '←',
11
+ meta: '⌘',
12
+ mod: '⌘',
13
+ opt: '⌥',
14
+ right: '→',
15
+ shift: '⇧',
16
+ space: 'Space',
17
+ tab: '⇥',
18
+ up: '↑',
19
+ };
20
+ const ELSEWHERE = {
21
+ alt: 'Alt',
22
+ backspace: 'Backspace',
23
+ cmd: 'Win',
24
+ ctrl: 'Ctrl',
25
+ del: 'Del',
26
+ down: '↓',
27
+ enter: 'Enter',
28
+ esc: 'Esc',
29
+ left: '←',
30
+ meta: 'Win',
31
+ mod: 'Ctrl',
32
+ opt: 'Alt',
33
+ right: '→',
34
+ shift: 'Shift',
35
+ space: 'Space',
36
+ tab: 'Tab',
37
+ up: '↑',
38
+ };
39
+ export const keyNames = (parts, apple, own = {}) => {
40
+ const names = apple ? APPLE : ELSEWHERE;
41
+ let worded = false;
42
+ const keys = parts.map((part) => {
43
+ const name = part.toLowerCase();
44
+ const mine = own[name];
45
+ if (mine !== undefined) {
46
+ worded ||= [...mine].length > 1;
47
+ return mine;
48
+ }
49
+ return names[name] ?? (part.length === 1 ? part.toUpperCase() : part);
50
+ });
51
+ return { keys, separator: apple ? (worded ? '-' : '') : '+' };
52
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `value` held between `low` and `high`.
3
+ *
4
+ * Written four times before: Lightbox's zoom, Scroll Area's scrollbar, Color
5
+ * Picker's channels and core's color reading.
6
+ */
7
+ export declare const clamp: (value: number, low: number, high: number) => number;
@@ -0,0 +1 @@
1
+ export const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Where Up or Down moves in a list laid out in columns, as a grid of tiles or
3
+ * a wrapping row is: to the next item after `at` (with `by` of `1`) or the
4
+ * last one before it (`-1`) that starts at the same inline offset, which is
5
+ * the item above or below it. `at` itself where there is none, so the key
6
+ * stops at the edge, as React Aria's grid layout stops.
7
+ *
8
+ * Read from where the items are drawn rather than from a column count, so it
9
+ * holds for a grid of any width, a row that wraps, and a column cut short.
10
+ * `starts` are the items' inline offsets in the order of the list, such as
11
+ * each one's `getBoundingClientRect().left`.
12
+ */
13
+ export declare const columnStep: (starts: readonly number[], at: number, by: -1 | 1) => number;
@@ -0,0 +1,9 @@
1
+ export const columnStep = (starts, at, by) => {
2
+ const column = starts[at];
3
+ for (let index = at + by; index >= 0 && index < starts.length; index += by) {
4
+ if (starts[index] === column) {
5
+ return index;
6
+ }
7
+ }
8
+ return at;
9
+ };
@@ -1,4 +1,4 @@
1
- const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
1
+ import { clamp } from '../clamp/clamp.js';
2
2
  const toAlpha = (token) => {
3
3
  if (token === undefined) {
4
4
  return 100;
@@ -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
+ };
@@ -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}`;
@@ -1,4 +1,4 @@
1
- import type { Relative } from '../relative-time/relative-time.js';
1
+ import { type Relative } from '../relative-time/relative-time.js';
2
2
  /**
3
3
  * A formatter for one locale, kept.
4
4
  *
@@ -1,10 +1,7 @@
1
+ import { UNITS as TIME_UNITS } from '../relative-time/relative-time.js';
1
2
  const held = new Map();
2
3
  const dated = new Map();
3
- const UNITS = [
4
- { ms: 60_000, unit: 'minute' },
5
- { ms: 3_600_000, unit: 'hour' },
6
- { ms: 86_400_000, unit: 'day' },
7
- ];
4
+ const UNITS = TIME_UNITS.slice(1, 4);
8
5
  const HORIZON = 604_800_000;
9
6
  const compact = (locale) => {
10
7
  let found = held.get(locale);
@@ -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
+ };