@fusion.dev/helix-core 0.142.1 → 0.144.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 (112) hide show
  1. package/custom-elements.json +10436 -3804
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +65 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +77 -16
  4. package/dist/controllers/arrival-controller/arrival-controller.d.ts +12 -0
  5. package/dist/controllers/arrival-controller/arrival-controller.js +10 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +99 -0
  7. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +107 -2
  8. package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
  9. package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
  10. package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
  11. package/dist/controllers/copy-controller/copy-controller.js +16 -9
  12. package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
  13. package/dist/controllers/delay-controller/delay-controller.js +54 -0
  14. package/dist/controllers/dismiss-controller/dismiss-controller.js +40 -3
  15. package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
  16. package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
  17. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +79 -1
  18. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +80 -10
  19. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
  20. package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
  21. package/dist/controllers/popover-controller/popover-controller.d.ts +11 -0
  22. package/dist/controllers/popover-controller/popover-controller.js +6 -0
  23. package/dist/controllers/source-controller/source-controller.d.ts +38 -0
  24. package/dist/controllers/source-controller/source-controller.js +71 -2
  25. package/dist/controllers/status-controller/status-controller.d.ts +47 -0
  26. package/dist/controllers/status-controller/status-controller.js +34 -0
  27. package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
  28. package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
  29. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +16 -0
  30. package/dist/controllers/virtualize-controller/virtualize-controller.js +24 -2
  31. package/dist/events/events.d.ts +137 -4
  32. package/dist/events/events.js +96 -0
  33. package/dist/helix-element/react-bridge.js +6 -4
  34. package/dist/helix-form-element/helix-form-element.d.ts +131 -2
  35. package/dist/helix-form-element/helix-form-element.js +216 -6
  36. package/dist/index.d.ts +30 -9
  37. package/dist/index.js +29 -8
  38. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +15 -2
  39. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +9 -1
  40. package/dist/mixins/mixin-collection/mixin-collection.d.ts +4 -2
  41. package/dist/mixins/mixin-collection/mixin-collection.js +8 -6
  42. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
  43. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
  44. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +19 -3
  45. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
  46. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +43 -0
  47. package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
  48. package/dist/mixins/mixin-field/mixin-field.js +46 -8
  49. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +23 -8
  50. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +25 -1
  51. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +29 -5
  52. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +94 -1
  53. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
  54. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
  55. package/dist/utils/arrow/arrow.d.ts +15 -0
  56. package/dist/utils/arrow/arrow.js +2 -0
  57. package/dist/utils/audit-name/audit-name.d.ts +28 -0
  58. package/dist/utils/audit-name/audit-name.js +95 -0
  59. package/dist/utils/calendar/calendar.d.ts +117 -0
  60. package/dist/utils/calendar/calendar.js +96 -0
  61. package/dist/utils/chord/chord.d.ts +43 -0
  62. package/dist/utils/chord/chord.js +53 -0
  63. package/dist/utils/clock/clock.d.ts +87 -0
  64. package/dist/utils/clock/clock.js +68 -0
  65. package/dist/utils/color-name/color-name.d.ts +28 -0
  66. package/dist/utils/color-name/color-name.js +150 -0
  67. package/dist/utils/count-label/count-label.d.ts +13 -0
  68. package/dist/utils/count-label/count-label.js +5 -0
  69. package/dist/utils/css-color/css-color.d.ts +12 -0
  70. package/dist/utils/css-color/css-color.js +2 -2
  71. package/dist/utils/date-segments/date-segments.d.ts +122 -0
  72. package/dist/utils/date-segments/date-segments.js +206 -0
  73. package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
  74. package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
  75. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -18
  76. package/dist/utils/draw-tooltip/draw-tooltip.js +16 -5
  77. package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
  78. package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
  79. package/dist/utils/follow-link/follow-link.d.ts +46 -0
  80. package/dist/utils/follow-link/follow-link.js +37 -0
  81. package/dist/utils/heading-in/heading-in.d.ts +19 -0
  82. package/dist/utils/heading-in/heading-in.js +14 -0
  83. package/dist/utils/number-format/number-format.d.ts +23 -0
  84. package/dist/utils/number-format/number-format.js +55 -0
  85. package/dist/utils/platform/platform.d.ts +23 -0
  86. package/dist/utils/platform/platform.js +11 -0
  87. package/dist/utils/read-color/read-color.d.ts +14 -0
  88. package/dist/utils/read-color/read-color.js +13 -0
  89. package/dist/utils/read-series/read-series.d.ts +34 -0
  90. package/dist/utils/read-series/read-series.js +32 -4
  91. package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
  92. package/dist/utils/reflect-elements/reflect-elements.js +40 -0
  93. package/dist/utils/relative-time/relative-time.d.ts +13 -1
  94. package/dist/utils/relative-time/relative-time.js +7 -4
  95. package/dist/utils/report-runtime/report-runtime.d.ts +12 -0
  96. package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
  97. package/dist/utils/roving-stop/roving-stop.js +3 -0
  98. package/dist/utils/scroll-lock/scroll-lock.d.ts +16 -0
  99. package/dist/utils/scroll-lock/scroll-lock.js +30 -0
  100. package/dist/utils/swipe/swipe.d.ts +63 -0
  101. package/dist/utils/swipe/swipe.js +48 -0
  102. package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
  103. package/dist/utils/tab-stops/tab-stops.js +22 -0
  104. package/dist/utils/value-as/value-as.d.ts +20 -0
  105. package/dist/utils/value-as/value-as.js +16 -0
  106. package/dist/utils/write-time/write-time.d.ts +11 -1
  107. package/dist/utils/write-time/write-time.js +7 -7
  108. package/dist/utils/written-list/written-list.d.ts +29 -2
  109. package/dist/utils/written-list/written-list.js +11 -1
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +4 -4
@@ -1,8 +1,44 @@
1
1
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
2
  /** What the controller needs to know: which element inside is the control. */
3
3
  export interface AriaForwardOptions {
4
+ /**
5
+ * Whether a host `aria-label` names the control too.
6
+ *
7
+ * For a control named by its content, a button or a switch, which has no
8
+ * `label` to render it into. A field renders its `aria-label` as a clipped
9
+ * label already, so it leaves this off. A function is asked on every
10
+ * forward, for a subclass that names the control itself.
11
+ */
12
+ label?: boolean | (() => boolean);
13
+ /**
14
+ * Elements read after whatever names the control from outside, the host's
15
+ * `aria-labelledby` or its `<label>`.
16
+ *
17
+ * For a control whose name is the label and then what it shows, as a
18
+ * native `<select>` is named. Added only where the host gives a name, so a
19
+ * control named by its own template keeps its own.
20
+ */
21
+ labelledByAfter?: () => Element[];
22
+ /**
23
+ * Whether the control has a name of its own, where its own labels do not
24
+ * say.
25
+ *
26
+ * A file input is labeled by the trigger that opens its picker, "Choose
27
+ * files", which is an action rather than a name, so a page's `<label>`
28
+ * should still name it. Absent, the control's own `aria-label`,
29
+ * `aria-labelledby` and labels with words in them decide.
30
+ */
31
+ named?: () => boolean;
4
32
  /** The focusable thing in the shadow root, or null before it renders. */
5
33
  target: () => HTMLElement | null;
34
+ /**
35
+ * How to name the control, said in a warning when it has no accessible
36
+ * name once it has rendered. Absent, the name is not checked.
37
+ *
38
+ * For a control named by its content or a host attribute, as a button is. A
39
+ * field is checked by `MixinField`, which knows its label.
40
+ */
41
+ unnamed?: string;
6
42
  }
7
43
  /**
8
44
  * Carries a host's ARIA references down to the control inside it.
@@ -42,6 +78,13 @@ export interface AriaForwardOptions {
42
78
  * a framework rendering a tooltip after this element upgrades, or a consumer
43
79
  * pointing `aria-describedby` at a different id, changes an attribute and
44
80
  * nothing schedules a Lit update.
81
+ *
82
+ * **A `<label>` that names the host names the control too**, when the control
83
+ * has no name of its own. The platform gives a form-associated host its labels
84
+ * and the host is a generic the reader never lands on, so a `<label for>` or a
85
+ * wrapping `<label>` left the textbox unnamed. The labels travel the same way a
86
+ * forwarded `aria-labelledby` does, as element references, and are read again
87
+ * when focus arrives, since a label can be added without the host hearing.
45
88
  */
46
89
  export declare class AriaForwardController implements ReactiveController {
47
90
  private readonly host;
@@ -51,7 +94,27 @@ export declare class AriaForwardController implements ReactiveController {
51
94
  private readonly forwarded;
52
95
  /** The control the record above is about, so a replacement resets it. */
53
96
  private recorded;
97
+ /** Whether a host `aria-label` is carried down. See the option. */
98
+ private readonly asked;
99
+ private get label();
100
+ /** The component's own answer to whether the control is named. See the option. */
101
+ private readonly named;
102
+ /** What is read after the host's names. See the option. */
103
+ private readonly after;
104
+ /** How to name the control, where its name is checked. See the option. */
105
+ private readonly unnamed;
54
106
  constructor(host: ReactiveControllerHost & HTMLElement, options: AriaForwardOptions);
107
+ /**
108
+ * Whether the control has a name that did not come from this controller: an
109
+ * `aria-label`, an `aria-labelledby` of its own, or a label of its own with
110
+ * words in it.
111
+ */
112
+ private namedByItself;
113
+ /**
114
+ * The labels the platform associated with the host, for a control with no
115
+ * name of its own, and none otherwise.
116
+ */
117
+ private hostLabels;
55
118
  /**
56
119
  * Resolves the host's references and hands them to the control.
57
120
  *
@@ -65,5 +128,7 @@ export declare class AriaForwardController implements ReactiveController {
65
128
  forward(): void;
66
129
  hostConnected(): void;
67
130
  hostDisconnected(): void;
131
+ /** Forwards again as focus arrives, for a label added since the last update. */
132
+ private readonly refresh;
68
133
  hostUpdated(): void;
69
134
  }
@@ -1,16 +1,63 @@
1
+ import { auditName } from '../../utils/audit-name/audit-name.js';
1
2
  import { elementById } from '../../utils/element-by-id/element-by-id.js';
3
+ const shownText = (node) => {
4
+ if (node instanceof HTMLSlotElement) {
5
+ return node.assignedNodes({ flatten: true }).map(shownText).join('');
6
+ }
7
+ if (node.nodeType === Node.TEXT_NODE) {
8
+ return node.textContent ?? '';
9
+ }
10
+ if (node instanceof HTMLElement && node.hidden) {
11
+ return '';
12
+ }
13
+ return [...node.childNodes].map(shownText).join('');
14
+ };
2
15
  const RELATIONSHIPS = [
3
16
  ['aria-describedby', 'ariaDescribedByElements'],
4
17
  ['aria-labelledby', 'ariaLabelledByElements'],
5
18
  ];
6
- const STATES = ['aria-expanded', 'aria-haspopup', 'aria-controls', 'aria-invalid'];
19
+ const STATES = [
20
+ 'aria-expanded',
21
+ 'aria-haspopup',
22
+ 'aria-controls',
23
+ 'aria-invalid',
24
+ 'aria-pressed',
25
+ ];
7
26
  export class AriaForwardController {
27
+ get label() {
28
+ return typeof this.asked === 'function' ? this.asked() : this.asked;
29
+ }
8
30
  constructor(host, options) {
9
31
  this.forwarded = new Set();
32
+ this.refresh = () => {
33
+ this.forward();
34
+ };
10
35
  this.host = host;
11
36
  this.target = options.target;
37
+ this.asked = options.label ?? false;
38
+ this.named = options.named;
39
+ this.after = options.labelledByAfter;
40
+ this.unnamed = options.unnamed;
12
41
  host.addController(this);
13
42
  }
43
+ namedByItself(target) {
44
+ if (target.getAttribute('aria-label')) {
45
+ return true;
46
+ }
47
+ if (!this.forwarded.has('aria-labelledby') && target.hasAttribute('aria-labelledby')) {
48
+ return true;
49
+ }
50
+ const own = target.labels;
51
+ return [...(own ?? [])].some((label) => shownText(label).trim() !== '');
52
+ }
53
+ hostLabels(target) {
54
+ const labels = this.host.labels;
55
+ const named = this.named ? this.named() : this.namedByItself(target);
56
+ if (!labels || labels.length === 0 || named) {
57
+ return [];
58
+ }
59
+ return [...labels].filter((label) => label instanceof HTMLElement);
60
+ }
14
61
  forward() {
15
62
  const target = this.target();
16
63
  if (!target) {
@@ -20,17 +67,34 @@ export class AriaForwardController {
20
67
  this.forwarded.clear();
21
68
  this.recorded = target;
22
69
  }
70
+ for (const attribute of this.label ? [...STATES, 'aria-label'] : STATES) {
71
+ const value = this.host.getAttribute(attribute);
72
+ if (value === null) {
73
+ if (this.forwarded.has(attribute)) {
74
+ target.removeAttribute(attribute);
75
+ this.forwarded.delete(attribute);
76
+ }
77
+ continue;
78
+ }
79
+ target.setAttribute(attribute, value);
80
+ this.forwarded.add(attribute);
81
+ }
23
82
  for (const [attribute, property] of RELATIONSHIPS) {
24
83
  if (!(property in target)) {
25
84
  continue;
26
85
  }
27
86
  const ids = (this.host.getAttribute(attribute) ?? '').split(/\s+/).filter(Boolean);
28
- const elements = ids
87
+ const referenced = ids
29
88
  .map((id) => elementById(this.host, id))
30
89
  .filter((element) => element !== null);
90
+ const elements = referenced.length === 0 && attribute === 'aria-labelledby'
91
+ ? this.hostLabels(target)
92
+ : referenced;
31
93
  const reflect = target;
32
94
  if (elements.length > 0) {
33
- reflect[property] = elements;
95
+ const after = attribute === 'aria-labelledby' ? (this.after?.() ?? []) : [];
96
+ const named = elements;
97
+ reflect[property] = [...named, ...after.filter((one) => !named.includes(one))];
34
98
  this.forwarded.add(attribute);
35
99
  continue;
36
100
  }
@@ -39,30 +103,27 @@ export class AriaForwardController {
39
103
  this.forwarded.delete(attribute);
40
104
  }
41
105
  }
42
- for (const attribute of STATES) {
43
- const value = this.host.getAttribute(attribute);
44
- if (value === null) {
45
- if (this.forwarded.has(attribute)) {
46
- target.removeAttribute(attribute);
47
- this.forwarded.delete(attribute);
48
- }
49
- continue;
50
- }
51
- target.setAttribute(attribute, value);
52
- this.forwarded.add(attribute);
53
- }
54
106
  }
55
107
  hostConnected() {
56
108
  this.observer = new MutationObserver(() => this.forward());
57
109
  this.observer.observe(this.host, {
58
- attributeFilter: [...RELATIONSHIPS.map(([attribute]) => attribute), ...STATES],
110
+ attributeFilter: [
111
+ ...RELATIONSHIPS.map(([attribute]) => attribute),
112
+ ...STATES,
113
+ ...(this.asked === false ? [] : ['aria-label']),
114
+ ],
59
115
  });
116
+ this.host.addEventListener('focusin', this.refresh);
60
117
  }
61
118
  hostDisconnected() {
62
119
  this.observer?.disconnect();
63
120
  this.observer = undefined;
121
+ this.host.removeEventListener('focusin', this.refresh);
64
122
  }
65
123
  hostUpdated() {
66
124
  this.forward();
125
+ if (this.unnamed !== undefined) {
126
+ auditName(this.host, this.target, this.unnamed);
127
+ }
67
128
  }
68
129
  }
@@ -5,6 +5,11 @@ export interface ArrivalOptions {
5
5
  text: () => string;
6
6
  /** Whether the element is arriving now, rather than with the page. */
7
7
  arriving: () => boolean;
8
+ /**
9
+ * How long the region keeps the words once it has said them, in
10
+ * milliseconds. Defaults to seven seconds.
11
+ */
12
+ hold?: number;
8
13
  }
9
14
  /**
10
15
  * The words of an element that is announced when it appears, for a region that
@@ -24,6 +29,12 @@ export interface ArrivalOptions {
24
29
  * second time on the headless tier, which has no sheet to clip the region, and
25
30
  * a served page changed its picture on hydrating.
26
31
  *
32
+ * **And emptied again once it has been heard.** The words stay in the element
33
+ * itself, so a region that kept its copy put the message in the accessibility
34
+ * tree twice, and a reader moving through the page met it twice. The region
35
+ * keeps the copy for `hold` and then lets it go, which a polite region does not
36
+ * read out, removals being no news.
37
+ *
27
38
  * The component renders `said` into its region and calls `say` when what it
28
39
  * holds changes, from a `slotchange`.
29
40
  */
@@ -35,6 +46,7 @@ export declare class ArrivalController implements ReactiveController {
35
46
  /** The words last accounted for, said or not. */
36
47
  private heard;
37
48
  private frame;
49
+ private timer;
38
50
  constructor(host: ReactiveControllerHost, options: ArrivalOptions);
39
51
  hostConnected(): void;
40
52
  hostDisconnected(): void;
@@ -1,3 +1,4 @@
1
+ const HOLD = 7000;
1
2
  export class ArrivalController {
2
3
  constructor(host, options) {
3
4
  this.host = host;
@@ -22,6 +23,9 @@ export class ArrivalController {
22
23
  hostDisconnected() {
23
24
  cancelAnimationFrame(this.frame);
24
25
  this.frame = 0;
26
+ clearTimeout(this.timer);
27
+ this.timer = undefined;
28
+ this.said = '';
25
29
  }
26
30
  say() {
27
31
  const next = this.options.text();
@@ -31,5 +35,11 @@ export class ArrivalController {
31
35
  this.heard = next;
32
36
  this.said = next;
33
37
  this.host.requestUpdate();
38
+ clearTimeout(this.timer);
39
+ this.timer = setTimeout(() => {
40
+ this.timer = undefined;
41
+ this.said = '';
42
+ this.host.requestUpdate();
43
+ }, this.options.hold ?? HOLD);
34
44
  }
35
45
  }
@@ -1,4 +1,19 @@
1
1
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * The id every chart puts on its plot, and the boundary a bubble is kept inside.
4
+ *
5
+ * **A name six templates and two files have to agree on is a name that will
6
+ * stop agreeing**, which is `PALETTE`'s argument and it holds harder here: the
7
+ * failure is silent. `within` resolves by id, so a plot renamed on one side
8
+ * returns null, the boundary falls back to the viewport, and the bubble goes
9
+ * back to hanging past the drawing with nothing reported anywhere. The plot is
10
+ * also what takes focus, and this controller finds it by the same id.
11
+ *
12
+ * Fixed rather than generated: it is scoped to one chart's shadow root, where
13
+ * it is the only one. Declared here, beside the keys that read it, and
14
+ * re-exported from `drawTooltip`'s module, where it was first declared.
15
+ */
16
+ export declare const CHART_PLOT = "helix-chart-plot";
2
17
  /** What the controller needs of its host, which is a Lit element and a count. */
3
18
  export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
4
19
  /**
@@ -9,6 +24,16 @@ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
9
24
  * Where the chart drew itself, which is where the plot is looked for.
10
25
  */
11
26
  readonly renderRoot: DocumentFragment | HTMLElement;
27
+ /**
28
+ * The positions of the cells in a row that hold numbers, which the series
29
+ * keys move through. Absent, a reading is one series.
30
+ */
31
+ readonly plottedColumns?: number[];
32
+ /**
33
+ * The palette slot for a cell, or `undefined` for a column no series was
34
+ * drawn from, such as a scatter's measure across.
35
+ */
36
+ seriesOf?(cell: number): number | undefined;
12
37
  }
13
38
  /**
14
39
  * Which reading a pointer is nearest, for a chart that draws no markers.
@@ -33,6 +58,21 @@ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
33
58
  * lifts. The table underneath is the readable version and is what a phone
34
59
  * should be given, so this listens for `pointermove` and ignores anything whose
35
60
  * `pointerType` is not fine.
61
+ *
62
+ * **The keyboard moves the same reading, which is what makes it the reading
63
+ * and not the pointer's.** A chart's plot takes focus (`tabindex="0"` on the
64
+ * element carrying `CHART_PLOT`), and while it has it the arrow keys walk the
65
+ * readings, Home and End go to the first and the last, and the other pair of
66
+ * arrows walks the series within a reading. Escape puts the bubble away, as it
67
+ * does a tooltip. The tooltip follows because it already follows `at`, and the
68
+ * chart's readout, a polite live region `drawTooltip` renders, says the
69
+ * reading in words. It speaks only for a key: a pointer crossing a chart is a
70
+ * sighted reader looking, and a region speaking on every slot it crossed would
71
+ * talk over everything else on the page.
72
+ *
73
+ * No peer charting library ships this, which is the reason it is spelled out
74
+ * rather than assumed: the table beside the chart has every number, and this
75
+ * is for a reader who wants the picture's own order, a reading at a time.
36
76
  */
37
77
  export declare class ChartPointerController implements ReactiveController {
38
78
  private host;
@@ -40,6 +80,22 @@ export declare class ChartPointerController implements ReactiveController {
40
80
  private plot;
41
81
  /** The reading under the pointer, or -1 when the pointer is elsewhere. */
42
82
  at: number;
83
+ /**
84
+ * How many readings stand in one column of the drawing, for a chart laid out
85
+ * as a grid.
86
+ *
87
+ * `1` is a row of readings, which is every chart but a heatmap. A heatmap's
88
+ * calendar week stands seven days to a column, so the up and down arrows move
89
+ * a day and the across pair moves a week, which is where the eye goes.
90
+ */
91
+ grid: number;
92
+ /** Whether the reading was last moved by a key, which is when the readout speaks. */
93
+ keyed: boolean;
94
+ /**
95
+ * The series the keyboard is reading within a reading, as its place among
96
+ * the plotted columns, or -1 while the whole reading is read.
97
+ */
98
+ series: number;
43
99
  /**
44
100
  * Which axis the readings are laid out along.
45
101
  *
@@ -60,6 +116,17 @@ export declare class ChartPointerController implements ReactiveController {
60
116
  constructor(host: ChartPointerHost);
61
117
  hostConnected(): void;
62
118
  hostDisconnected(): void;
119
+ /**
120
+ * The cell of the reading the series keys rest on, by its place in the row,
121
+ * or `undefined` while the whole reading is read.
122
+ *
123
+ * The tooltip marks this cell and the readout says it alone. A place in the
124
+ * row and not a series depth, because a row holds columns nothing was drawn
125
+ * from, and the bubble lists every one of them.
126
+ */
127
+ get column(): number | undefined;
128
+ /** The cells a series key can rest on: every column a series was drawn from. */
129
+ private columns;
63
130
  /**
64
131
  * Tells the tooltip its anchor moved, which nothing else would.
65
132
  *
@@ -110,8 +177,40 @@ export declare class ChartPointerController implements ReactiveController {
110
177
  * can change its direction without the chart updating.
111
178
  */
112
179
  get mirrored(): boolean;
180
+ /**
181
+ * Whether a pointer event may move the reading, which only a mouse or a pen
182
+ * may.
183
+ *
184
+ * Public for a chart that hears the pointer itself, a donut's slices and a
185
+ * heatmap's cells, so the rule a coarse pointer gets is this one everywhere.
186
+ * Heard unfiltered, a touch opened the bubble under the finger and left it
187
+ * there after the finger lifted.
188
+ */
189
+ accepts(event: Event): boolean;
113
190
  /** Puts the pointer on a reading, for a chart that resolves it some other way. */
114
191
  set(at: number): void;
192
+ /**
193
+ * Whether an event came from the plot itself, which is the one element in
194
+ * the chart the keys are for.
195
+ *
196
+ * By the id every chart gives its plot, asked of the first node on the
197
+ * event's path: a key pressed in a donut's middle, which a consumer can fill
198
+ * with a link, is that link's and not the chart's.
199
+ */
200
+ private fromPlot;
201
+ /** Puts the reading away, as Escape and leaving the plot both do. */
202
+ private clear;
203
+ private readonly key;
204
+ /**
205
+ * Shows the first reading when the plot is reached from the keyboard, so the
206
+ * bubble and the readout say where the reader has arrived.
207
+ *
208
+ * A pointer pressing the plot focuses it too, and is left alone: the
209
+ * pointer is already showing a reading, and `:focus-visible` is how the
210
+ * platform says which of the two arrived.
211
+ */
212
+ private readonly arrive;
213
+ private readonly depart;
115
214
  private readonly move;
116
215
  private readonly leave;
117
216
  }
@@ -1,11 +1,76 @@
1
1
  import { isRtl } from '../../utils/is-rtl/is-rtl.js';
2
+ import { stepReading } from './reading-step.js';
3
+ export const CHART_PLOT = 'helix-chart-plot';
2
4
  export class ChartPointerController {
3
5
  constructor(host) {
4
6
  this.moved = false;
5
7
  this.at = -1;
8
+ this.grid = 1;
9
+ this.keyed = false;
10
+ this.series = -1;
6
11
  this.axis = 'inline';
12
+ this.key = (event) => {
13
+ if (!this.fromPlot(event) || event.altKey || event.ctrlKey || event.metaKey) {
14
+ return;
15
+ }
16
+ const count = this.host.readings;
17
+ if (count === 0) {
18
+ return;
19
+ }
20
+ if (event.key === 'Escape') {
21
+ if (this.at >= 0) {
22
+ event.preventDefault();
23
+ this.clear();
24
+ }
25
+ return;
26
+ }
27
+ const next = stepReading(event.key, { at: this.at, series: this.series }, {
28
+ axis: this.axis,
29
+ count,
30
+ grid: this.grid,
31
+ rtl: isRtl(this.host),
32
+ series: this.columns().length,
33
+ });
34
+ if (next === undefined) {
35
+ return;
36
+ }
37
+ const { at, series } = next;
38
+ event.preventDefault();
39
+ if (at === this.at && series === this.series && this.keyed) {
40
+ return;
41
+ }
42
+ this.at = at;
43
+ this.series = series;
44
+ this.keyed = true;
45
+ this.moved = true;
46
+ this.host.requestUpdate();
47
+ };
48
+ this.arrive = (event) => {
49
+ if (!this.fromPlot(event) || this.at >= 0) {
50
+ return;
51
+ }
52
+ const plot = event.composedPath()[0];
53
+ let visible = true;
54
+ try {
55
+ visible = plot.matches(':focus-visible');
56
+ }
57
+ catch {
58
+ }
59
+ if (!visible || this.host.readings === 0) {
60
+ return;
61
+ }
62
+ this.at = 0;
63
+ this.keyed = true;
64
+ this.moved = true;
65
+ this.host.requestUpdate();
66
+ };
67
+ this.depart = (event) => {
68
+ if (this.fromPlot(event) && this.keyed) {
69
+ this.clear();
70
+ }
71
+ };
7
72
  this.move = (event) => {
8
- if (this.axis === 'none' || (event.pointerType !== 'mouse' && event.pointerType !== 'pen')) {
73
+ if (this.axis === 'none' || !this.accepts(event)) {
9
74
  return;
10
75
  }
11
76
  const box = this.box();
@@ -23,12 +88,14 @@ export class ChartPointerController {
23
88
  const next = Math.min(Math.max(slot, 0), count - 1);
24
89
  if (next !== this.at) {
25
90
  this.at = next;
91
+ this.keyed = false;
92
+ this.series = -1;
26
93
  this.moved = true;
27
94
  this.host.requestUpdate();
28
95
  }
29
96
  };
30
97
  this.leave = () => {
31
- if (this.at !== -1) {
98
+ if (this.at !== -1 && !this.keyed) {
32
99
  this.at = -1;
33
100
  this.host.requestUpdate();
34
101
  }
@@ -39,11 +106,29 @@ export class ChartPointerController {
39
106
  hostConnected() {
40
107
  this.host.addEventListener('pointermove', this.move);
41
108
  this.host.addEventListener('pointerleave', this.leave);
109
+ this.host.addEventListener('keydown', this.key);
110
+ this.host.addEventListener('focusin', this.arrive);
111
+ this.host.addEventListener('focusout', this.depart);
42
112
  }
43
113
  hostDisconnected() {
44
114
  this.host.removeEventListener('pointermove', this.move);
45
115
  this.host.removeEventListener('pointerleave', this.leave);
116
+ this.host.removeEventListener('keydown', this.key);
117
+ this.host.removeEventListener('focusin', this.arrive);
118
+ this.host.removeEventListener('focusout', this.depart);
46
119
  this.at = -1;
120
+ this.series = -1;
121
+ this.keyed = false;
122
+ }
123
+ get column() {
124
+ return this.series < 0 ? undefined : this.columns()[this.series];
125
+ }
126
+ columns() {
127
+ const host = this.host;
128
+ const all = host.plottedColumns ?? [];
129
+ return host.seriesOf === undefined
130
+ ? all
131
+ : all.filter((one) => host.seriesOf?.(one) !== undefined);
47
132
  }
48
133
  hostUpdated() {
49
134
  if (!this.moved) {
@@ -63,11 +148,31 @@ export class ChartPointerController {
63
148
  get mirrored() {
64
149
  return isRtl(this.host);
65
150
  }
151
+ accepts(event) {
152
+ const type = event.pointerType;
153
+ return type === 'mouse' || type === 'pen';
154
+ }
66
155
  set(at) {
67
156
  if (at !== this.at) {
68
157
  this.at = at;
158
+ this.keyed = false;
69
159
  this.moved = true;
70
160
  this.host.requestUpdate();
71
161
  }
72
162
  }
163
+ fromPlot(event) {
164
+ const target = event.composedPath()[0];
165
+ return (target instanceof Element &&
166
+ target.id === CHART_PLOT &&
167
+ target.getRootNode() === this.host.renderRoot);
168
+ }
169
+ clear() {
170
+ const was = this.at;
171
+ this.at = -1;
172
+ this.series = -1;
173
+ this.keyed = false;
174
+ if (was !== -1) {
175
+ this.host.requestUpdate();
176
+ }
177
+ }
73
178
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Where a key moves the reading of a chart, as arithmetic with no element in
3
+ * it.
4
+ *
5
+ * `ChartPointerController` hears the key and asks this where it goes, so the
6
+ * map of keys to readings is one function every chart shares, and a spec can
7
+ * hold it without a page.
8
+ */
9
+ /** How the readings of a chart are laid out, which decides what each arrow does. */
10
+ export interface ReadingLayout {
11
+ /**
12
+ * The axis the readings run along: `inline` along the line of text, `block`
13
+ * down the box, as a horizontal bar chart runs its categories, and `none`
14
+ * for a ring or a plane, whose readings are walked in the table's order.
15
+ */
16
+ axis: 'block' | 'inline' | 'none';
17
+ /** How many readings there are. */
18
+ count: number;
19
+ /** How many readings stand in one column, for a grid. `1` is a row of them. */
20
+ grid: number;
21
+ /** Whether the line of text runs right to left, which turns the inline arrows. */
22
+ rtl: boolean;
23
+ /** How many series a reading holds, which the cross pair of arrows walks. */
24
+ series: number;
25
+ }
26
+ /** A reading and the series within it, `-1` for none and for the whole reading. */
27
+ export interface ReadingPlace {
28
+ /** The reading, or `-1` for none. */
29
+ at: number;
30
+ /** The series within it, by its place among the plotted ones, or `-1` for all of them. */
31
+ series: number;
32
+ }
33
+ /**
34
+ * Which way a key moves along the readings: `1`, `-1`, or `0` for a key that
35
+ * moves something else.
36
+ *
37
+ * The arrows along the line on an inline chart, down the box on a block one,
38
+ * and a whole column across on a grid. The cross pair walks the readings too
39
+ * wherever a reading holds one series, so no arrow is dead.
40
+ */
41
+ export declare const alongReadings: (key: string, layout: ReadingLayout) => number;
42
+ /**
43
+ * Which way a key moves among the series of a reading: the pair of arrows
44
+ * that does not walk the readings, and only where a reading holds two or more.
45
+ * Down the bubble's list for the arrow pointing down, or along the line.
46
+ */
47
+ export declare const amongSeries: (key: string, layout: ReadingLayout) => number;
48
+ /**
49
+ * Where a key moves the reading, or `undefined` for a key that moves nothing,
50
+ * which the page keeps.
51
+ *
52
+ * Home and End go to the first and the last. From no reading, an arrow moving
53
+ * on starts at the first and one moving back at the last, so either arrow
54
+ * starts somewhere a reader expects. A series key on no reading starts on the
55
+ * first. Nothing walks off either end.
56
+ */
57
+ export declare const stepReading: (key: string, from: ReadingPlace, layout: ReadingLayout) => ReadingPlace | undefined;