@fusion.dev/helix-headless 0.144.0 → 0.145.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 (155) hide show
  1. package/custom-elements.json +8312 -881
  2. package/dist/accordion/item/accordion-item.d.ts +48 -12
  3. package/dist/accordion/item/accordion-item.js +26 -5
  4. package/dist/accordion/root/accordion-root.d.ts +6 -0
  5. package/dist/accordion/root/accordion-root.js +7 -1
  6. package/dist/announcement/root/announcement-root.d.ts +21 -1
  7. package/dist/announcement/root/announcement-root.js +36 -1
  8. package/dist/app-header/root/app-header-root.d.ts +7 -0
  9. package/dist/app-header/root/app-header-root.js +5 -1
  10. package/dist/app-header/sectioning.d.ts +7 -0
  11. package/dist/app-header/sectioning.js +26 -0
  12. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +32 -1
  13. package/dist/breadcrumbs/root/breadcrumbs-root.js +25 -2
  14. package/dist/canvas/root/canvas-root.d.ts +38 -3
  15. package/dist/canvas/root/canvas-root.js +121 -40
  16. package/dist/carousel/root/carousel-root.d.ts +90 -0
  17. package/dist/carousel/root/carousel-root.js +134 -12
  18. package/dist/chat/composer/chat-composer.d.ts +38 -6
  19. package/dist/chat/composer/chat-composer.js +42 -9
  20. package/dist/chat/message/chat-message.d.ts +32 -1
  21. package/dist/chat/message/chat-message.js +25 -9
  22. package/dist/chat/naming.d.ts +22 -0
  23. package/dist/chat/naming.js +12 -0
  24. package/dist/chat/relative-clock.d.ts +39 -0
  25. package/dist/chat/relative-clock.js +31 -0
  26. package/dist/chat/root/chat-root.d.ts +36 -0
  27. package/dist/chat/root/chat-root.js +24 -1
  28. package/dist/code-block/root/code-block-root.d.ts +8 -1
  29. package/dist/code-block/root/code-block-root.js +5 -5
  30. package/dist/copy-button/root/copy-button-root.d.ts +21 -1
  31. package/dist/copy-button/root/copy-button-root.js +2 -2
  32. package/dist/data-table/root/data-table-root.d.ts +150 -2
  33. package/dist/data-table/root/data-table-root.js +303 -29
  34. package/dist/data-table/tree.d.ts +40 -0
  35. package/dist/data-table/tree.js +55 -0
  36. package/dist/description-list/item/description-list-item.d.ts +17 -2
  37. package/dist/description-list/item/description-list-item.js +15 -2
  38. package/dist/description-list/root/description-list-root.d.ts +27 -1
  39. package/dist/description-list/root/description-list-root.js +26 -1
  40. package/dist/drilldown/item/drilldown-item.d.ts +11 -6
  41. package/dist/drilldown/item/drilldown-item.js +8 -5
  42. package/dist/drilldown/root/drilldown-root.d.ts +11 -0
  43. package/dist/drilldown/root/drilldown-root.js +8 -3
  44. package/dist/element-events.generated.js +53 -8
  45. package/dist/filter-input/menu/filter-menu.d.ts +3 -0
  46. package/dist/filter-input/menu/filter-menu.js +8 -4
  47. package/dist/image/root/image-root.js +9 -1
  48. package/dist/input/root/input-root.d.ts +1 -1
  49. package/dist/input/root/input-root.js +2 -2
  50. package/dist/list/item/list-item.d.ts +68 -1
  51. package/dist/list/item/list-item.js +66 -4
  52. package/dist/list/numbering.d.ts +14 -0
  53. package/dist/list/numbering.js +8 -0
  54. package/dist/list/root/list-root.d.ts +106 -9
  55. package/dist/list/root/list-root.js +189 -4
  56. package/dist/list/selection.d.ts +19 -0
  57. package/dist/list/selection.js +21 -0
  58. package/dist/listbox/root/listbox-root.js +1 -2
  59. package/dist/map/root/map-root.d.ts +6 -2
  60. package/dist/map/root/map-root.js +5 -2
  61. package/dist/megamenu/root/megamenu-root.d.ts +21 -0
  62. package/dist/megamenu/root/megamenu-root.js +16 -2
  63. package/dist/menu/index.d.ts +1 -0
  64. package/dist/menu/index.js +1 -0
  65. package/dist/menu/item/menu-item.d.ts +38 -11
  66. package/dist/menu/item/menu-item.js +33 -1
  67. package/dist/menu/root/menu-root.d.ts +25 -1
  68. package/dist/menu/root/menu-root.js +63 -6
  69. package/dist/menu/section/menu-section.d.ts +55 -0
  70. package/dist/menu/section/menu-section.js +60 -0
  71. package/dist/navigation/root/navigation-root.d.ts +16 -1
  72. package/dist/navigation/root/navigation-root.js +27 -1
  73. package/dist/notifications/root/notifications-root.d.ts +18 -0
  74. package/dist/notifications/root/notifications-root.js +17 -1
  75. package/dist/object-inspector/root/object-inspector-root.d.ts +97 -16
  76. package/dist/object-inspector/root/object-inspector-root.js +179 -79
  77. package/dist/object-inspector/rows.d.ts +57 -0
  78. package/dist/object-inspector/rows.js +86 -0
  79. package/dist/outline/levels.d.ts +31 -0
  80. package/dist/outline/levels.js +23 -0
  81. package/dist/outline/root/outline-root.d.ts +68 -3
  82. package/dist/outline/root/outline-root.js +73 -8
  83. package/dist/pagination/size/pagination-size.d.ts +11 -2
  84. package/dist/pagination/size/pagination-size.js +19 -4
  85. package/dist/radio/group/radio-group.d.ts +4 -10
  86. package/dist/radio/group/radio-group.js +5 -10
  87. package/dist/rating/root/rating-root.d.ts +1 -1
  88. package/dist/rating/root/rating-root.js +2 -2
  89. package/dist/scroll-area/root/scroll-area-root.d.ts +113 -0
  90. package/dist/scroll-area/root/scroll-area-root.js +218 -3
  91. package/dist/scroll-area/scrollbar.d.ts +65 -0
  92. package/dist/scroll-area/scrollbar.js +24 -0
  93. package/dist/select/root/select-root.js +1 -1
  94. package/dist/splitter/root/splitter-root.d.ts +13 -0
  95. package/dist/splitter/root/splitter-root.js +20 -2
  96. package/dist/splitter/snap.d.ts +9 -0
  97. package/dist/splitter/snap.js +12 -0
  98. package/dist/steps/reach.d.ts +15 -0
  99. package/dist/steps/reach.js +12 -0
  100. package/dist/steps/root/steps-root.d.ts +30 -0
  101. package/dist/steps/root/steps-root.js +53 -0
  102. package/dist/steps/step/step.d.ts +38 -2
  103. package/dist/steps/step/step.js +28 -6
  104. package/dist/table/column/table-column.d.ts +11 -0
  105. package/dist/table/column/table-column.js +6 -1
  106. package/dist/table/root/table-root.d.ts +101 -0
  107. package/dist/table/root/table-root.js +166 -5
  108. package/dist/table/row/table-row.d.ts +47 -1
  109. package/dist/table/row/table-row.js +39 -2
  110. package/dist/tabs/panel/tab-panel.d.ts +9 -5
  111. package/dist/tabs/panel/tab-panel.js +3 -4
  112. package/dist/tabs/root/tabs-root.d.ts +25 -1
  113. package/dist/tabs/root/tabs-root.js +21 -3
  114. package/dist/tabs/tab/tab.d.ts +36 -4
  115. package/dist/tabs/tab/tab.js +46 -3
  116. package/dist/timeline/group/timeline-group.d.ts +46 -0
  117. package/dist/timeline/group/timeline-group.js +37 -0
  118. package/dist/timeline/index.d.ts +1 -0
  119. package/dist/timeline/index.js +1 -0
  120. package/dist/timeline/root/timeline-root.d.ts +10 -3
  121. package/dist/toast/item/toast-item.d.ts +10 -17
  122. package/dist/toast/item/toast-item.js +15 -36
  123. package/dist/toolbar/root/toolbar-root.d.ts +13 -0
  124. package/dist/toolbar/root/toolbar-root.js +15 -5
  125. package/dist/tree/item/tree-item.d.ts +54 -1
  126. package/dist/tree/item/tree-item.js +89 -20
  127. package/dist/tree/root/tree-root.d.ts +37 -0
  128. package/dist/tree/root/tree-root.js +51 -6
  129. package/dist/tree/selection.d.ts +24 -0
  130. package/dist/tree/selection.js +26 -0
  131. package/dist/video/media-events.d.ts +49 -0
  132. package/dist/video/media-events.js +39 -0
  133. package/dist/video/root/video-root.d.ts +99 -13
  134. package/dist/video/root/video-root.js +84 -47
  135. package/dist/video-playlist/menu/video-playlist-menu.d.ts +3 -0
  136. package/dist/video-playlist/menu/video-playlist-menu.js +2 -1
  137. package/dist/video-playlist/order.d.ts +41 -0
  138. package/dist/video-playlist/order.js +34 -0
  139. package/dist/video-playlist/root/video-playlist-root.d.ts +70 -2
  140. package/dist/video-playlist/root/video-playlist-root.js +60 -16
  141. package/dist/virtual-list/controls.d.ts +4 -0
  142. package/dist/virtual-list/controls.js +30 -0
  143. package/dist/virtual-list/fill.d.ts +30 -0
  144. package/dist/virtual-list/fill.js +39 -0
  145. package/dist/virtual-list/keys.d.ts +15 -0
  146. package/dist/virtual-list/keys.js +24 -0
  147. package/dist/virtual-list/root/virtual-list-root.d.ts +155 -23
  148. package/dist/virtual-list/root/virtual-list-root.js +175 -34
  149. package/lint.json +1 -1
  150. package/package.json +5 -5
  151. package/types/jsx.d.ts +3778 -801
  152. package/types/svelte.d.ts +2 -0
  153. package/types/vue.d.ts +782 -215
  154. package/dist/listbox/range.d.ts +0 -17
  155. package/dist/listbox/range.js +0 -5
@@ -1,5 +1,7 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
+ /** The values `current` takes, which are `aria-current`'s. */
4
+ export type ListItemCurrent = 'date' | 'location' | 'page' | 'step' | 'time' | 'true';
3
5
  /**
4
6
  * @summary One row of a list.
5
7
  *
@@ -25,6 +27,14 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
25
27
  * **For a row that holds controls and is not a destination, use
26
28
  * `data-list-item`.** With `href` unset, the row is not a link.
27
29
  *
30
+ * `current` marks the row a reader is on, such as the page in a list of
31
+ * pages, with `aria-current` on its link, or on the row where it has none.
32
+ *
33
+ * **In a list with `selection-mode`, the row is a row of a grid.** The list
34
+ * gives it its role, its place in the one tab stop and, where the list
35
+ * selects, whether it is selected. Its link leaves the tab order there, and
36
+ * Enter follows it from the row.
37
+ *
28
38
  * @since 0.1.0
29
39
  * @status stable
30
40
  * @aliases Row, Entry
@@ -35,10 +45,38 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
35
45
  * @slot end - Trailing content.
36
46
  * @slot start - Leading content, usually an icon or an avatar.
37
47
  *
48
+ * @event {HelixActionEvent} helix-action - The reader pressed Enter on the row, or pressed the row in a list that selects nothing, in a list with `selection-mode`. Read the row from `target`. A row with `href` follows its link instead.
49
+ *
38
50
  * @csspart link - The link, when the row has `href`. It holds every region except `end`.
39
- * @csspart row - The row's layout. It sits inside the host so the host can carry a list marker.
51
+ * @csspart row - The row's layout. It sits inside the host so the host can carry a list marker. In a list with `selection-mode` it is the grid cell that takes focus.
52
+ * @csspart row-current - Added to `row` while the row is `current`.
53
+ *
54
+ * @cssstate selected - The row is selected, in a list that selects.
40
55
  */
41
56
  export declare class HelixHeadlessListItem extends HelixElement {
57
+ /**
58
+ * Marks the row a reader is on: `page` for the page they are reading, `step`,
59
+ * `location`, `date` or `time` for those, and written bare for the current
60
+ * one of a set. It is `aria-current` on the row's link, or on the row where
61
+ * it has none.
62
+ *
63
+ * Not reflected, as a state never is. `row-current` is the part a
64
+ * stylesheet draws it from.
65
+ */
66
+ current?: ListItemCurrent;
67
+ /**
68
+ * Takes the row out of reach in a list with `selection-mode`: the arrow keys
69
+ * pass over it and it cannot be selected or acted on.
70
+ */
71
+ disabled: boolean;
72
+ /**
73
+ * Whether the row holds its list's tab stop.
74
+ *
75
+ * Written by a list with `selection-mode` on every row it holds, and unset
76
+ * otherwise, which is how a row knows it is a row of a grid. No attribute:
77
+ * it is coordination between elements, as `Tag.focusable` is.
78
+ */
79
+ focusable?: boolean | undefined;
42
80
  /**
43
81
  * The URL the row links to. Leave it empty for a row that is not a link.
44
82
  *
@@ -52,10 +90,39 @@ export declare class HelixHeadlessListItem extends HelixElement {
52
90
  * `target`, for the reason `Tree.Item` gives.
53
91
  */
54
92
  rel: string;
93
+ /**
94
+ * Whether the row is selected, in a list that selects.
95
+ *
96
+ * Written by the list from its own `value`, and unset where nothing can be
97
+ * selected, which keeps `aria-selected` off a list that does not select.
98
+ */
99
+ selected?: boolean | undefined;
55
100
  /**
56
101
  * Where the link opens. Use it for a row that leaves the site.
57
102
  */
58
103
  target: string;
104
+ /**
105
+ * What the row stands for in its list's `value`.
106
+ *
107
+ * Absent is the row's own words.
108
+ */
109
+ value: string;
110
+ /** The row's words, or none where the children are not known yet. */
111
+ get words(): string;
112
+ /** Whether a list with `selection-mode` holds this row, which makes it a row of a grid. */
113
+ private get grouped();
114
+ /** @internal The cell a list with `selection-mode` moves focus to. */
115
+ get cell(): HTMLElement | null;
116
+ /**
117
+ * @internal The controls in the `end` slot, in order, which Left and Right
118
+ * move between in a list with `selection-mode`. Disabled ones are passed
119
+ * over.
120
+ */
121
+ get controls(): HTMLElement[];
122
+ /** Moves focus to the row's cell, in a list with `selection-mode`. */
123
+ focusItem(): void;
124
+ /** @internal Follows the row's link, as a press on it would. Called by the list on Enter. */
125
+ follow(): void;
59
126
  protected willUpdate(changed: PropertyValues): void;
60
127
  /**
61
128
  * The cell before the row's content, which this tier leaves empty.
@@ -4,19 +4,50 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, slottedWords } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ const CONTROL = 'a[href], button, input, select, textarea, [tabindex]';
10
11
  let HelixHeadlessListItem = class HelixHeadlessListItem extends HelixElement {
11
12
  constructor() {
12
13
  super(...arguments);
14
+ this.disabled = false;
13
15
  this.href = '';
14
16
  this.rel = '';
15
17
  this.target = '';
18
+ this.value = '';
19
+ }
20
+ get words() {
21
+ return this.childrenKnown ? slottedWords(this, ['']) : '';
22
+ }
23
+ get grouped() {
24
+ return this.focusable !== undefined;
25
+ }
26
+ get cell() {
27
+ return this.renderRoot?.querySelector?.('[part~="row"]') ?? null;
28
+ }
29
+ get controls() {
30
+ const slot = this.renderRoot?.querySelector?.('slot[name="end"]');
31
+ return (slot?.assignedElements({ flatten: true }) ?? [])
32
+ .flatMap((element) => [element, ...element.querySelectorAll('*')])
33
+ .filter((element) => element instanceof HTMLElement &&
34
+ element.matches(CONTROL) &&
35
+ !element.matches(':disabled'));
36
+ }
37
+ focusItem() {
38
+ this.cell?.focus();
39
+ }
40
+ follow() {
41
+ this.renderRoot?.querySelector('[part~="link"]')?.click();
16
42
  }
17
43
  willUpdate(changed) {
18
44
  super.willUpdate(changed);
19
- hostAria(this.internals, { role: 'listitem' });
45
+ hostAria(this.internals, { role: this.grouped ? 'row' : 'listitem' });
46
+ this.internals.ariaSelected = this.selected === undefined ? null : String(this.selected);
47
+ this.internals.ariaDisabled = this.grouped && this.disabled ? 'true' : null;
48
+ this.internals.ariaCurrent =
49
+ this.current !== undefined && this.href === '' ? this.current : null;
50
+ this.toggleState('selected', this.selected === true);
20
51
  }
21
52
  renderRail() {
22
53
  return nothing;
@@ -25,26 +56,57 @@ let HelixHeadlessListItem = class HelixHeadlessListItem extends HelixElement {
25
56
  const content = html `${this.renderRail()}<slot name="start"></slot>
26
57
  <slot></slot>
27
58
  <slot name="description"></slot>`;
59
+ const role = this.grouped ? 'gridcell' : nothing;
60
+ const tabindex = this.grouped ? (this.focusable ? '0' : '-1') : nothing;
28
61
  return this.href === ''
29
- ? html `<div part="row">${content}<slot name="end"></slot></div>`
30
- : html `<div part="row"><a
62
+ ? html `<div
63
+ part="row${this.current === undefined ? '' : ' row-current'}"
64
+ role=${role}
65
+ tabindex=${tabindex}
66
+ >${content}<slot name="end"></slot></div>`
67
+ : html `<div
68
+ part="row${this.current === undefined ? '' : ' row-current'}"
69
+ role=${role}
70
+ tabindex=${tabindex}
71
+ ><a
31
72
  part="link"
32
73
  href=${this.href}
33
74
  target=${this.target || nothing}
34
75
  rel=${this.rel || nothing}
76
+ aria-current=${this.current ?? nothing}
77
+ tabindex=${this.grouped ? '-1' : nothing}
35
78
  >${content}</a
36
79
  ><slot name="end"></slot></div>`;
37
80
  }
38
81
  };
82
+ __decorate([
83
+ property({
84
+ converter: {
85
+ fromAttribute: (value) => value === null || value === 'false' ? undefined : (value || 'true'),
86
+ },
87
+ })
88
+ ], HelixHeadlessListItem.prototype, "current", void 0);
89
+ __decorate([
90
+ property({ reflect: true, type: Boolean })
91
+ ], HelixHeadlessListItem.prototype, "disabled", void 0);
92
+ __decorate([
93
+ property({ attribute: false })
94
+ ], HelixHeadlessListItem.prototype, "focusable", void 0);
39
95
  __decorate([
40
96
  property()
41
97
  ], HelixHeadlessListItem.prototype, "href", void 0);
42
98
  __decorate([
43
99
  property()
44
100
  ], HelixHeadlessListItem.prototype, "rel", void 0);
101
+ __decorate([
102
+ property({ attribute: false })
103
+ ], HelixHeadlessListItem.prototype, "selected", void 0);
45
104
  __decorate([
46
105
  property()
47
106
  ], HelixHeadlessListItem.prototype, "target", void 0);
107
+ __decorate([
108
+ property()
109
+ ], HelixHeadlessListItem.prototype, "value", void 0);
48
110
  HelixHeadlessListItem = __decorate([
49
111
  customElement('helix-headless-list-item')
50
112
  ], HelixHeadlessListItem);
@@ -0,0 +1,14 @@
1
+ /** The first row's number, and what each row after it adds. */
2
+ export interface Numbering {
3
+ first: number;
4
+ step: -1 | 1;
5
+ }
6
+ /**
7
+ * The numbering `start` and `reversed` ask for, as `<ol>` reads them, or
8
+ * nothing where they ask for the ordinary count from one.
9
+ *
10
+ * Reversed without a start counts down from the number of rows, which is
11
+ * unknown until the rows are: `count` is `undefined` on a server and while
12
+ * hydrating, and the list counts from one until it knows.
13
+ */
14
+ export declare const numbering: (start: number | undefined, reversed: boolean, count: number | undefined) => Numbering | undefined;
@@ -0,0 +1,8 @@
1
+ export const numbering = (start, reversed, count) => {
2
+ const from = start !== undefined && Number.isFinite(start) ? Math.trunc(start) : undefined;
3
+ if (!reversed) {
4
+ return from === undefined ? undefined : { first: from, step: 1 };
5
+ }
6
+ const first = from ?? count;
7
+ return first === undefined ? undefined : { first, step: -1 };
8
+ };
@@ -1,5 +1,7 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import { HelixHeadlessListItem } from '../item/list-item.js';
4
+ import { type ListSelectionMode } from '../selection.js';
3
5
  /**
4
6
  * @summary A set of rows, announced as a list.
5
7
  *
@@ -7,6 +9,29 @@ import { type PropertyValues, type TemplateResult } from 'lit';
7
9
  * Sets `role="list"` on the host, so a screen reader counts the rows and
8
10
  * announces each one's position, such as "1 of 7".
9
11
  *
12
+ * **`selection-mode` makes the list interactive**, in the way of a grid list:
13
+ * a grid of rows that is one tab stop. Up and Down move between rows, Home
14
+ * and End go to the first and the last, and a typed letter goes to the next
15
+ * row starting with it. Left and Right move between the row and the controls
16
+ * in its `end` slot. `single` selects one row at a time and `multiple` any
17
+ * number, with Space or a press, and `value` holds the selected rows' values,
18
+ * separated by commas. A change asks with `helix-before-change` and tells
19
+ * with `change`. Enter acts on the row: it follows the row's link, or sends
20
+ * `helix-action` from the row. `none` selects nothing, so a press acts too. A
21
+ * `disabled` row is passed over.
22
+ *
23
+ * **Leave `selection-mode` unset for a list a reader reads**, and set it for
24
+ * one a reader chooses from or acts on. The rows of an interactive list are
25
+ * grid rows, which hold controls as a list box option cannot. Give a control
26
+ * in `end` `tabindex="-1"` to leave it to the arrow keys and out of the tab
27
+ * order.
28
+ *
29
+ * `start` and `reversed` number an ordered list as `<ol>` does. The list
30
+ * writes where the numbers start and which way they run as `--list-first` and
31
+ * `--list-step` on its slot, which the styled tier counts from. Reversed with
32
+ * no `start` counts down from the number of rows, which a server cannot know,
33
+ * so served markup counts from one until the list runs.
34
+ *
10
35
  * @since 0.1.0
11
36
  * @status stable
12
37
  * @category data
@@ -14,6 +39,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
14
39
  * @documentation https://fusion.dev/helix/components/list
15
40
  *
16
41
  * @slot - The rows.
42
+ *
43
+ * @event {Event} change - Fired once a press has changed which rows are selected.
44
+ * @event {HelixActionEvent} helix-action - Sent from a row when the reader acts on it: Enter, or a press in a list that selects nothing. Read the row from `target`. A row with `href` follows its link instead.
45
+ * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a press changes which rows are selected, with the `value` it would leave. Cancelable.
46
+ *
47
+ * @cssproperty --list-first - The number of the first row, written where `start` or `reversed` moves it.
48
+ * @cssproperty --list-step - What each row adds to the number before it: `1`, or `-1` where `reversed`.
17
49
  */
18
50
  export declare class HelixHeadlessList extends HelixElement {
19
51
  /**
@@ -43,6 +75,22 @@ export declare class HelixHeadlessList extends HelixElement {
43
75
  * The list's name, read by screen readers.
44
76
  */
45
77
  label: string;
78
+ /**
79
+ * Numbers an ordered list downward, so the count falls row by row and ends at
80
+ * its last row, as `<ol reversed>` does.
81
+ */
82
+ reversed: boolean;
83
+ /**
84
+ * Makes the list interactive: `single` selects one row, `multiple` any
85
+ * number, and `none` moves and acts without selecting. Unset is a list a
86
+ * reader reads, with no tab stop of its own.
87
+ */
88
+ selectionMode?: ListSelectionMode;
89
+ /**
90
+ * The number of an ordered list's first row, as `<ol start>` is. Unset counts
91
+ * from one, or down from the number of rows where `reversed`.
92
+ */
93
+ start?: number;
46
94
  /**
47
95
  * The markers an ordered list draws. It takes the five values of `<ol type>`.
48
96
  *
@@ -51,18 +99,67 @@ export declare class HelixHeadlessList extends HelixElement {
51
99
  * the CSS ones, so `<helix-headless-list kind="ordered" type="i">` is what a
52
100
  * consumer would have written on an `<ol>`.
53
101
  *
54
- * **`start` and `reversed` are both absent and neither can be done here.**
55
- * This renders no `<ol>`: the rows are slotted and numbered by the CSS
56
- * `list-item` counter, and that counter is not author-resettable. Measured in
57
- * Chrome 148, `counter-reset: list-item 4` moves nothing on a `<div>`, on a
58
- * `<ul>`, or even on a real `<ol>`, while `<ol start="5">` does and a *named*
59
- * counter with explicit `::before` content does. So the markers would have to
60
- * be rebuilt out of generated content to gain a start, which is a different
61
- * component. `type` is only `list-style-type` and works.
102
+ * `type` is only `list-style-type`. `start` and `reversed` move the count
103
+ * itself.
62
104
  */
63
105
  type?: '1' | 'a' | 'A' | 'i' | 'I';
106
+ /**
107
+ * The selected rows' values, separated by commas, in a list that selects.
108
+ *
109
+ * Set it to start with rows selected. A row's value is its `value`, or its
110
+ * words where it has none.
111
+ */
112
+ value: string;
113
+ /** The row the reader was last on, which keeps the tab stop. */
114
+ private last;
115
+ /**
116
+ * Up, Down, Home, End and typed letters, moving focus and choosing nothing,
117
+ * from the focused row and without wrapping. Left and Right are the row's.
118
+ */
119
+ protected readonly keys: KeyboardNavigationController;
120
+ /** Whether the list is a grid a reader moves through. */
121
+ private get interactive();
122
+ /** The rows the list holds, in order. */
123
+ get rows(): HelixHeadlessListItem[];
124
+ /** The rows a key can land on, in an interactive list. */
125
+ private get reachable();
126
+ /** A row as the keyboard controller sees it: an item it moves focus to and never chooses. */
127
+ private navigable;
128
+ /** What a row stands for in `value`: its own `value`, or its words. */
129
+ private valueFor;
130
+ /**
131
+ * Tells every row its place: whether it holds the one tab stop and whether
132
+ * it is selected, or that it is a plain row. Through `whenHydrated`, so a
133
+ * row a server rendered draws its served markup first.
134
+ */
135
+ private sync;
136
+ /** The row a press or a key came from, read off the event's path. */
137
+ private rowOf;
138
+ /**
139
+ * Selects the row, or lets it go, asking first. `helix-before-change`
140
+ * carries the `value` the press would leave, and `change` follows once it
141
+ * has moved.
142
+ */
143
+ protected press(row: HelixHeadlessListItem): void;
144
+ /** Acts on the row: follows its link, or says it was acted on. */
145
+ protected act(row: HelixHeadlessListItem): void;
146
+ /** Left and Right inside a row: the cell, then each control in `end`, and back. */
147
+ private moveInRow;
148
+ private readonly handleKeydown;
149
+ private readonly handleClick;
150
+ private readonly handleFocusin;
151
+ connectedCallback(): void;
152
+ disconnectedCallback(): void;
153
+ protected firstUpdated(changed: PropertyValues): void;
154
+ protected updated(changed: PropertyValues): void;
64
155
  protected willUpdate(changed: PropertyValues): void;
156
+ /**
157
+ * Where the numbers start and which way they run, as custom properties on
158
+ * the slot the rows inherit from, or nothing where they count up from one.
159
+ */
160
+ private get numbers();
65
161
  protected render(): TemplateResult;
162
+ private readonly handleSlotChange;
66
163
  }
67
164
  declare global {
68
165
  interface HTMLElementTagNameMap {
@@ -4,20 +4,193 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
7
+ import { commaList, customElement, HelixActionEvent, HelixBeforeChangeEvent, HelixElement, hostAria, inlineStep, KeyboardNavigationController, requestChange, rovingStop, whenHydrated, } from '@fusion.dev/helix-core';
8
+ import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { HelixHeadlessListItem } from '../item/list-item.js';
11
+ import { numbering } from '../numbering.js';
12
+ import { pressedValue, rowStop } from '../selection.js';
13
+ const PRESSABLE = 'a[href], button, input, label, select, textarea, [tabindex]';
10
14
  let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
11
15
  constructor() {
12
16
  super(...arguments);
13
17
  this.label = '';
18
+ this.reversed = false;
19
+ this.value = '';
20
+ this.keys = new KeyboardNavigationController(this, {
21
+ items: () => this.reachable.map((row) => this.navigable(row)),
22
+ origin: 'focus',
23
+ reserved: () => ['ArrowLeft', 'ArrowRight'],
24
+ selects: () => false,
25
+ typeahead: (item) => item.words,
26
+ wrap: false,
27
+ });
28
+ this.handleKeydown = (event) => {
29
+ const row = this.interactive ? this.rowOf(event) : undefined;
30
+ if (!row || row.disabled || event.altKey || event.ctrlKey || event.metaKey) {
31
+ return;
32
+ }
33
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
34
+ this.moveInRow(row, event);
35
+ return;
36
+ }
37
+ if (event.composedPath()[0] !== row.cell) {
38
+ return;
39
+ }
40
+ if (event.key === 'Enter') {
41
+ event.preventDefault();
42
+ this.act(row);
43
+ }
44
+ else if (event.key === ' ') {
45
+ event.preventDefault();
46
+ this.press(row);
47
+ }
48
+ };
49
+ this.handleClick = (event) => {
50
+ const row = this.interactive ? this.rowOf(event) : undefined;
51
+ if (!row) {
52
+ return;
53
+ }
54
+ const path = event.composedPath();
55
+ const cell = row.cell;
56
+ const end = cell && path.includes(cell) ? path.indexOf(cell) : path.indexOf(row);
57
+ const inner = path.slice(0, Math.max(0, end));
58
+ if (inner.some((node) => node instanceof HTMLElement && node.matches(PRESSABLE))) {
59
+ return;
60
+ }
61
+ if (this.selectionMode === 'none') {
62
+ this.act(row);
63
+ }
64
+ else {
65
+ this.press(row);
66
+ }
67
+ };
68
+ this.handleFocusin = (event) => {
69
+ const row = this.interactive ? this.rowOf(event) : undefined;
70
+ if (row && row !== this.last) {
71
+ this.last = row;
72
+ this.sync();
73
+ }
74
+ };
75
+ this.handleSlotChange = () => {
76
+ this.requestUpdate();
77
+ };
78
+ }
79
+ get interactive() {
80
+ return this.selectionMode !== undefined;
81
+ }
82
+ get rows() {
83
+ return this.knownChildren.filter((child) => child instanceof HelixHeadlessListItem);
84
+ }
85
+ get reachable() {
86
+ return this.interactive ? this.rows.filter((row) => !row.disabled) : [];
87
+ }
88
+ navigable(row) {
89
+ return {
90
+ contains: (node) => row.contains(node),
91
+ disabled: row.disabled,
92
+ focusItem: () => row.focusItem(),
93
+ select: () => { },
94
+ selected: false,
95
+ words: row.words,
96
+ };
97
+ }
98
+ valueFor(row) {
99
+ return row.value || row.words;
100
+ }
101
+ sync() {
102
+ const mode = this.selectionMode;
103
+ const chosen = new Set(commaList(this.value));
104
+ const rows = this.rows;
105
+ const holder = mode
106
+ ? rovingStop(rows, this.last, {
107
+ disabled: (row) => row.disabled,
108
+ selected: (row) => chosen.has(this.valueFor(row)),
109
+ })
110
+ : undefined;
111
+ for (const row of rows) {
112
+ whenHydrated(row, () => {
113
+ row.focusable = mode ? row === holder : undefined;
114
+ row.selected =
115
+ mode === undefined || mode === 'none' ? undefined : chosen.has(this.valueFor(row));
116
+ });
117
+ }
118
+ }
119
+ rowOf(event) {
120
+ return event
121
+ .composedPath()
122
+ .find((node) => node instanceof HelixHeadlessListItem && node.parentElement === this);
123
+ }
124
+ press(row) {
125
+ const mode = this.selectionMode;
126
+ if (mode === undefined || mode === 'none' || row.disabled) {
127
+ return;
128
+ }
129
+ requestChange({
130
+ apply: (value) => {
131
+ this.value = value;
132
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
133
+ },
134
+ event: (value) => new HelixBeforeChangeEvent({ value }),
135
+ host: this,
136
+ next: pressedValue(mode, this.value, this.valueFor(row)),
137
+ });
138
+ }
139
+ act(row) {
140
+ if (row.disabled) {
141
+ return;
142
+ }
143
+ if (row.href !== '') {
144
+ row.follow();
145
+ return;
146
+ }
147
+ row.dispatchEvent(new HelixActionEvent());
148
+ }
149
+ moveInRow(row, event) {
150
+ const step = inlineStep(this, event.key);
151
+ const cell = row.cell;
152
+ if (step === 0 || !cell) {
153
+ return;
154
+ }
155
+ const stops = [cell, ...row.controls];
156
+ const on = event.composedPath().find((node) => stops.includes(node));
157
+ const at = Math.max(0, stops.indexOf(on));
158
+ event.preventDefault();
159
+ stops[rowStop(stops.length, at, step)]?.focus();
160
+ }
161
+ connectedCallback() {
162
+ super.connectedCallback();
163
+ this.addEventListener('keydown', this.handleKeydown);
164
+ this.addEventListener('click', this.handleClick);
165
+ this.addEventListener('focusin', this.handleFocusin);
166
+ }
167
+ disconnectedCallback() {
168
+ super.disconnectedCallback();
169
+ this.removeEventListener('keydown', this.handleKeydown);
170
+ this.removeEventListener('click', this.handleClick);
171
+ this.removeEventListener('focusin', this.handleFocusin);
172
+ }
173
+ firstUpdated(changed) {
174
+ super.firstUpdated(changed);
175
+ this.finishHydration();
176
+ }
177
+ updated(changed) {
178
+ super.updated(changed);
179
+ if (this.interactive || changed.get('selectionMode') !== undefined) {
180
+ this.sync();
181
+ }
14
182
  }
15
183
  willUpdate(changed) {
16
184
  super.willUpdate(changed);
17
- hostAria(this.internals, { label: this.label, role: 'list' });
185
+ hostAria(this.internals, { label: this.label, role: this.interactive ? 'grid' : 'list' });
186
+ this.internals.ariaMultiSelectable = this.selectionMode === 'multiple' ? 'true' : null;
187
+ }
188
+ get numbers() {
189
+ const counted = numbering(this.start, this.reversed, this.childrenKnown ? this.knownChildren.length : undefined);
190
+ return counted ? `--list-first: ${counted.first}; --list-step: ${counted.step}` : nothing;
18
191
  }
19
192
  render() {
20
- return html `<slot></slot>`;
193
+ return html `<slot style=${this.numbers} @slotchange=${this.handleSlotChange}></slot>`;
21
194
  }
22
195
  };
23
196
  __decorate([
@@ -26,9 +199,21 @@ __decorate([
26
199
  __decorate([
27
200
  property()
28
201
  ], HelixHeadlessList.prototype, "label", void 0);
202
+ __decorate([
203
+ property({ type: Boolean })
204
+ ], HelixHeadlessList.prototype, "reversed", void 0);
205
+ __decorate([
206
+ property({ attribute: 'selection-mode' })
207
+ ], HelixHeadlessList.prototype, "selectionMode", void 0);
208
+ __decorate([
209
+ property({ type: Number })
210
+ ], HelixHeadlessList.prototype, "start", void 0);
29
211
  __decorate([
30
212
  property({ reflect: true })
31
213
  ], HelixHeadlessList.prototype, "type", void 0);
214
+ __decorate([
215
+ property()
216
+ ], HelixHeadlessList.prototype, "value", void 0);
32
217
  HelixHeadlessList = __decorate([
33
218
  customElement('helix-headless-list')
34
219
  ], HelixHeadlessList);
@@ -0,0 +1,19 @@
1
+ /** How many rows a reader can select in an interactive list. */
2
+ export type ListSelectionMode = 'multiple' | 'none' | 'single';
3
+ /**
4
+ * The `value` a press on the row carrying `pressed` leaves.
5
+ *
6
+ * `single` selects that row alone, or none where it was the one selected.
7
+ * `multiple` adds it, or takes it out. `none` selects nothing and leaves the
8
+ * value as it was. Tag Group answers the same question for its tags.
9
+ */
10
+ export declare const pressedValue: (mode: ListSelectionMode, value: string, pressed: string) => string;
11
+ /**
12
+ * Where Left or Right goes inside a row: `at` is the row's place among its
13
+ * `count` stops, the cell first and its controls after it, and `step` is the
14
+ * way the key points along the line.
15
+ *
16
+ * Past either end it comes back to the cell, so the keys never leave the row,
17
+ * and from the cell going back reaches the last control.
18
+ */
19
+ export declare const rowStop: (count: number, at: number, step: -1 | 1) => number;
@@ -0,0 +1,21 @@
1
+ import { commaList, toggleValue } from '@fusion.dev/helix-core';
2
+ export const pressedValue = (mode, value, pressed) => {
3
+ const now = commaList(value);
4
+ if (mode === 'single') {
5
+ return now.includes(pressed) ? '' : pressed;
6
+ }
7
+ if (mode === 'multiple') {
8
+ return toggleValue(now, pressed).join(', ');
9
+ }
10
+ return value;
11
+ };
12
+ export const rowStop = (count, at, step) => {
13
+ if (count <= 1) {
14
+ return 0;
15
+ }
16
+ const next = at + step;
17
+ if (next >= count) {
18
+ return 0;
19
+ }
20
+ return next < 0 ? count - 1 : next;
21
+ };
@@ -4,14 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, countLabel, customElement, DelayController, DisabledCascadeController, HelixFilterEvent, HelixFormElement, hostAria, MixinCollection, MixinControllerValue, MixinField, reportValues, requestChange, scrollItemIntoView, Typeahead, typesCharacter, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, countLabel, customElement, DelayController, DisabledCascadeController, HelixFilterEvent, HelixFormElement, hostAria, MixinCollection, MixinControllerValue, MixinField, rangeAdd, reportValues, requestChange, scrollItemIntoView, Typeahead, typesCharacter, } from '@fusion.dev/helix-core';
8
8
  import { magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { HelixHeadlessOptionGroup } from '../../option/group/option-group.js';
13
13
  import { HelixHeadlessOption } from '../../option/root/option-root.js';
14
- import { rangeAdd } from '../range.js';
15
14
  const MOVES = new Set(['ArrowDown', 'ArrowUp', 'End', 'Home', 'PageDown', 'PageUp']);
16
15
  const FIELD_MOVES = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
17
16
  const PAGE = 10;