@fusion.dev/helix-core 0.147.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 (65) hide show
  1. package/custom-elements.json +1741 -133
  2. package/dist/controllers/children-controller/children-controller.d.ts +28 -0
  3. package/dist/controllers/children-controller/children-controller.js +18 -0
  4. package/dist/controllers/countdown-controller/countdown-controller.d.ts +15 -1
  5. package/dist/controllers/countdown-controller/countdown-controller.js +27 -2
  6. package/dist/controllers/floating-controller/floating-controller.d.ts +12 -1
  7. package/dist/controllers/floating-controller/floating-controller.js +2 -1
  8. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +25 -0
  9. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +8 -0
  10. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +10 -0
  11. package/dist/controllers/open-phase-controller/open-phase-controller.js +7 -4
  12. package/dist/controllers/option-window-controller/option-window-controller.d.ts +96 -0
  13. package/dist/controllers/option-window-controller/option-window-controller.js +81 -0
  14. package/dist/controllers/popover-controller/popover-controller.js +1 -1
  15. package/dist/helix-element/helix-element.d.ts +14 -5
  16. package/dist/helix-element/helix-element.js +4 -2
  17. package/dist/index.d.ts +24 -10
  18. package/dist/index.js +22 -8
  19. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +5 -0
  20. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +3 -0
  21. package/dist/mixins/mixin-viewport/mixin-viewport.js +8 -7
  22. package/dist/utils/assign-slots/assign-slots.d.ts +18 -0
  23. package/dist/utils/assign-slots/assign-slots.js +14 -0
  24. package/dist/utils/autofill-select/autofill-select.d.ts +40 -0
  25. package/dist/utils/autofill-select/autofill-select.js +14 -0
  26. package/dist/utils/center-in-view/center-in-view.d.ts +18 -0
  27. package/dist/utils/center-in-view/center-in-view.js +14 -0
  28. package/dist/utils/chord/key-names.d.ts +30 -0
  29. package/dist/utils/chord/key-names.js +52 -0
  30. package/dist/utils/column-step/column-step.d.ts +13 -0
  31. package/dist/utils/column-step/column-step.js +9 -0
  32. package/dist/utils/data-options/data-options.d.ts +60 -0
  33. package/dist/utils/data-options/data-options.js +46 -0
  34. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +7 -2
  35. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -1
  36. package/dist/utils/draw-tooltip/spoken-reading.d.ts +3 -2
  37. package/dist/utils/draw-tooltip/spoken-reading.js +3 -2
  38. package/dist/utils/export-parts/export-parts.d.ts +14 -0
  39. package/dist/utils/export-parts/export-parts.js +1 -0
  40. package/dist/utils/fill-template/fill-template.d.ts +9 -0
  41. package/dist/utils/fill-template/fill-template.js +3 -0
  42. package/dist/utils/filter-marks/filter-marks.d.ts +36 -0
  43. package/dist/utils/filter-marks/filter-marks.js +41 -0
  44. package/dist/utils/focus-target/focus-target.d.ts +12 -0
  45. package/dist/utils/focus-target/focus-target.js +9 -0
  46. package/dist/utils/focusable-elements/focusable-elements.d.ts +17 -0
  47. package/dist/utils/focusable-elements/focusable-elements.js +8 -0
  48. package/dist/utils/grid-move/grid-move.d.ts +56 -0
  49. package/dist/utils/grid-move/grid-move.js +28 -0
  50. package/dist/utils/number-format/number-format.d.ts +7 -0
  51. package/dist/utils/number-format/number-format.js +1 -0
  52. package/dist/utils/report-runtime/report-runtime.d.ts +7 -1
  53. package/dist/utils/report-values/report-values.d.ts +16 -4
  54. package/dist/utils/report-values/report-values.js +2 -2
  55. package/dist/utils/request-change/request-change.d.ts +11 -0
  56. package/dist/utils/request-change/request-change.js +10 -0
  57. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +17 -0
  58. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +1 -0
  59. package/dist/utils/slot-filled/slot-filled.d.ts +8 -0
  60. package/dist/utils/slot-filled/slot-filled.js +1 -0
  61. package/dist/utils/toolbar-around/toolbar-around.d.ts +18 -0
  62. package/dist/utils/toolbar-around/toolbar-around.js +5 -0
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +4 -4
@@ -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;
@@ -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);
@@ -18,8 +18,10 @@
18
18
  */
19
19
  /** What went wrong, stable across versions, so it can be counted. */
20
20
  export type RuntimeKind =
21
+ /** A Blockquote's `cite` holds words, where it takes the source's URL. */
22
+ 'cite-not-url'
21
23
  /** A deprecated property, attribute or slot was used; the message names what replaces it. */
22
- 'deprecated'
24
+ | 'deprecated'
23
25
  /** Another class asked for a tag that is already registered: two copies of Helix. */
24
26
  | 'duplicate-registration'
25
27
  /** The browser refused fullscreen, usually for a frame without the permission. */
@@ -79,8 +81,12 @@ export type RuntimeKind =
79
81
  | 'unlinked-trigger'
80
82
  /** A Text is rendered as a label, a deprecated value whose shadow root reaches no control. */
81
83
  | 'unreachable-label'
84
+ /** A QR code holds a logo at a correction level below `H`, so it may not scan. */
85
+ | 'unscannable-logo'
82
86
  /** A virtualized list's box has no height of its own, so its window never settles. */
83
87
  | 'unsettled-window'
88
+ /** A native button in a Button Group with `selection-mode`, which cannot be told it is pressed. */
89
+ | 'untold-toggle'
84
90
  /** A slotted trigger is a plain element, which cannot be told it opens something. */
85
91
  | 'untold-trigger'
86
92
  /** A tooltip wraps its trigger, so its text reaches no screen reader. */
@@ -9,13 +9,25 @@
9
9
  * sent the first and Date Picker the second, so what a server received for the
10
10
  * same answer depended on which control asked for it.
11
11
  *
12
- * **The joined string is the state**, which is what the browser hands back to
13
- * `formStateRestoreCallback` and what `value` holds, so a restored control
14
- * reads the shape it wrote.
12
+ * **The joined string is the state by default**, which is what the browser
13
+ * hands back to `formStateRestoreCallback` and what `value` holds, so a
14
+ * restored control reads the shape it wrote. A control whose answers can hold
15
+ * a comma asks for `state: 'entries'`, and the browser hands back the form
16
+ * data itself, one entry per answer, so `b,1` comes back as one answer and not
17
+ * two.
15
18
  *
16
19
  * **Nothing without a `name`.** A plain string is skipped by the form when its
17
20
  * control has no name, but `FormData` carries its own names and its entries
18
21
  * are appended whatever the element is called, so an unnamed control would
19
22
  * submit empty-named values.
20
23
  */
21
- export declare const reportValues: (internals: ElementInternals, name: string, values: readonly string[]) => void;
24
+ /** How `reportValues` saves the state the browser restores. */
25
+ export interface ReportValuesOptions {
26
+ /**
27
+ * What the browser hands back to `formStateRestoreCallback`: `joined`, the
28
+ * answers joined with commas, or `entries`, the form data with one entry per
29
+ * answer. Defaults to `joined`.
30
+ */
31
+ state?: 'entries' | 'joined';
32
+ }
33
+ export declare const reportValues: (internals: ElementInternals, name: string, values: readonly string[], { state }?: ReportValuesOptions) => void;
@@ -1,4 +1,4 @@
1
- export const reportValues = (internals, name, values) => {
1
+ export const reportValues = (internals, name, values, { state = 'joined' } = {}) => {
2
2
  if (name === '' || values.length === 0) {
3
3
  internals.setFormValue(null);
4
4
  return;
@@ -7,5 +7,5 @@ export const reportValues = (internals, name, values) => {
7
7
  for (const value of values) {
8
8
  data.append(name, value);
9
9
  }
10
- internals.setFormValue(data, values.join(','));
10
+ internals.setFormValue(data, state === 'entries' ? data : values.join(','));
11
11
  };
@@ -35,3 +35,14 @@ export interface ChangeRequestOptions<T> {
35
35
  next: T;
36
36
  }
37
37
  export declare const requestChange: <T>({ apply, event, host, next, }: ChangeRequestOptions<T>) => boolean;
38
+ /**
39
+ * Asks before a reader's press moves a value, then applies it and says so:
40
+ * `helix-before-change` with the value it would leave, which a listener may
41
+ * cancel, and `change` once it has moved.
42
+ *
43
+ * **The order every selectable set in the library keeps**, so a page reads the
44
+ * same pair from a list, a tag group, a heatmap or a legend: the cancelable
45
+ * question first, the platform's `change` after, bubbling and composed as a
46
+ * native control's does. Returns whether the value moved.
47
+ */
48
+ export declare const requestValue: (host: EventTarget, next: string, apply: (value: string) => void) => boolean;
@@ -1,3 +1,4 @@
1
+ import { HelixBeforeChangeEvent } from '../../events/before-change.js';
1
2
  export const requestChange = ({ apply, event, host, next, }) => {
2
3
  if (!host.dispatchEvent(event(next))) {
3
4
  return false;
@@ -5,3 +6,12 @@ export const requestChange = ({ apply, event, host, next, }) => {
5
6
  apply(next);
6
7
  return true;
7
8
  };
9
+ export const requestValue = (host, next, apply) => requestChange({
10
+ apply: (value) => {
11
+ apply(value);
12
+ host.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
13
+ },
14
+ event: (value) => new HelixBeforeChangeEvent({ value }),
15
+ host,
16
+ next,
17
+ });
@@ -27,6 +27,23 @@ export interface ScrollItemOptions {
27
27
  * bar with it.
28
28
  */
29
29
  export declare const scrollingAncestor: (item: HTMLElement, options?: ScrollerOptions) => HTMLElement | null;
30
+ /** What `scrollRoom` takes off a box that scrolls. */
31
+ export interface ScrollRoomOptions {
32
+ /**
33
+ * How much of the box its own content covers while it scrolls, a sticky row
34
+ * of headings, in pixels. Defaults to `0`.
35
+ */
36
+ covered?: number;
37
+ }
38
+ /**
39
+ * How much of a list a reader sees at once, for Page Up and Page Down: the
40
+ * box's own height where it scrolls, less what covers it, and the window's
41
+ * where it does not and the page scrolls instead.
42
+ *
43
+ * **A pixel of slack**, since a box whose content rounds to one pixel more
44
+ * than its height does not scroll, and was paging by the window's height.
45
+ */
46
+ export declare const scrollRoom: (box: Element | null | undefined, { covered }?: ScrollRoomOptions) => number;
30
47
  /** Where `scrollingAncestor` looks, and what it takes for a scroller. */
31
48
  export interface ScrollerOptions {
32
49
  /** The axis whose overflow counts, or either where absent. */
@@ -15,6 +15,7 @@ export const scrollingAncestor = (item, options = {}) => {
15
15
  }
16
16
  return null;
17
17
  };
18
+ export const scrollRoom = (box, { covered = 0 } = {}) => box && box.scrollHeight > box.clientHeight + 1 ? box.clientHeight - covered : window.innerHeight;
18
19
  const overflows = (node, axis) => {
19
20
  const tall = node.scrollHeight > node.clientHeight;
20
21
  const wide = node.scrollWidth > node.clientWidth;
@@ -22,3 +22,11 @@
22
22
  * anything", and a slot showing its fallback does render.
23
23
  */
24
24
  export declare const slotFilled: (slot: HTMLSlotElement | null | undefined) => boolean;
25
+ /**
26
+ * The slot of a name in a shadow root, or its default slot for `''`.
27
+ *
28
+ * `slot[name=""]` matches nothing, since the default slot has no `name`
29
+ * attribute at all, which is the one thing every hand-written lookup had to
30
+ * remember.
31
+ */
32
+ export declare const slotNamed: (root: ParentNode | null | undefined, name: string) => HTMLSlotElement | null;
@@ -1 +1,2 @@
1
1
  export const slotFilled = (slot) => (slot?.assignedNodes({ flatten: true }) ?? []).some((node) => node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim() !== '');
2
+ export const slotNamed = (root, name) => root?.querySelector?.(name === '' ? 'slot:not([name])' : `slot[name="${name}"]`) ?? null;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The toolbar an element sits in, or `null`.
3
+ *
4
+ * Read from the parent up, so a toolbar is never its own. One answer for every
5
+ * family that changes inside a toolbar: Toolbar becomes a group of the outer
6
+ * one, a multiple Toggle Group becomes a group, and Divider turns across the
7
+ * toolbar's axis. By tag and by role, since a family other than Toolbar cannot
8
+ * import Toolbar's class; Toolbar adds its own class for a third tag.
9
+ *
10
+ * `closest` stops at a shadow root, which is what a slotted child wants: its
11
+ * toolbar is its light-DOM parent or above.
12
+ */
13
+ export declare const toolbarAround: (element: Element) => Element | null;
14
+ /**
15
+ * The axis a toolbar runs along: its own `orientation`, else the page's
16
+ * `aria-orientation`, else horizontal, which is the role's default.
17
+ */
18
+ export declare const toolbarAxis: (toolbar: Element) => 'horizontal' | 'vertical';
@@ -0,0 +1,5 @@
1
+ const TOOLBAR = '[role="toolbar"], helix-toolbar, helix-headless-toolbar';
2
+ export const toolbarAround = (element) => element.parentElement?.closest(TOOLBAR) ?? null;
3
+ export const toolbarAxis = (toolbar) => (toolbar.getAttribute('orientation') ?? toolbar.getAttribute('aria-orientation')) === 'vertical'
4
+ ? 'vertical'
5
+ : 'horizontal';
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.147.0";
9
+ export declare const VERSION = "0.148.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.147.0';
1
+ export const VERSION = '0.148.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.147.0",
4
+ "version": "0.148.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -34,11 +34,11 @@
34
34
  "bugs": "https://github.com/project-fusion/fusion/issues",
35
35
  "dependencies": {
36
36
  "@floating-ui/dom": "^1.8.0",
37
- "@fusion.dev/helix-icons": "0.147.0"
37
+ "@fusion.dev/helix-icons": "0.148.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@fusion.dev/helix-cem": "0.147.0",
41
- "@fusion.dev/helix-testing": "0.147.0",
40
+ "@fusion.dev/helix-cem": "0.148.0",
41
+ "@fusion.dev/helix-testing": "0.148.0",
42
42
  "@lit-labs/ssr": "^4.1.0",
43
43
  "lit": "^3.2.1"
44
44
  },