@fusion.dev/helix-core 0.146.0 → 0.147.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 (84) hide show
  1. package/custom-elements.json +1299 -60
  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/copy-controller/copy-controller.d.ts +4 -1
  6. package/dist/controllers/copy-controller/copy-controller.js +2 -1
  7. package/dist/controllers/countdown-controller/countdown-controller.d.ts +4 -1
  8. package/dist/controllers/countdown-controller/countdown-controller.js +3 -1
  9. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +5 -0
  10. package/dist/controllers/source-controller/source-controller.js +2 -0
  11. package/dist/controllers/swipe-controller/swipe-controller.d.ts +10 -1
  12. package/dist/controllers/swipe-controller/swipe-controller.js +1 -1
  13. package/dist/controllers/virtualize-controller/virtualize-controller.js +8 -6
  14. package/dist/events/band.d.ts +13 -0
  15. package/dist/events/band.js +11 -0
  16. package/dist/events/details.d.ts +5 -0
  17. package/dist/events/events.d.ts +4 -2
  18. package/dist/events/events.js +3 -1
  19. package/dist/events/names.d.ts +3 -0
  20. package/dist/events/names.js +13 -88
  21. package/dist/helix-element/helix-element.js +9 -43
  22. package/dist/helix-element/react-bridge.d.ts +13 -0
  23. package/dist/helix-element/react-bridge.js +1 -1
  24. package/dist/index.d.ts +18 -9
  25. package/dist/index.js +18 -9
  26. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -0
  27. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +1 -0
  28. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +25 -4
  29. package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.d.ts +42 -0
  30. package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.js +46 -0
  31. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +2 -11
  32. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +1 -6
  33. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +1 -1
  34. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +10 -6
  35. package/dist/utils/audit-name/audit-name.d.ts +7 -2
  36. package/dist/utils/audit-name/audit-name.js +12 -3
  37. package/dist/utils/audit-source/audit-source-off.d.ts +10 -0
  38. package/dist/utils/audit-source/audit-source-off.js +1 -0
  39. package/dist/utils/audit-source/audit-source.d.ts +19 -0
  40. package/dist/utils/audit-source/audit-source.js +27 -0
  41. package/dist/utils/call-listener/call-listener.d.ts +16 -0
  42. package/dist/utils/call-listener/call-listener.js +17 -0
  43. package/dist/utils/chord/chord.d.ts +14 -0
  44. package/dist/utils/chord/chord.js +1 -0
  45. package/dist/utils/clamp/clamp.d.ts +7 -0
  46. package/dist/utils/clamp/clamp.js +1 -0
  47. package/dist/utils/css-color/css-color.js +1 -1
  48. package/dist/utils/elapsed-time/elapsed-time.d.ts +1 -1
  49. package/dist/utils/elapsed-time/elapsed-time.js +2 -5
  50. package/dist/utils/host-words/host-words.d.ts +16 -0
  51. package/dist/utils/host-words/host-words.js +13 -0
  52. package/dist/utils/load-more/load-more.d.ts +25 -0
  53. package/dist/utils/load-more/load-more.js +1 -0
  54. package/dist/utils/locale-of/locale-of.d.ts +18 -0
  55. package/dist/utils/locale-of/locale-of.js +26 -0
  56. package/dist/utils/nearest/nearest.d.ts +9 -0
  57. package/dist/utils/nearest/nearest.js +9 -0
  58. package/dist/utils/number-format/number-format.d.ts +9 -0
  59. package/dist/utils/number-format/number-format.js +4 -0
  60. package/dist/utils/own-rule/own-rule.d.ts +16 -0
  61. package/dist/utils/own-rule/own-rule.js +7 -0
  62. package/dist/utils/platform/platform.d.ts +8 -0
  63. package/dist/utils/platform/platform.js +1 -0
  64. package/dist/utils/read-series/read-series.d.ts +14 -0
  65. package/dist/utils/read-series/read-series.js +15 -0
  66. package/dist/utils/relative-time/relative-time.d.ts +8 -0
  67. package/dist/utils/relative-time/relative-time.js +5 -2
  68. package/dist/utils/report-render/report-render.js +2 -14
  69. package/dist/utils/report-runtime/report-runtime.d.ts +5 -1
  70. package/dist/utils/report-runtime/report-runtime.js +2 -14
  71. package/dist/utils/report-usage/report-usage.js +2 -14
  72. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +33 -0
  73. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +13 -3
  74. package/dist/utils/slotted-glyph/slotted-glyph.d.ts +33 -0
  75. package/dist/utils/slotted-glyph/slotted-glyph.js +27 -0
  76. package/dist/utils/typeahead/typeahead.d.ts +8 -0
  77. package/dist/utils/typeahead/typeahead.js +1 -0
  78. package/dist/utils/verdict-flags/verdict-flags.d.ts +13 -0
  79. package/dist/utils/verdict-flags/verdict-flags.js +6 -0
  80. package/dist/utils/written-list/written-list.d.ts +31 -0
  81. package/dist/utils/written-list/written-list.js +16 -0
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +15 -4
@@ -0,0 +1,25 @@
1
+ /** Where a scrolling set stands, for the question of whether to ask for more. */
2
+ export interface EndInSight {
3
+ /** The length the set last asked for more at, or `-1` where it never has. */
4
+ askedAt: number;
5
+ /** The set's length now, which the answer is kept for. */
6
+ count: number;
7
+ /** How far below the bottom of the box the set goes, in pixels. */
8
+ remaining: number;
9
+ /** The box's own height, in pixels: one screen. */
10
+ screen: number;
11
+ }
12
+ /**
13
+ * Whether a reader has come within one screen of the end of a set, and the
14
+ * set has not already asked for more at this length: React Aria's
15
+ * `onLoadMore` rule.
16
+ *
17
+ * **Once for each length**, so a scroll that goes on asks again only after the
18
+ * next page arrived and the length moved. False while the box has no height or
19
+ * the set no items, so a list that has not been laid out asks for nothing.
20
+ * The caller keeps `askedAt`, setting it to `count` when this answers true.
21
+ *
22
+ * Listbox asks this of its own scroll, and `VirtualizeController.endInSight`
23
+ * of the rows it lays out, so the two lists ask on the same terms.
24
+ */
25
+ export declare const endInSight: ({ askedAt, count, remaining, screen }: EndInSight) => boolean;
@@ -0,0 +1 @@
1
+ export const endInSight = ({ askedAt, count, remaining, screen }) => count > 0 && screen > 0 && askedAt !== count && remaining <= screen;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The locale an element speaks: `own`, the element's own `locale` property,
3
+ * where one is set, and otherwise the `lang` of the nearest element with one,
4
+ * read out through any shadow root the element is drawn in.
5
+ *
6
+ * **The page's language is the default a reader expects.** A date picker on a
7
+ * page written `lang="de"` that names its months in English and starts its
8
+ * weeks on Sunday is a widget that ignored the one place the page said what
9
+ * language it is in; Shoelace and Web Awesome read the same attribute.
10
+ * `lang=""` is the page saying it does not know, and answers empty.
11
+ *
12
+ * Empty where nothing says, which every `Intl` call here takes as the
13
+ * runtime's own locale, and empty for a bare language the runtime already
14
+ * speaks (see `usable`). Read when asked rather than watched, so a page that
15
+ * changes its `lang` is answered in the new one on the element's next render.
16
+ * A server shim with no `closest` answers `own`, or empty.
17
+ */
18
+ export declare const localeOf: (element: Element, own?: string) => string;
@@ -0,0 +1,26 @@
1
+ let runtime;
2
+ const usable = (lang) => {
3
+ try {
4
+ 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();
7
+ }
8
+ catch {
9
+ return '';
10
+ }
11
+ };
12
+ export const localeOf = (element, own = '') => {
13
+ if (own) {
14
+ return own;
15
+ }
16
+ let from = element;
17
+ while (from && typeof from.closest === 'function') {
18
+ const found = from.closest('[lang]');
19
+ if (found) {
20
+ return usable(found.getAttribute('lang') ?? '');
21
+ }
22
+ const root = from.getRootNode?.();
23
+ from = root?.host ?? null;
24
+ }
25
+ return '';
26
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The value in `values` nearest `target`, the first of two at the same
3
+ * distance, or `undefined` for none.
4
+ *
5
+ * Drawer's `snapTo` and Rating's reading of a score between its steps each
6
+ * wrote this loop. Given in ascending order, a target halfway between two
7
+ * goes to the lower, which is the rule both kept.
8
+ */
9
+ export declare const nearest: (values: Iterable<number>, target: number) => number | undefined;
@@ -0,0 +1,9 @@
1
+ export const nearest = (values, target) => {
2
+ let found;
3
+ for (const one of values) {
4
+ if (found === undefined || Math.abs(one - target) < Math.abs(found - target)) {
5
+ found = one;
6
+ }
7
+ }
8
+ return found;
9
+ };
@@ -9,6 +9,15 @@
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 reading in words: `value` written with `options` where they say another
14
+ * style, and otherwise `share`, from 0 to 1, as a percentage, whole unless
15
+ * `options` say more.
16
+ *
17
+ * Meter and Progress each wrote this, with the value each holds to its range
18
+ * and the share of it that is filled.
19
+ */
20
+ export declare const readingText: (value: number, share: number, locale?: string, options?: Intl.NumberFormatOptions) => string;
12
21
  /**
13
22
  * Reads a number written in a locale with the same options it is shown with,
14
23
  * or `null` where the text is no number.
@@ -9,6 +9,10 @@ const formatter = (locale, options) => {
9
9
  return found;
10
10
  };
11
11
  export const formatNumber = (value, locale = '', options) => formatter(locale, options).format(value);
12
+ const WHOLE_PERCENT = { maximumFractionDigits: 0, style: 'percent' };
13
+ export const readingText = (value, share, locale = '', options) => options && options.style !== 'percent'
14
+ ? formatNumber(value, locale, options)
15
+ : formatNumber(share, locale, options ?? WHOLE_PERCENT);
12
16
  const grouped = (text, separator, decimal) => {
13
17
  const whole = (text.split(decimal)[0] ?? '').replace(/^[+-]/u, '');
14
18
  return new RegExp(`^\\d{1,3}(${literal(separator)}\\d{3})+$`, 'u').test(whole);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A page's own rule for a field's value: a message, a list of them, or `true`,
3
+ * `null` or nothing to accept the value. Input's and Textarea's `validate`.
4
+ */
5
+ export type OwnRule = (value: string) => string | string[] | true | null | undefined;
6
+ /**
7
+ * Applies a page's own rule after the native verdict was copied, as React
8
+ * Aria's TextField `validate` does: only while the browser finds nothing
9
+ * wrong, so a required empty field says the browser's sentence first. A list
10
+ * of messages is joined with spaces, and the message is a `customError`
11
+ * anchored at the control, so the bubble points at the text.
12
+ *
13
+ * A function rather than a member of `MixinField`, so only the fields that
14
+ * take `validate` carry it.
15
+ */
16
+ export declare const applyOwnRule: (internals: ElementInternals, control: HTMLInputElement | HTMLTextAreaElement, rule: OwnRule | undefined, value: string) => void;
@@ -0,0 +1,7 @@
1
+ export const applyOwnRule = (internals, control, rule, value) => {
2
+ const own = control.validity.valid ? rule?.(value) : undefined;
3
+ const message = Array.isArray(own) ? own.join(' ') : typeof own === 'string' ? own : '';
4
+ if (message !== '') {
5
+ internals.setValidity({ customError: true }, message, control);
6
+ }
7
+ };
@@ -21,3 +21,11 @@ export declare const appleTouch: (platform: Platform | undefined) => boolean;
21
21
  * `mod` as the key it names.
22
22
  */
23
23
  export declare const onApple: (platform?: Platform | undefined) => boolean;
24
+ /**
25
+ * Whether the browser runs on Android, where TalkBack reads no `aria-sort`.
26
+ *
27
+ * React Aria's table header describes its sort order in words there for the
28
+ * same reason, so a reader who cannot hear the state still hears which way a
29
+ * column is sorted. From the user agent; `false` on a server.
30
+ */
31
+ export declare const onAndroid: (platform?: Platform | undefined) => boolean;
@@ -9,3 +9,4 @@ export const appleTouch = (platform) => {
9
9
  return platform.platform === 'MacIntel' && (platform.maxTouchPoints ?? 0) > 1;
10
10
  };
11
11
  export const onApple = (platform = typeof navigator === 'undefined' ? undefined : navigator) => platform !== undefined && /mac|iphone|ipad|ipod/i.test(platform.userAgent ?? '');
12
+ export const onAndroid = (platform = typeof navigator === 'undefined' ? undefined : navigator) => platform !== undefined && /android/i.test(platform.userAgent ?? '');
@@ -136,3 +136,17 @@ export declare const readRows: (table: Element) => Row[];
136
136
  * name is written, in `lines`, `hidden-series` and `hide()` alike.
137
137
  */
138
138
  export declare const seriesKey: (name: string) => string;
139
+ /** Where the highest and the lowest readings are, by place, or none. */
140
+ export interface Extremes {
141
+ /** The place of the highest reading, the first where several tie. */
142
+ high?: number;
143
+ /** The place of the lowest reading, the first where several tie. */
144
+ low?: number;
145
+ }
146
+ /**
147
+ * Where the highest and the lowest readings stand. A blank slot is passed
148
+ * over, and a list with nothing in it has neither.
149
+ *
150
+ * Sparkline's overview and Heatmap's busiest place each looked for these.
151
+ */
152
+ export declare const extremesOf: (slots: readonly (number | undefined)[]) => Extremes;
@@ -139,3 +139,18 @@ export const readRows = (table) => {
139
139
  return [...(element.tBodies[0]?.rows ?? [])].map((one) => row(one.cells[0], [...one.cells].slice(1), names));
140
140
  };
141
141
  export const seriesKey = (name) => name.trim().toLowerCase();
142
+ export const extremesOf = (slots) => {
143
+ const found = {};
144
+ slots.forEach((value, at) => {
145
+ if (value === undefined) {
146
+ return;
147
+ }
148
+ if (found.high === undefined || value > slots[found.high]) {
149
+ found.high = at;
150
+ }
151
+ if (found.low === undefined || value < slots[found.low]) {
152
+ found.low = at;
153
+ }
154
+ });
155
+ return found;
156
+ };
@@ -1,3 +1,11 @@
1
+ /**
2
+ * How long each unit lasts, in milliseconds, largest last. `elapsedTime`
3
+ * reads its minute, hour and day from here.
4
+ */
5
+ export declare const UNITS: Array<{
6
+ ms: number;
7
+ unit: Intl.RelativeTimeFormatUnit;
8
+ }>;
1
9
  /** What one timestamp reads as, and when that stops being true. */
2
10
  export interface Relative {
3
11
  /**
@@ -1,5 +1,5 @@
1
1
  const held = new Map();
2
- const UNITS = [
2
+ export const UNITS = [
3
3
  { ms: 1000, unit: 'second' },
4
4
  { ms: 60_000, unit: 'minute' },
5
5
  { ms: 3_600_000, unit: 'hour' },
@@ -25,7 +25,10 @@ export const relativeTime = (locale, options = {}) => {
25
25
  }
26
26
  const size = Math.abs(elapsed);
27
27
  if (size < 60_000) {
28
- return { next: Math.max(1000, 60_000 - size), text: found.format(0, 'second') };
28
+ return {
29
+ next: Math.max(1000, 60_000 - size),
30
+ text: found.format(elapsed > 0 ? 0 : -0, 'second'),
31
+ };
29
32
  }
30
33
  let chosen = UNITS[1];
31
34
  for (const unit of UNITS) {
@@ -1,18 +1,6 @@
1
+ import { callListener } from '../call-listener/call-listener.js';
1
2
  export const RENDER_SLOT = Symbol.for('fusion.dev/helix/render');
2
3
  export const measuring = () => typeof globalThis[RENDER_SLOT] === 'function';
3
- let told = false;
4
4
  export const reportRender = (element, start, duration) => {
5
- const listener = globalThis[RENDER_SLOT];
6
- if (typeof listener !== 'function') {
7
- return;
8
- }
9
- try {
10
- listener(element, { duration, start });
11
- }
12
- catch (error) {
13
- if (!told) {
14
- told = true;
15
- console.error('helix: a render listener threw, and the element rendered as usual. Later failures are not printed.', error);
16
- }
17
- }
5
+ callListener(RENDER_SLOT, 'render', 'the element rendered as usual', element, { duration, start });
18
6
  };
@@ -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 deprecated property, attribute or slot was used; the message names what replaces it. */
22
+ 'deprecated'
21
23
  /** Another class asked for a tag that is already registered: two copies of Helix. */
22
- 'duplicate-registration'
24
+ | 'duplicate-registration'
23
25
  /** The browser refused fullscreen, usually for a frame without the permission. */
24
26
  | 'fullscreen-refused'
25
27
  /** A rating's `min` is no score between 0 and its `max`. */
@@ -41,6 +43,8 @@ export type RuntimeKind =
41
43
  | 'missing-row-key'
42
44
  /** A stencil names parts the shape does not have. */
43
45
  | 'missing-stencil-parts'
46
+ /** A chart, a legend or a sparkline names no table with `for`. Development builds only. */
47
+ | 'missing-table'
44
48
  /** A Skip Link's `href` names no element in its tree, so a press moves focus nowhere. */
45
49
  | 'missing-target'
46
50
  /** A tooltip's `for` names no element in its own tree. */
@@ -1,7 +1,7 @@
1
+ import { callListener } from '../call-listener/call-listener.js';
1
2
  export const RUNTIME_SLOT = Symbol.for('fusion.dev/helix/runtime');
2
3
  export const RUNTIME_LOG = Symbol.for('fusion.dev/helix/runtime-log');
3
4
  const KEPT = 200;
4
- let told = false;
5
5
  export const reportRuntime = (event) => {
6
6
  const store = globalThis;
7
7
  const log = Array.isArray(store[RUNTIME_LOG]) ? store[RUNTIME_LOG] : [];
@@ -9,19 +9,7 @@ export const reportRuntime = (event) => {
9
9
  log.push(event);
10
10
  }
11
11
  store[RUNTIME_LOG] = log;
12
- let quiet = false;
13
- const listener = store[RUNTIME_SLOT];
14
- if (typeof listener === 'function') {
15
- try {
16
- quiet = listener(event) === true;
17
- }
18
- catch (error) {
19
- if (!told) {
20
- told = true;
21
- console.error('helix: a runtime listener threw, and the warning is printed here. Later failures are not printed.', error);
22
- }
23
- }
24
- }
12
+ const quiet = callListener(RUNTIME_SLOT, 'runtime', 'the warning is printed here', event) === true;
25
13
  if (quiet) {
26
14
  return;
27
15
  }
@@ -1,4 +1,5 @@
1
1
  import { VERSION } from '../../version.js';
2
+ import { callListener } from '../call-listener/call-listener.js';
2
3
  export const USAGE_SLOT = Symbol.for('fusion.dev/helix/usage');
3
4
  export const VERSIONS_SLOT = Symbol.for('fusion.dev/helix/versions');
4
5
  export const noteVersion = () => {
@@ -9,19 +10,6 @@ export const noteVersion = () => {
9
10
  }
10
11
  store[VERSIONS_SLOT] = listed;
11
12
  };
12
- let told = false;
13
13
  export const reportUsage = (element) => {
14
- const listener = globalThis[USAGE_SLOT];
15
- if (typeof listener !== 'function') {
16
- return;
17
- }
18
- try {
19
- listener(element);
20
- }
21
- catch (error) {
22
- if (!told) {
23
- told = true;
24
- console.error('helix: a usage listener threw, and the element rendered as usual. Later failures are not printed.', error);
25
- }
26
- }
14
+ callListener(USAGE_SLOT, 'usage', 'the element rendered as usual', element);
27
15
  };
@@ -7,4 +7,37 @@ export interface ScrollItemOptions {
7
7
  /** Whether to animate. Ignored where the reader asked for less motion. */
8
8
  smooth?: boolean;
9
9
  }
10
+ /**
11
+ * The nearest ancestor that actually scrolls, crossing shadow boundaries.
12
+ *
13
+ * **Not `item.offsetParent`**, which is the nearest *positioned* ancestor and
14
+ * has nothing whatever to do with overflow. The two coincide often enough to
15
+ * look right, because a scroller frequently is positioned, and they come apart
16
+ * exactly where it matters: put anything `position: relative` between the item
17
+ * and the box that scrolls, and the util scrolls the thing in between, which
18
+ * does not scroll, and reports nothing.
19
+ *
20
+ * The walk crosses shadow boundaries through the host, because the item is
21
+ * almost always in one and `parentElement` is null at the root. Without that
22
+ * step, a component whose scroller is its own `[part]` would find nothing.
23
+ *
24
+ * It stops before the document. Scrolling the page is the one thing this whole
25
+ * util exists not to do, so finding nothing inside the component and returning
26
+ * null is the honest answer: the caller no-ops rather than taking the address
27
+ * bar with it.
28
+ */
29
+ export declare const scrollingAncestor: (item: HTMLElement, options?: ScrollerOptions) => HTMLElement | null;
30
+ /** Where `scrollingAncestor` looks, and what it takes for a scroller. */
31
+ export interface ScrollerOptions {
32
+ /** The axis whose overflow counts, or either where absent. */
33
+ readonly axis?: 'x' | 'y';
34
+ /**
35
+ * Only a box whose content is larger than it, along `axis`: one that can
36
+ * scroll now, rather than one that would once it fills. Outline asks this of
37
+ * the box its links and its sections scroll in.
38
+ */
39
+ readonly overflowing?: boolean;
40
+ /** Where the walk stops, unread: the document's body where absent. */
41
+ readonly stop?: Element | null;
42
+ }
10
43
  export declare const scrollItemIntoView: (item: HTMLElement, options?: ScrollItemOptions) => void;
@@ -1,15 +1,25 @@
1
- const scrollingAncestor = (item) => {
2
- const stop = item.ownerDocument.body;
1
+ export const scrollingAncestor = (item, options = {}) => {
2
+ const { axis, overflowing = false, stop = item.ownerDocument.body } = options;
3
3
  let node = climb(item);
4
4
  while (node && node !== stop) {
5
5
  const style = getComputedStyle(node);
6
- if (/\b(auto|scroll|overlay)\b/.test(`${style.overflowX} ${style.overflowY}`)) {
6
+ const overflow = axis === 'x'
7
+ ? style.overflowX
8
+ : axis === 'y'
9
+ ? style.overflowY
10
+ : `${style.overflowX} ${style.overflowY}`;
11
+ if (/\b(auto|scroll|overlay)\b/.test(overflow) && (!overflowing || overflows(node, axis))) {
7
12
  return node;
8
13
  }
9
14
  node = climb(node);
10
15
  }
11
16
  return null;
12
17
  };
18
+ const overflows = (node, axis) => {
19
+ const tall = node.scrollHeight > node.clientHeight;
20
+ const wide = node.scrollWidth > node.clientWidth;
21
+ return axis === 'x' ? wide : axis === 'y' ? tall : tall || wide;
22
+ };
13
23
  const climb = (node) => {
14
24
  if (node.parentElement) {
15
25
  return node.parentElement;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A glyph a consumer slots once and a component draws in many places: the
3
+ * marks of a rating, the remove icon on every row of a file list.
4
+ *
5
+ * A slotted element can be assigned to one slot only, so a component that
6
+ * draws it in several places draws copies. **One copy per place, kept**, so a
7
+ * render does not make them again: a drag renders on every pointer move, and
8
+ * handing Lit a fresh node each time swaps every copy on every frame.
9
+ */
10
+ export declare class SlottedGlyph {
11
+ /** The copies made so far, by the place each is drawn in. */
12
+ private readonly copies;
13
+ /** The element slotted last, which every copy is made from. */
14
+ private source;
15
+ /** Whether a glyph is slotted, so the component draws copies of it. */
16
+ get slotted(): boolean;
17
+ /**
18
+ * Reads the slot after a `slotchange`: the first element assigned to it,
19
+ * through any slot it is forwarded by. The copies made of the last one go.
20
+ */
21
+ read(slot: HTMLSlotElement): void;
22
+ /**
23
+ * Takes `source` as the glyph, or none, as `read` does from a slot: the
24
+ * copies made of the last one go. For the first render, which reads the
25
+ * host's children before any `slotchange` answers.
26
+ */
27
+ take(source: Element | undefined): void;
28
+ /**
29
+ * The copy for one place, made the first time it is asked for and the same
30
+ * node after, or `undefined` while nothing is slotted.
31
+ */
32
+ copyFor(place: number | string): Node | undefined;
33
+ }
@@ -0,0 +1,27 @@
1
+ export class SlottedGlyph {
2
+ constructor() {
3
+ this.copies = new Map();
4
+ }
5
+ get slotted() {
6
+ return this.source !== undefined;
7
+ }
8
+ read(slot) {
9
+ this.take(slot.assignedElements({ flatten: true })[0]);
10
+ }
11
+ take(source) {
12
+ this.source = source;
13
+ this.copies.clear();
14
+ }
15
+ copyFor(place) {
16
+ if (!this.source) {
17
+ return undefined;
18
+ }
19
+ const key = String(place);
20
+ let copy = this.copies.get(key);
21
+ if (!copy) {
22
+ copy = this.source.cloneNode(true);
23
+ this.copies.set(key, copy);
24
+ }
25
+ return copy;
26
+ }
27
+ }
@@ -65,3 +65,11 @@ export interface TypeaheadBy<Item> {
65
65
  * `TypeaheadBy`.
66
66
  */
67
67
  export declare const typeaheadBy: <Item>(words: (item: Item) => string) => TypeaheadBy<Item>;
68
+ /**
69
+ * Turns on typeahead for a set whose items carry their own `words`, as List's
70
+ * rows, Tag Group's tags and Drilldown's items do. An item with none is
71
+ * passed over.
72
+ */
73
+ export declare const typeaheadByWords: () => TypeaheadBy<{
74
+ readonly words?: string;
75
+ }>;
@@ -34,3 +34,4 @@ export const typeaheadBy = (words) => ({
34
34
  search: new Typeahead(),
35
35
  words,
36
36
  });
37
+ export const typeaheadByWords = () => typeaheadBy((item) => item.words ?? '');
@@ -0,0 +1,13 @@
1
+ /**
2
+ * What a picker field finds wrong: a message, reported as `badInput`, or the
3
+ * validity flag to set and its message. Empty means nothing is wrong.
4
+ */
5
+ export type PickerVerdict = string | readonly [flag: keyof ValidityStateFlags, message: string];
6
+ /**
7
+ * A verdict as the flags and the message `setValidity` takes, or `null` where
8
+ * there is nothing wrong.
9
+ *
10
+ * Shared by `MixinPickerField` and `MixinSegmentedField`, the two shapes a
11
+ * date, time or color field takes, which report the same verdicts.
12
+ */
13
+ export declare const verdictFlags: (verdict: PickerVerdict) => [flags: ValidityStateFlags, message: string] | null;
@@ -0,0 +1,6 @@
1
+ export const verdictFlags = (verdict) => {
2
+ if (typeof verdict === 'string') {
3
+ return verdict === '' ? null : [{ badInput: true }, verdict];
4
+ }
5
+ return [{ [verdict[0]]: true }, verdict[1]];
6
+ };
@@ -71,6 +71,37 @@ export declare const toggleValue: (list: readonly string[], value: string, order
71
71
  * follows.
72
72
  */
73
73
  export declare const toggleEvery: (chosen: Iterable<string>, values: readonly string[], order?: readonly string[]) => string[];
74
+ /**
75
+ * What a select-all box shows for `values`: checked when every one is
76
+ * chosen, mixed when some are, and neither for none or an empty set.
77
+ *
78
+ * Checkbox Group's select-all box and the header boxes of Table and Data
79
+ * Table each worked this out. A set that does not count its disabled boxes
80
+ * passes only the enabled values.
81
+ */
82
+ export declare const everyState: (chosen: Iterable<string>, values: readonly string[]) => {
83
+ checked: boolean;
84
+ indeterminate: boolean;
85
+ };
86
+ /**
87
+ * `keys` in the order the rows are, which is `order`: the keys of the whole
88
+ * set. A key the set does not hold, a selected row a search has hidden say,
89
+ * keeps its place after them in the order it was given.
90
+ *
91
+ * So a selection reads in row order whatever order its rows were chosen in:
92
+ * Data Table's, and Table's and Virtual List's, which wrote it in press order
93
+ * or wrote this again.
94
+ */
95
+ export declare const inRowOrder: (keys: Iterable<string>, order: readonly string[]) => string[];
96
+ /**
97
+ * Where a Shift range runs from, as an index into `keys`: the row last chosen
98
+ * while it is among them, and otherwise the row the reader is on, `from`,
99
+ * which becomes the one to run from.
100
+ */
101
+ export declare const anchorAt: (keys: readonly string[], anchorKey: string | undefined, from: number) => {
102
+ anchor: number;
103
+ anchorKey: string | undefined;
104
+ };
74
105
  /** How many items a set lets a reader select: Tag Group's and List's `selection-mode`. */
75
106
  export type SelectionMode = 'multiple' | 'none' | 'single';
76
107
  /**
@@ -25,6 +25,22 @@ export const toggleEvery = (chosen, values, order) => {
25
25
  }
26
26
  return order ? order.filter((value) => next.has(value)) : [...next];
27
27
  };
28
+ export const everyState = (chosen, values) => {
29
+ const held = new Set(chosen);
30
+ const count = values.filter((value) => held.has(value)).length;
31
+ const checked = values.length > 0 && count === values.length;
32
+ return { checked, indeterminate: count > 0 && !checked };
33
+ };
34
+ export const inRowOrder = (keys, order) => {
35
+ const wanted = new Set(keys);
36
+ const placed = order.filter((key) => wanted.has(key));
37
+ const seen = new Set(placed);
38
+ return [...placed, ...[...wanted].filter((key) => !seen.has(key))];
39
+ };
40
+ export const anchorAt = (keys, anchorKey, from) => {
41
+ const anchor = anchorKey === undefined ? -1 : keys.indexOf(anchorKey);
42
+ return anchor < 0 ? { anchor: from, anchorKey: keys[from] } : { anchor, anchorKey };
43
+ };
28
44
  export const pressedValue = (mode, value, pressed) => {
29
45
  const now = commaList(value);
30
46
  if (mode === 'single') {
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.146.0";
9
+ export declare const VERSION = "0.147.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.146.0';
1
+ export const VERSION = '0.147.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.146.0",
4
+ "version": "0.147.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.146.0"
37
+ "@fusion.dev/helix-icons": "0.147.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@fusion.dev/helix-cem": "0.146.0",
41
- "@fusion.dev/helix-testing": "0.146.0",
40
+ "@fusion.dev/helix-cem": "0.147.0",
41
+ "@fusion.dev/helix-testing": "0.147.0",
42
42
  "@lit-labs/ssr": "^4.1.0",
43
43
  "lit": "^3.2.1"
44
44
  },
@@ -54,6 +54,17 @@
54
54
  "development": "./dist/utils/audit-name/audit-name.js",
55
55
  "helix-development": "./dist/utils/audit-name/audit-name.js",
56
56
  "default": "./dist/utils/audit-name/audit-name-off.js"
57
+ },
58
+ "#audit-source": {
59
+ "helix-source": {
60
+ "development": "./src/utils/audit-source/audit-source.ts",
61
+ "helix-development": "./src/utils/audit-source/audit-source.ts",
62
+ "default": "./src/utils/audit-source/audit-source-off.ts"
63
+ },
64
+ "types": "./dist/utils/audit-source/audit-source.d.ts",
65
+ "development": "./dist/utils/audit-source/audit-source.js",
66
+ "helix-development": "./dist/utils/audit-source/audit-source.js",
67
+ "default": "./dist/utils/audit-source/audit-source-off.js"
57
68
  }
58
69
  },
59
70
  "keywords": [