@fusion.dev/helix-core 0.1.0 → 0.3.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 (56) hide show
  1. package/custom-elements.json +4 -4
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +0 -105
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +0 -71
  4. package/dist/controllers/command-controller/command-controller.js +0 -62
  5. package/dist/controllers/copy-controller/copy-controller.js +0 -63
  6. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +0 -40
  7. package/dist/controllers/dismiss-controller/dismiss-controller.js +0 -124
  8. package/dist/controllers/drilldown-controller/drilldown-controller.js +0 -87
  9. package/dist/controllers/floating-controller/floating-controller.js +0 -152
  10. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +0 -35
  11. package/dist/controllers/indicator-controller/indicator-controller.js +0 -82
  12. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +0 -46
  13. package/dist/controllers/popover-controller/popover-controller.js +0 -72
  14. package/dist/controllers/source-controller/source-controller.js +0 -38
  15. package/dist/controllers/token-color-controller/token-color-controller.js +0 -51
  16. package/dist/controllers/virtualize-controller/virtualize-controller.js +0 -128
  17. package/dist/events/element-events.js +0 -22
  18. package/dist/events/events.js +0 -386
  19. package/dist/helix-element/helix-element.d.ts +10 -17
  20. package/dist/helix-element/helix-element.js +0 -505
  21. package/dist/helix-form-element/helix-form-element.js +0 -140
  22. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +0 -77
  23. package/dist/mixins/mixin-collection/mixin-collection.js +0 -83
  24. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +0 -82
  25. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +0 -78
  26. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +0 -93
  27. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +0 -87
  28. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +0 -38
  29. package/dist/mixins/mixin-field/mixin-field.js +0 -277
  30. package/dist/utils/anchor-element/anchor-element.js +0 -23
  31. package/dist/utils/capture-pointer/capture-pointer.js +0 -18
  32. package/dist/utils/closest-instance/closest-instance.js +0 -20
  33. package/dist/utils/create-sheet/create-sheet.js +0 -14
  34. package/dist/utils/custom-element/custom-element.js +0 -83
  35. package/dist/utils/draw-icon/draw-icon.js +0 -28
  36. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -50
  37. package/dist/utils/elapsed-time/elapsed-time.js +0 -51
  38. package/dist/utils/focusable-elements/focusable-elements.js +1 -59
  39. package/dist/utils/format-numbers/format-numbers.js +0 -16
  40. package/dist/utils/host-aria/host-aria.js +0 -26
  41. package/dist/utils/inherit-disabled/inherit-disabled.js +0 -36
  42. package/dist/utils/line-path/line-path.js +0 -36
  43. package/dist/utils/on-unless-false/on-unless-false.js +0 -23
  44. package/dist/utils/read-series/read-series.js +0 -71
  45. package/dist/utils/relative-time/relative-time.js +0 -43
  46. package/dist/utils/resolve-active-item/resolve-active-item.js +0 -20
  47. package/dist/utils/restore-value/restore-value.js +0 -54
  48. package/dist/utils/scale/scale.js +0 -19
  49. package/dist/utils/scale/ticks.js +0 -36
  50. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +0 -53
  51. package/dist/utils/slot-filled/slot-filled.js +0 -23
  52. package/dist/utils/tell-disclosure/tell-disclosure.js +0 -53
  53. package/dist/utils/ticker/ticker.js +0 -22
  54. package/dist/utils/tone-marks/tone-marks.js +0 -24
  55. package/dist/utils/value-as/value-as.js +0 -56
  56. package/package.json +5 -5
@@ -2,43 +2,6 @@ import { nothing } from 'lit';
2
2
  import { styleMap } from 'lit/directives/style-map.js';
3
3
  import { html } from 'lit/static-html.js';
4
4
  import { seriesIndex } from '../../mixins/mixin-chart-source/mixin-chart-source.js';
5
- /**
6
- * What a pointer resting on a reading shows, as `Tooltip` pointed at a mark.
7
- *
8
- * **A chart does not draw its own tooltip, and the top layer is why.** `Tooltip`
9
- * renders `popover="manual"` through `PopoverController`, so it clears every
10
- * stacking context and every `overflow` on the page. A box drawn by the chart is
11
- * `position: absolute` inside the plot and is confined to the chart's own
12
- * context, so it is clipped by, or layered under, whatever sits beside the
13
- * chart. Escaping that means reimplementing `PopoverController`, which is
14
- * `Tooltip` with extra steps and a second copy of the hard part.
15
- *
16
- * **`for` was already the anchor and needed nothing added.** It resolves through
17
- * `getRootNode().getElementById()`, and a chart's shadow root *is* a
18
- * `ShadowRoot`, so a chart names an element inside itself and `Tooltip` finds
19
- * it. What the chart provides is an invisible mark at the reading's point.
20
- *
21
- * **An HTML mark rather than the `<rect>` or `<circle>` already there.**
22
- * `FloatingController` types its anchor as `HTMLElement` and an SVG element is
23
- * not one; and a slice's `<circle>` has the bounding box of the whole ring
24
- * rather than of its arc, so anchoring to it would point at the ring's center
25
- * for every slice.
26
- *
27
- * **And the arrow now follows from placement rather than from a compass.** The
28
- * chart says which side it prefers and `FloatingController` moves it if that
29
- * side does not fit, which is how a slice at any bearing gets a bubble that
30
- * points at it. Four hard-coded sides could not express a bearing, which is what
31
- * made the donut's arrow miss its own ring.
32
- *
33
- * **It shows the table's text and not the chart's numbers.** `$128k` is what the
34
- * cell says and `128000` is what the drawing measured. Every cell of the row is
35
- * here, including columns nothing was plotted from, because a note beside the
36
- * figures is most of the reason to want a tooltip.
37
- *
38
- * **Hidden from assistive technology, on purpose.** The table is the accessible
39
- * representation and the drawing is `aria-hidden`; a reader on a keyboard gets
40
- * the table, which they can reach, rather than a tooltip which they cannot.
41
- */
42
5
  export const drawTooltip = (rows, at, tag, where) => {
43
6
  const row = rows[at];
44
7
  if (!row || rows.length === 0) {
@@ -50,10 +13,6 @@ export const drawTooltip = (rows, at, tag, where) => {
50
13
  x: `${at * slot + slot / 2}%`,
51
14
  y: '0%',
52
15
  };
53
- /* The mark and the bubble are siblings, and the id is fixed rather than
54
- generated: it is scoped to this chart's shadow root, where it is the only
55
- one, so nothing can collide with it and nothing has to be threaded through
56
- a render to stay in step. */
57
16
  return html `<div
58
17
  id="helix-chart-mark"
59
18
  part="mark"
@@ -69,15 +28,7 @@ export const drawTooltip = (rows, at, tag, where) => {
69
28
  >
70
29
  <div aria-hidden="true" slot="content">
71
30
  <span part="tooltip-label">${row.label}</span>
72
- ${row.cells.map((cell, index) =>
73
- /* A palette color only where something was plotted in it.
74
- *
75
- * A cell with no number is a column no chart drew: a note, a status,
76
- * a name. Given `data-series` anyway it takes a series color and the
77
- * stylesheet draws a dot, so a row reading `Note Outage` arrived
78
- * wearing series three's green while the legend beside the chart said
79
- * series three was something else. */
80
- html `<span
31
+ ${row.cells.map((cell, index) => html `<span
81
32
  data-series=${cell.value === undefined ? nothing : seriesIndex(index)}
82
33
  part="tooltip-cell"
83
34
  >
@@ -1,35 +1,10 @@
1
- /**
2
- * A timestamp read the way a feed reads one: short, and a date once short stops
3
- * meaning anything.
4
- *
5
- * **The compact form is ambiguous past a week, and that is why it stops.**
6
- * `Intl.NumberFormat` with `unitDisplay: 'narrow'` is the only locale-aware way
7
- * to get "42m" rather than "42 minutes ago", and measured across locales it
8
- * renders both `minute` and `month` as `m` in en-US: "3m" is three minutes or
9
- * three months and a reader cannot tell which. `Intl.RelativeTimeFormat` in its
10
- * narrow style disambiguates the month as "3mo" and keeps an affix that cannot
11
- * be removed across locales, German prefixing "vor" and Japanese suffixing 前.
12
- *
13
- * So the compact form covers the range where it is unambiguous, the units up to
14
- * a week, and past that this returns a date. Nothing is guessed and nothing is
15
- * hand-written: the short units come from `Intl.NumberFormat`, the dates from
16
- * `Intl.DateTimeFormat`, and both answer to the caller's locale.
17
- *
18
- * Cached per locale for the reason `relativeTime` and `formatNumbers` are: the
19
- * constructors are expensive, the calls are cheap, and a feed of fifty items
20
- * formats fifty times per update.
21
- */
22
1
  const held = new Map();
23
2
  const dated = new Map();
24
- /** The units the compact form covers, largest last. `week` is deliberately not
25
- among them: past six days this returns a date instead, which is both shorter
26
- to read and the only reading that cannot be mistaken for minutes. */
27
3
  const UNITS = [
28
4
  { ms: 60_000, unit: 'minute' },
29
5
  { ms: 3_600_000, unit: 'hour' },
30
6
  { ms: 86_400_000, unit: 'day' },
31
7
  ];
32
- /** Where the compact form gives out, in milliseconds. Seven days. */
33
8
  const HORIZON = 604_800_000;
34
9
  const compact = (locale) => {
35
10
  let found = held.get(locale);
@@ -43,9 +18,6 @@ const compact = (locale) => {
43
18
  }
44
19
  return found;
45
20
  };
46
- /** Same day and month for a timestamp inside the year, month and year outside
47
- it: "12 Mar" says everything while the year is understood, and "Mar 2023" is
48
- what a reader wants once it is not. */
49
21
  const calendar = (locale) => {
50
22
  let found = dated.get(locale);
51
23
  if (!found) {
@@ -57,21 +29,9 @@ const calendar = (locale) => {
57
29
  }
58
30
  return found;
59
31
  };
60
- /**
61
- * A formatter for one locale, kept.
62
- *
63
- * `now` is a parameter rather than read from the clock, for the reason
64
- * `relativeTime` gives: a function that reads `Date.now()` internally can only
65
- * be tested by waiting or by replacing a global, and both produce specs that
66
- * fail on a slow machine.
67
- */
68
32
  export const elapsedTime = (locale) => {
69
33
  const short = compact(locale);
70
34
  const dates = calendar(locale);
71
- /* "now" rather than a count of seconds, and taken from `Intl` rather than
72
- written here so it is a word in the reader's language. Under a minute a feed
73
- that reports 3, then 4, then 5 seconds is a feed moving under somebody
74
- trying to read it, and nobody acts differently at seven seconds than four. */
75
35
  const zero = new Intl.RelativeTimeFormat(locale || undefined, { numeric: 'auto' });
76
36
  return (from, now) => {
77
37
  const elapsed = now.getTime() - from.getTime();
@@ -80,14 +40,8 @@ export const elapsedTime = (locale) => {
80
40
  }
81
41
  const size = Math.abs(elapsed);
82
42
  if (size < 60_000) {
83
- /* Floored like every other branch. At 59,999 milliseconds the remainder is
84
- one, and a caller handed a one is re-armed inside the callback that
85
- fired it. */
86
43
  return { next: Math.max(1000, 60_000 - size), text: zero.format(0, 'second') };
87
44
  }
88
- /* A date, and it will not change again. `Infinity` is the whole point of
89
- saying so: the caller schedules nothing, so a feed of old notifications
90
- left open overnight wakes for none of them. */
91
45
  if (size >= HORIZON) {
92
46
  const sameYear = from.getFullYear() === now.getFullYear();
93
47
  const format = sameYear ? dates[0] : dates[1];
@@ -101,11 +55,6 @@ export const elapsedTime = (locale) => {
101
55
  }
102
56
  const chosen = UNITS[index];
103
57
  const count = Math.trunc(size / chosen.ms);
104
- /* Time until the count ticks over, which is the remainder of the current
105
- unit rather than the whole of one: read at 90 seconds an item is "1m" for
106
- another 30 seconds, not another 60. Capped where `relativeTime` caps it,
107
- and floored at a second so nobody is handed a zero and schedules a busy
108
- loop. */
109
58
  const remaining = Math.min(chosen.ms - (size % chosen.ms), HORIZON - size);
110
59
  return {
111
60
  next: Math.max(1000, Math.min(remaining, 3_600_000)),
@@ -1,11 +1,3 @@
1
- /**
2
- * Elements the platform will focus, without naming a single component.
3
- *
4
- * Custom elements are covered separately below: one with `delegatesFocus` is
5
- * focusable as a host, and that is readable from outside through `shadowRoot`.
6
- * Listing tag names instead would mean a utility in `core` that only worked for
7
- * the components `core` happens to know about.
8
- */
9
1
  const NATIVE_FOCUSABLE = [
10
2
  'a[href]',
11
3
  'button:not([disabled])',
@@ -15,40 +7,8 @@ const NATIVE_FOCUSABLE = [
15
7
  '[contenteditable]:not([contenteditable="false"])',
16
8
  '[tabindex]:not([tabindex="-1"])',
17
9
  ].join(',');
18
- /**
19
- * Everything inside `root` the reader can reach with Tab, in document order.
20
- *
21
- * Separate from the focus trap because finding is the half most callers want:
22
- * roving tabindex, arrow-key navigation and "focus the first thing" all need
23
- * this list and none of them traps anything.
24
- *
25
- * Visibility is tested with `getClientRects()` rather than `offsetParent`,
26
- * which is null for anything inside a `position: fixed` container. A drawer is
27
- * exactly that, so the `offsetParent` test excluded every control in the thing
28
- * being trapped.
29
- */
30
- export const focusableElements = (root) =>
31
- /* One pass asking both questions, rather than two passes concatenated.
32
- *
33
- * Two lists joined end to end is not document order: every native element
34
- * followed by every delegating host comes out `b d a c` for a tree holding
35
- * `a b c d`, measured. The trap reads `items[0]` and `items.at(-1)` as its
36
- * boundaries, so a surface whose last control is a plain `<button>` and whose
37
- * first is a Helix component has a last that is not last, forward Tab off the
38
- * real end does not wrap, and focus leaves the dialog.
39
- *
40
- * It is invisible in a set that is all one kind, which is what every example
41
- * and every spec had. A single filtered walk cannot express the fault, and
42
- * dedup comes free with it: a delegating host that also matches the native
43
- * selector is visited once because it is one node. */
44
- [...root.querySelectorAll('*')].filter((node) => (node.matches(NATIVE_FOCUSABLE) || node.shadowRoot?.delegatesFocus === true) &&
10
+ export const focusableElements = (root) => [...root.querySelectorAll('*')].filter((node) => (node.matches(NATIVE_FOCUSABLE) || node.shadowRoot?.delegatesFocus === true) &&
45
11
  node.getClientRects().length > 0);
46
- /**
47
- * The truly focused element, following shadow roots down.
48
- *
49
- * `document.activeElement` stops at the outermost host, so a button inside two
50
- * shadow roots reports as the component rather than as itself.
51
- */
52
12
  export const deepActiveElement = () => {
53
13
  let active = document.activeElement;
54
14
  while (active?.shadowRoot?.activeElement) {
@@ -56,24 +16,6 @@ export const deepActiveElement = () => {
56
16
  }
57
17
  return active;
58
18
  };
59
- /**
60
- * Whether `node` sits inside `root`, following shadow boundaries upward.
61
- *
62
- * `Node.contains` walks `parentNode` and a shadow root's `parentNode` is null,
63
- * so it answers false for anything inside a component. Paired with
64
- * `deepActiveElement`, which returns exactly such a node, that pairing is a trap
65
- * of its own: the two look like they belong together and one of them crosses.
66
- *
67
- * `contains(deepActiveElement())` is the shape to avoid: focus on a
68
- * `helix-button` reports as the `<button>` in its shadow root, which is in no
69
- * light-DOM container anywhere, so a trap written that way reads every reader
70
- * as already outside it.
71
- *
72
- * `host` is reached without naming `ShadowRoot`, which the server has no global
73
- * for. This runs from a keydown handler and would never execute there, but a
74
- * bare reference is the kind that surfaces as a crash in Lit's result collector
75
- * the first time something calls it early.
76
- */
77
19
  export const deepContains = (root, node) => {
78
20
  let current = node;
79
21
  while (current) {
@@ -1,19 +1,3 @@
1
- /**
2
- * A number formatter for a locale, built once and kept.
3
- *
4
- * `Intl.NumberFormat` is expensive to construct and cheap to call, and a
5
- * pagination renders its count on every update, so building one per render is
6
- * the whole cost of the component.
7
- *
8
- * Keyed on the locale, so a component that never changes locale builds one
9
- * formatter for its life and one that does builds a second and drops the first.
10
- * Shared across components rather than per instance: a table of a hundred rows
11
- * asking for the same locale gets the same object, which is what a cache is
12
- * for and what a private field per element cannot do.
13
- *
14
- * An empty string means the runtime's own, which is what every caller was
15
- * already passing `|| undefined` to say.
16
- */
17
1
  const held = new Map();
18
2
  export const formatNumbers = (locale) => {
19
3
  let format = held.get(locale);
@@ -1,29 +1,3 @@
1
- /**
2
- * Writes a host's semantics onto its `ElementInternals`.
3
- *
4
- * Onto internals rather than onto the host, which is the whole point: an
5
- * attribute written at upgrade is markup a React consumer's server output never
6
- * had, and hydration reports the difference. Internals carry the same meaning
7
- * to the accessibility tree and leave the element as the consumer wrote it.
8
- *
9
- * **Empty means absent, and that is the trap this exists for.** Assigning `''`
10
- * to `ariaLabel` is not the same as not naming the element: it is a name of no
11
- * characters, which a screen reader announces as an unnamed control while every
12
- * check for "is it labeled" passes. Clearing is `null`, and this is the one
13
- * place that has to remember it.
14
- *
15
- * **Element references are feature-tested rather than assumed.** They are the
16
- * only way a host in the light DOM can point at a node in its own shadow root,
17
- * an IDREF cannot cross that boundary, and the plain `element.aria*Elements`
18
- * reflection drops the reference where `ElementInternals` keeps it. Older
19
- * engines have neither, and assigning to a property they do not implement is a
20
- * silent no-op rather than an error, so the guard is what makes the absence
21
- * visible to anything reading this.
22
- *
23
- * Omitted keys are left alone, so a component that sets its role once in
24
- * `willUpdate` and its references later in `updated` does not have to repeat
25
- * itself or risk clearing what the other call wrote.
26
- */
27
1
  export const hostAria = (internals, semantics) => {
28
2
  if (semantics.role !== undefined) {
29
3
  internals.role = semantics.role === '' ? null : semantics.role;
@@ -1,31 +1,4 @@
1
- /**
2
- * What each item declared for itself, before any group wrote to it.
3
- *
4
- * A `WeakMap` because the alternative is reading the attribute back, and the
5
- * attribute is the thing being written: `disabled` reflects, so the second pass
6
- * would read what the first pass wrote and the item would never come back.
7
- */
8
1
  const own = new WeakMap();
9
- /**
10
- * Disables a group's members without forgetting what they said themselves.
11
- *
12
- * A group and its members are two sources for one fact: a member is out of play
13
- * if either says so. Written the obvious way that is
14
- * `item.disabled = group || item.disabled`, and it latches. The first pass with
15
- * the group disabled writes `true` onto every member; the next pass reads its
16
- * own writing back as the member's own answer; and enabling the group leaves
17
- * every member disabled for good. Nothing about it is visible until something
18
- * re-enables a group, which is rare enough to ship and miserable to find.
19
- *
20
- * So what a member said itself is recorded before it is first overridden, and
21
- * given back when the group lets go. Restoring is the half that matters: a
22
- * version that only recorded would read the overridden value as the member's
23
- * own the moment the group was enabled, which is the same latch by a longer
24
- * route.
25
- *
26
- * Once given back, the record is dropped, so a consumer disabling a member
27
- * afterwards is its own answer again and is left alone.
28
- */
29
2
  export const inheritDisabled = (disabled, items) => {
30
3
  for (const item of items) {
31
4
  if (disabled) {
@@ -35,21 +8,12 @@ export const inheritDisabled = (disabled, items) => {
35
8
  item.disabled = true;
36
9
  continue;
37
10
  }
38
- /* Only where this held it down. An item nothing overrode is the consumer's
39
- business and is not touched, which is what keeps a group that was never
40
- disabled from writing to its members at all. */
41
11
  if (own.has(item)) {
42
12
  item.disabled = own.get(item) ?? false;
43
13
  own.delete(item);
44
14
  }
45
15
  }
46
16
  };
47
- /**
48
- * Forgets what a group recorded about an item.
49
- *
50
- * For an item leaving a group, which would otherwise carry the group's last
51
- * word into wherever it goes next.
52
- */
53
17
  export const forgetDisabled = (item) => {
54
18
  own.delete(item);
55
19
  };
@@ -1,21 +1,4 @@
1
- /** Two decimals, which is more than a 100 unit view box can show. */
2
1
  const round = (value) => Math.round(value * 100) / 100;
3
- /**
4
- * Tangents that cannot overshoot, by Fritsch and Carlson.
5
- *
6
- * **The reason `smooth` is monotone cubic and not a Catmull-Rom spline, which
7
- * is the shorter code.** A spline through the points is free to bulge past them
8
- * between two of them, so a series whose lowest reading is 40 draws a curve
9
- * that dips to 32 on the way there. Every value on that curve is a claim about
10
- * a measurement, and the ones between the readings are the chart's own
11
- * invention either way; a curve that stays inside the data at least invents
12
- * nothing the data contradicts.
13
- *
14
- * Two rules do it. A tangent at a turning point is flat, so the curve arrives
15
- * and leaves level rather than carrying on past the peak; and where a tangent
16
- * is steep enough relative to its neighbors to bulge anyway, the pair is
17
- * scaled back until it cannot.
18
- */
19
2
  const tangents = (xs, ys) => {
20
3
  const count = xs.length;
21
4
  const secant = [];
@@ -29,7 +12,6 @@ const tangents = (xs, ys) => {
29
12
  for (let index = 1; index < count - 1; index += 1) {
30
13
  const before = secant[index - 1];
31
14
  const after = secant[index];
32
- // Flat at a turning point, which is what keeps the curve off the outside.
33
15
  slope[index] = before * after <= 0 ? 0 : (before + after) / 2;
34
16
  }
35
17
  for (let index = 0; index < count - 1; index += 1) {
@@ -50,24 +32,6 @@ const tangents = (xs, ys) => {
50
32
  }
51
33
  return slope;
52
34
  };
53
- /**
54
- * Builds an SVG path through a set of points.
55
- *
56
- * Shared by every chart that draws a line, so the choice of curve is one
57
- * implementation rather than one per chart. Pure: it takes points and returns a
58
- * string, and knows nothing about elements or lifecycles.
59
- *
60
- * `straight` joins the readings and claims nothing between them, which is why
61
- * it is the default. `smooth` is easier to follow across a long series and
62
- * invents the values in between, so it is a reading aid rather than a neutral
63
- * one. `step` claims the opposite, that a reading holds until the next one, and
64
- * changes at the midpoint rather than at either end, since a chart's x axis is
65
- * an order and neither neighbor has a better claim on the moment it moved.
66
- *
67
- * Fewer than two points is an empty string rather than a partial path: one
68
- * reading is not a line, and `M0,0` alone draws nothing while still being a
69
- * path element that looks like it should.
70
- */
71
35
  export const linePath = (points, curve = 'straight') => {
72
36
  if (points.length < 2) {
73
37
  return '';
@@ -1,27 +1,4 @@
1
- /**
2
- * For a boolean property that is on unless a consumer says otherwise.
3
- *
4
- * **Lit's boolean converter cannot express false, and that is the whole
5
- * problem.** It answers `value !== null`, so an attribute is true by being
6
- * there: `modal` is true, `modal=""` is true, and `modal="false"` is true as
7
- * well, because it is present. A property defaulting to `true` therefore has no
8
- * attribute that turns it off at all, and a consumer who writes the obvious
9
- * thing gets the opposite of what they asked for, silently.
10
- *
11
- * So: absent is the default, present is on, and the one string that means off
12
- * is the one a consumer would write. `"false"` only, and not `"0"` or `"no"`,
13
- * because a converter that accepts a vocabulary invites a consumer to guess at
14
- * it, and the one they guess wrong is the one that silently means on.
15
- *
16
- * **The other answer is to invert the property, and it is worse.** `Drawer`
17
- * would take `modeless`, absent meaning modal, which satisfies the rule that a
18
- * reflected property takes no default. It also renames the thing every consumer
19
- * already writes, to a word nobody reaches for, so that markup can express a
20
- * state that is rare. The name stays and the attribute learns to say no.
21
- */
22
1
  export const onUnlessFalse = {
23
2
  fromAttribute: (value) => value !== null && value !== 'false',
24
- /* Written out so it survives being reflected later. `''` rather than
25
- `'true'`, which is what a boolean attribute looks like everywhere else. */
26
3
  toAttribute: (value) => (value ? '' : 'false'),
27
4
  };
@@ -1,32 +1,9 @@
1
- /* A grid's own rows and cells, by role and by tag.
2
- *
3
- * **Both, because `helix-table` sets its roles through `ElementInternals`.**
4
- * That writes no attribute at all, which is the whole point of it: the
5
- * semantics reach the accessibility tree and never the DOM. So a selector of
6
- * `[role="row"]` matches a hand-rolled grid and matches nothing at all in this
7
- * library's own table, and a selector of the tag matches ours and nothing
8
- * anybody else builds. Neither alone is the answer. */
9
1
  const ROWS = 'helix-table-row, helix-headless-table-row, [role="row"]';
10
2
  const CELLS = 'helix-table-cell, helix-headless-table-cell, [role="cell"], [role="gridcell"], [role="rowheader"]';
11
3
  const COLUMNS = 'helix-table-column, helix-headless-table-column, [role="columnheader"]';
12
- /** Whether an element is a grid rather than a document table. */
13
4
  const isGrid = (element) => element.getAttribute('role') === 'table' ||
14
5
  element.getAttribute('role') === 'grid' ||
15
6
  /^helix-(headless-)?table$/i.test(element.tagName);
16
- /**
17
- * Reads a grid built from roles rather than from table markup.
18
- *
19
- * The shape is the same question asked of different elements: a first row of
20
- * column headers naming the series, a first cell per row labeling it, and
21
- * numbers in the rest. `helix-table` puts its columns beside its rows rather
22
- * than inside a header row, so the columns are looked for in the whole grid and
23
- * the rows are read separately.
24
- *
25
- * **A paginated grid reads as the page it is showing.** That is worth knowing
26
- * before pointing a chart at one: a table element holds every row it has, and a
27
- * grid may be holding twenty of two thousand. The chart is not wrong, the
28
- * question was.
29
- */
30
7
  const readGrid = (grid) => {
31
8
  const columns = [...grid.querySelectorAll(COLUMNS)];
32
9
  const rows = [...grid.querySelectorAll(ROWS)];
@@ -59,36 +36,7 @@ const readGrid = (grid) => {
59
36
  }),
60
37
  }));
61
38
  };
62
- /* A column with no numbers in it is not a series.
63
- *
64
- * Both readers build one entry per data column and then drop the rows whose
65
- * cells will not parse, so a column of notes arrives here as a series with a
66
- * name and no points. Nothing draws it, so it is invisible until `Legend` names
67
- * it anyway: a swatch in the palette against a column no chart plotted, and
68
- * every series after it shifted a color. */
69
39
  const plotted = (series) => series.filter((one) => one.points.length > 0);
70
- /**
71
- * Reads a table as series, one per data column.
72
- *
73
- * **The table is the chart's accessible representation, not a fallback for
74
- * one.** Every chart here takes its numbers from real markup rather than from a
75
- * property, so the page is readable with no script at all, sortable by anything
76
- * that sorts tables, and correct in a screen reader without a parallel
77
- * description that has to be kept in step by hand. A `values` array would be
78
- * easier to pass and would make the accessible version the thing nobody
79
- * maintains.
80
- *
81
- * The shape is the ordinary one: a header row naming the series, a first column
82
- * of row headers, and numbers in the rest. A single-column table has one series
83
- * and takes its name from the caption, since a header row of one is usually
84
- * absent.
85
- *
86
- * `data-value` wins over the cell's text, because `$1.2k` is what a reader
87
- * should see and is not a number. `Number()` rather than `parseFloat`, because
88
- * the second reads `12abc` as 12: a value that is nearly a number is a mistake
89
- * worth dropping rather than rounding off, and a dropped point is visible where
90
- * a silently truncated one is not.
91
- */
92
40
  export const readSeries = (table) => {
93
41
  if (isGrid(table)) {
94
42
  return plotted(readGrid(table));
@@ -124,25 +72,6 @@ export const readSeries = (table) => {
124
72
  }),
125
73
  })));
126
74
  };
127
- /**
128
- * Reads a table as rows rather than as columns.
129
- *
130
- * **Everything `readSeries` throws away on purpose, a tooltip needs.** That
131
- * function answers "what should I draw", so it keeps the parsed number and
132
- * discards the cell's text, and it drops a column whose cells are not numbers
133
- * at all. A chart drawing `128000` and a tooltip saying `128000` would be the
134
- * chart overruling the table, which already decided that number reads `$128k`;
135
- * and a column of notes beside the figures is exactly the extra a reader wants
136
- * when their pointer is resting on a reading.
137
- *
138
- * So this keeps both, and keeps the columns nothing plotted. A chart of two
139
- * series over a table of four columns still has four columns to show.
140
- *
141
- * The traversal is the same shape as `readSeries` and deliberately separate.
142
- * Folding them into one pass returning both would make every caller who wants
143
- * one build the other, and the two are asked for at different moments: series
144
- * on render, rows only where a pointer has stopped.
145
- */
146
75
  export const readRows = (table) => {
147
76
  const cell = (name, element) => {
148
77
  if (!element) {
@@ -1,21 +1,4 @@
1
- /**
2
- * A timestamp read the way a person reads one, and the moment it stops being true.
3
- *
4
- * **The second half is the point.** "2 minutes ago" is correct for sixty seconds
5
- * and wrong for the rest of the session, and a feed a user leaves open is
6
- * exactly where that shows: every item drifts, quietly, and the panel ends up
7
- * describing when the page loaded rather than when anything happened. So this
8
- * returns the text and the milliseconds until that text changes, which is what
9
- * lets a caller schedule one update instead of polling and lets a page hold one
10
- * timer instead of one per item.
11
- *
12
- * `Intl.RelativeTimeFormat` with `numeric: 'auto'` is what produces "yesterday"
13
- * rather than "1 day ago", which is what a reader would say. Cached per locale
14
- * for the reason `formatNumbers` is: the constructor is expensive, the call is
15
- * cheap, and a feed of fifty items formats fifty times per update.
16
- */
17
1
  const held = new Map();
18
- /** How long each unit lasts, in milliseconds, largest last. */
19
2
  const UNITS = [
20
3
  { ms: 1000, unit: 'second' },
21
4
  { ms: 60_000, unit: 'minute' },
@@ -25,14 +8,6 @@ const UNITS = [
25
8
  { ms: 2_629_800_000, unit: 'month' },
26
9
  { ms: 31_557_600_000, unit: 'year' },
27
10
  ];
28
- /**
29
- * A formatter for one locale, kept.
30
- *
31
- * `now` is a parameter rather than read from the clock, which is what makes this
32
- * testable at all: a function that reads `Date.now()` internally can only be
33
- * tested by waiting or by replacing a global, and both produce specs that fail
34
- * on a slow machine.
35
- */
36
11
  export const relativeTime = (locale) => {
37
12
  let format = held.get(locale);
38
13
  if (!format) {
@@ -41,27 +16,12 @@ export const relativeTime = (locale) => {
41
16
  }
42
17
  const found = format;
43
18
  return (from, now) => {
44
- /* Signed from the timestamp's point of view, not the reader's:
45
- `Intl.RelativeTimeFormat` takes a negative count for the past, so
46
- `format(-2, 'minute')` is "2 minutes ago" and `format(2, 'minute')` is "in
47
- 2 minutes". Computed as `now - from` the sign is backwards and every past
48
- item reads as the future, which is the shape of it that looks like a
49
- working feed until you read one. */
50
19
  const elapsed = from.getTime() - now.getTime();
51
20
  if (!Number.isFinite(elapsed)) {
52
21
  return { next: Number.POSITIVE_INFINITY, text: '' };
53
22
  }
54
- /* Under a minute is "now" rather than a count of seconds. A feed that
55
- reports 3, then 4, then 5 seconds ago is a feed that moves under the
56
- reader while they are trying to read it, and the precision is worth
57
- nothing: nobody acts differently at seven seconds than at four. */
58
23
  const size = Math.abs(elapsed);
59
24
  if (size < 60_000) {
60
- /* Floored, as `Relative.next` says every branch is: at 59,999
61
- milliseconds the remainder is one, so a caller scheduling for itself is
62
- re-armed inside the callback that fired it. The shared ticker's own
63
- `Math.max(1000, ...)` hides this from the two readers that go through
64
- it. */
65
25
  return { next: Math.max(1000, 60_000 - size), text: found.format(0, 'second') };
66
26
  }
67
27
  let chosen = UNITS[1];
@@ -71,9 +31,6 @@ export const relativeTime = (locale) => {
71
31
  }
72
32
  }
73
33
  const count = Math.trunc(elapsed / chosen.ms);
74
- /* Time until the count ticks over, which is the remainder of the current
75
- unit rather than the whole of one: an item read at 90 seconds is "1 minute
76
- ago" for another 30 seconds, not another 60. */
77
34
  const remaining = chosen.ms - (size % chosen.ms);
78
35
  return {
79
36
  next: Math.max(1000, Math.min(remaining, 3_600_000)),
@@ -1,21 +1 @@
1
- /**
2
- * Which item a value names, or the first that could stand in for it.
3
- *
4
- * `items.find(match) ?? items[0]` was written by hand in four components and it
5
- * is not the obvious line it looks like. It is what makes a set whose `value`
6
- * names nothing still show something, which is every set before its consumer
7
- * has chosen and every set whose current item has just been removed.
8
- *
9
- * The fallback skips disabled items and the match does not. A consumer who
10
- * names a disabled item meant it, and quietly moving to another would be the
11
- * component overruling them; but falling back *onto* a disabled item is the
12
- * component choosing, and it should not choose something out of play. Taking
13
- * `items[0]` either way opens a set on a disabled first item and shows it as
14
- * current.
15
- *
16
- * A plain function rather than only a method on `MixinControllerActive`,
17
- * because the components that need it are split across two mixins: a set whose
18
- * value is submitted takes `MixinControllerValue` and cannot also take the
19
- * active one without two properties called `value`.
20
- */
21
1
  export const resolveActiveItem = (value, items) => items.find((item) => item.value === value) ?? items.find((item) => !item.disabled);