@nldd/design-system 0.8.82 → 0.8.83

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 (146) hide show
  1. package/custom-elements.json +436 -68
  2. package/dist/components/actions/button/button.d.ts +5 -0
  3. package/dist/components/actions/button/button.d.ts.map +1 -1
  4. package/dist/components/actions/button/button.js +8 -0
  5. package/dist/components/actions/button/button.js.map +1 -1
  6. package/dist/components/actions/button/button.template.d.ts.map +1 -1
  7. package/dist/components/actions/button/button.template.js +2 -0
  8. package/dist/components/actions/button/button.template.js.map +1 -1
  9. package/dist/components/actions/icon-button/icon-button.d.ts +1 -1
  10. package/dist/components/actions/icon-button/icon-button.js +1 -1
  11. package/dist/components/components.js +894 -226
  12. package/dist/components/content/avatar/avatar.styles.d.ts.map +1 -1
  13. package/dist/components/content/avatar/avatar.styles.js +5 -1
  14. package/dist/components/content/avatar/avatar.styles.js.map +1 -1
  15. package/dist/components/content/icon/icon-aliases.d.ts.map +1 -1
  16. package/dist/components/content/icon/icon-aliases.js +78 -6
  17. package/dist/components/content/icon/icon-aliases.js.map +1 -1
  18. package/dist/components/content/icon/icon-gallery-status.d.ts.map +1 -1
  19. package/dist/components/content/icon/icon-gallery-status.js +55 -94
  20. package/dist/components/content/icon/icon-gallery-status.js.map +1 -1
  21. package/dist/components/content/icon/icon-registry.d.ts.map +1 -1
  22. package/dist/components/content/icon/icon-registry.js +34 -4
  23. package/dist/components/content/icon/icon-registry.js.map +1 -1
  24. package/dist/components/content/icon/icon.d.ts +8 -2
  25. package/dist/components/content/icon/icon.d.ts.map +1 -1
  26. package/dist/components/content/icon/icon.js +17 -2
  27. package/dist/components/content/icon/icon.js.map +1 -1
  28. package/dist/components/content/icon/icon.styles.d.ts.map +1 -1
  29. package/dist/components/content/icon/icon.styles.js +8 -0
  30. package/dist/components/content/icon/icon.styles.js.map +1 -1
  31. package/dist/components/content/text/text.d.ts +50 -0
  32. package/dist/components/content/text/text.d.ts.map +1 -0
  33. package/dist/components/content/text/text.js +81 -0
  34. package/dist/components/content/text/text.js.map +1 -0
  35. package/dist/components/content/text/text.styles.d.ts +2 -0
  36. package/dist/components/content/text/text.styles.d.ts.map +1 -0
  37. package/dist/components/content/text/text.styles.js +116 -0
  38. package/dist/components/content/text/text.styles.js.map +1 -0
  39. package/dist/components/content/text/text.template.d.ts +3 -0
  40. package/dist/components/content/text/text.template.d.ts.map +1 -0
  41. package/dist/components/content/text/text.template.js +7 -0
  42. package/dist/components/content/text/text.template.js.map +1 -0
  43. package/dist/components/content/title/title.styles.d.ts.map +1 -1
  44. package/dist/components/content/title/title.styles.js +9 -13
  45. package/dist/components/content/title/title.styles.js.map +1 -1
  46. package/dist/components/{icon-CIpJq_cf.js → icon-C1Kikpol.js} +100 -10
  47. package/dist/components/{icon-button-BAkUJTn6.js → icon-button-BYy6hnAY.js} +2 -2
  48. package/dist/components/index.d.ts +2 -1
  49. package/dist/components/index.d.ts.map +1 -1
  50. package/dist/components/index.js +2 -1
  51. package/dist/components/index.js.map +1 -1
  52. package/dist/components/inputs/combo-box/combo-box.d.ts +2 -0
  53. package/dist/components/inputs/combo-box/combo-box.d.ts.map +1 -1
  54. package/dist/components/inputs/combo-box/combo-box.js +22 -0
  55. package/dist/components/inputs/combo-box/combo-box.js.map +1 -1
  56. package/dist/components/inputs/combo-box/combo-box.styles.d.ts.map +1 -1
  57. package/dist/components/inputs/combo-box/combo-box.styles.js +5 -0
  58. package/dist/components/inputs/combo-box/combo-box.styles.js.map +1 -1
  59. package/dist/components/inputs/combo-box/combo-box.template.d.ts.map +1 -1
  60. package/dist/components/inputs/combo-box/combo-box.template.js +23 -20
  61. package/dist/components/inputs/combo-box/combo-box.template.js.map +1 -1
  62. package/dist/components/inputs/date-field/date-field.template.d.ts.map +1 -1
  63. package/dist/components/inputs/date-field/date-field.template.js +7 -1
  64. package/dist/components/inputs/date-field/date-field.template.js.map +1 -1
  65. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.d.ts +8 -1
  66. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.d.ts.map +1 -1
  67. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.js +8 -0
  68. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.js.map +1 -1
  69. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.template.d.ts.map +1 -1
  70. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.template.js +2 -0
  71. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.template.js.map +1 -1
  72. package/dist/components/inputs/text-field/text-field.d.ts +21 -0
  73. package/dist/components/inputs/text-field/text-field.d.ts.map +1 -1
  74. package/dist/components/inputs/text-field/text-field.js +48 -0
  75. package/dist/components/inputs/text-field/text-field.js.map +1 -1
  76. package/dist/components/inputs/text-field/text-field.template.d.ts.map +1 -1
  77. package/dist/components/inputs/text-field/text-field.template.js +2 -0
  78. package/dist/components/inputs/text-field/text-field.template.js.map +1 -1
  79. package/dist/components/inputs/time-field/time-field.template.d.ts.map +1 -1
  80. package/dist/components/inputs/time-field/time-field.template.js +7 -1
  81. package/dist/components/inputs/time-field/time-field.template.js.map +1 -1
  82. package/dist/components/layout/box/box.d.ts +3 -3
  83. package/dist/components/layout/box/box.d.ts.map +1 -1
  84. package/dist/components/layout/box/box.js +3 -3
  85. package/dist/components/layout/box/box.js.map +1 -1
  86. package/dist/components/layout/box/box.styles.d.ts.map +1 -1
  87. package/dist/components/layout/box/box.styles.js +3 -6
  88. package/dist/components/layout/box/box.styles.js.map +1 -1
  89. package/dist/components/layout/sheet/sheet.styles.d.ts.map +1 -1
  90. package/dist/components/layout/sheet/sheet.styles.js +10 -3
  91. package/dist/components/layout/sheet/sheet.styles.js.map +1 -1
  92. package/dist/components/lists-and-tables/list/list.d.ts +60 -45
  93. package/dist/components/lists-and-tables/list/list.d.ts.map +1 -1
  94. package/dist/components/lists-and-tables/list/list.js +94 -80
  95. package/dist/components/lists-and-tables/list/list.js.map +1 -1
  96. package/dist/components/lists-and-tables/list-item/list-item.d.ts +94 -21
  97. package/dist/components/lists-and-tables/list-item/list-item.d.ts.map +1 -1
  98. package/dist/components/lists-and-tables/list-item/list-item.js +275 -48
  99. package/dist/components/lists-and-tables/list-item/list-item.js.map +1 -1
  100. package/dist/components/lists-and-tables/list-item/list-item.styles.d.ts.map +1 -1
  101. package/dist/components/lists-and-tables/list-item/list-item.styles.js +116 -34
  102. package/dist/components/lists-and-tables/list-item/list-item.styles.js.map +1 -1
  103. package/dist/components/lists-and-tables/list-item/list-item.template.d.ts +1 -1
  104. package/dist/components/lists-and-tables/list-item/list-item.template.d.ts.map +1 -1
  105. package/dist/components/lists-and-tables/list-item/list-item.template.js +5 -2
  106. package/dist/components/lists-and-tables/list-item/list-item.template.js.map +1 -1
  107. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.d.ts +116 -0
  108. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.d.ts.map +1 -0
  109. package/dist/components/lists-and-tables/{list-item-action/list-item-action.js → list-item-segment/list-item-segment.js} +77 -52
  110. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.js.map +1 -0
  111. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.styles.d.ts +2 -0
  112. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.styles.d.ts.map +1 -0
  113. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.styles.js +179 -0
  114. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.styles.js.map +1 -0
  115. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.template.d.ts +3 -0
  116. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.template.d.ts.map +1 -0
  117. package/dist/components/lists-and-tables/{list-item-action/list-item-action.template.js → list-item-segment/list-item-segment.template.js} +5 -4
  118. package/dist/components/lists-and-tables/list-item-segment/list-item-segment.template.js.map +1 -0
  119. package/dist/components/status-and-feedback/badge/badge.d.ts +8 -2
  120. package/dist/components/status-and-feedback/badge/badge.d.ts.map +1 -1
  121. package/dist/components/status-and-feedback/badge/badge.js +17 -1
  122. package/dist/components/status-and-feedback/badge/badge.js.map +1 -1
  123. package/dist/components/status-and-feedback/badge/badge.styles.d.ts.map +1 -1
  124. package/dist/components/status-and-feedback/badge/badge.styles.js +35 -6
  125. package/dist/components/status-and-feedback/badge/badge.styles.js.map +1 -1
  126. package/dist/components/status-and-feedback/modal-dialog/modal-dialog.styles.d.ts.map +1 -1
  127. package/dist/components/status-and-feedback/modal-dialog/modal-dialog.styles.js +7 -0
  128. package/dist/components/status-and-feedback/modal-dialog/modal-dialog.styles.js.map +1 -1
  129. package/dist/css/variables.css +25 -1
  130. package/dist/nldd.min.js +1002 -593
  131. package/dist/types/vue.d.ts +93 -47
  132. package/dist/utilities/scroll-mode-controller.d.ts +9 -1
  133. package/dist/utilities/scroll-mode-controller.d.ts.map +1 -1
  134. package/dist/utilities/scroll-mode-controller.js +10 -2
  135. package/dist/utilities/scroll-mode-controller.js.map +1 -1
  136. package/package.json +8 -4
  137. package/dist/components/lists-and-tables/list-item-action/list-item-action.d.ts +0 -108
  138. package/dist/components/lists-and-tables/list-item-action/list-item-action.d.ts.map +0 -1
  139. package/dist/components/lists-and-tables/list-item-action/list-item-action.js.map +0 -1
  140. package/dist/components/lists-and-tables/list-item-action/list-item-action.styles.d.ts +0 -2
  141. package/dist/components/lists-and-tables/list-item-action/list-item-action.styles.d.ts.map +0 -1
  142. package/dist/components/lists-and-tables/list-item-action/list-item-action.styles.js +0 -145
  143. package/dist/components/lists-and-tables/list-item-action/list-item-action.styles.js.map +0 -1
  144. package/dist/components/lists-and-tables/list-item-action/list-item-action.template.d.ts +0 -3
  145. package/dist/components/lists-and-tables/list-item-action/list-item-action.template.d.ts.map +0 -1
  146. package/dist/components/lists-and-tables/list-item-action/list-item-action.template.js.map +0 -1
@@ -13,10 +13,62 @@ import { template } from './list-item.template.js';
13
13
  import { isPointerMode } from '../../../utilities/input-modality.js';
14
14
  import { withTranslations } from '../../../utilities/with-translations.js';
15
15
  import { nlddListItemTranslations } from './list-item.i18n.js';
16
+ /** Anything in a row's light DOM that is a tab stop of its own. `[tabindex]`
17
+ * counts because an authored one makes any element focusable; the value itself
18
+ * is read per element, and put back when the row becomes the current one. */
19
+ const OWN_TAB_STOP = 'a[href], button, input, select, textarea, [contenteditable]:not([contenteditable="false"]), [tabindex]';
20
+ /** Keyed on the element, so a row that is re-rendered around the same control
21
+ * still knows what the consumer authored. */
22
+ const authoredTabState = new WeakMap();
23
+ function authoredStateOf(el) {
24
+ let authored = authoredTabState.get(el);
25
+ if (!authored) {
26
+ authored = { noTab: 'noTab' in el ? el.noTab : undefined, tabindex: el.getAttribute('tabindex') };
27
+ authoredTabState.set(el, authored);
28
+ }
29
+ return authored;
30
+ }
31
+ function applyAuthoredTabState(el, authored) {
32
+ if (authored.noTab !== undefined) {
33
+ el.noTab = authored.noTab;
34
+ return;
35
+ }
36
+ if (authored.tabindex === null)
37
+ el.removeAttribute('tabindex');
38
+ else
39
+ el.setAttribute('tabindex', authored.tabindex);
40
+ }
41
+ /**
42
+ * Puts one control in or out of the tab order on behalf of the row it sits in.
43
+ *
44
+ * A design-system control keeps its tab stop inside its own shadow root, where
45
+ * a tabindex on the host cannot reach it, so it carries `no-tab` for exactly
46
+ * this. Everything else is native and takes the tabindex directly. A consumer
47
+ * who set either themselves keeps it: their `no-tab` or `tabindex="-1"` is a
48
+ * decision, not a leftover.
49
+ */
50
+ function setTabbable(el, tabbable) {
51
+ const authored = authoredStateOf(el);
52
+ if (authored.noTab !== undefined) {
53
+ el.noTab = authored.noTab || !tabbable;
54
+ return;
55
+ }
56
+ if (tabbable)
57
+ applyAuthoredTabState(el, authored);
58
+ else
59
+ el.setAttribute('tabindex', '-1');
60
+ }
61
+ function releaseTabbable(el) {
62
+ const authored = authoredTabState.get(el);
63
+ if (!authored)
64
+ return;
65
+ applyAuthoredTabState(el, authored);
66
+ authoredTabState.delete(el);
67
+ }
16
68
  /**
17
69
  * A row within an `nldd-list`. Renders as a link when `href` is set, as a
18
70
  * checkbox when `checkbox` is set, as a button when `button` is set, or as a
19
- * plain container otherwise. All cells and action segmented actions share one flat
71
+ * plain container otherwise. All cells and segments share one flat
20
72
  * slot, in source order.
21
73
  *
22
74
  * ## Geometry
@@ -30,7 +82,7 @@ import { nlddListItemTranslations } from './list-item.i18n.js';
30
82
  * itself outward (`is-interactive` host class → negative inline margin)
31
83
  * and pads the row block back by the same amount, so content stays on the
32
84
  * grid. A row-wide action owns that padding itself, making the whole
33
- * widened box one hit area; slotted segmented actions stay in the grid, so their
85
+ * widened box one hit area; slotted segments stay in the grid, so their
34
86
  * footprint is position-independent and tree columns line up at any depth.
35
87
  *
36
88
  * ## Divider
@@ -39,7 +91,7 @@ import { nlddListItemTranslations } from './list-item.i18n.js';
39
91
  * an avatar, a checkbox, or the spacers a tree indents with. Rows of different
40
92
  * shapes then still line their dividers up with each other, and a tree's
41
93
  * dividers step inward with its indentation. Text inside an
42
- * `nldd-list-item-action` counts as the row's content, and the marker is that
94
+ * `nldd-list-item-segment` counts as the row's content, and the marker is that
43
95
  * text cell rather than the action: the action carries its own inline padding,
44
96
  * so its edge sits before the words. A row with no text or title cell keeps the
45
97
  * full content width. Mark a cell with `divider-start` and/or `divider-end` to
@@ -51,7 +103,7 @@ import { nlddListItemTranslations } from './list-item.i18n.js';
51
103
  *
52
104
  * ## Disclosure
53
105
  * A branch row can disclose its children in two ways: a dedicated
54
- * `nldd-list-item-action[disclosure]` segment (only the chevron is clickable),
106
+ * `nldd-list-item-segment[disclosure]` segment (only the chevron is clickable),
55
107
  * or the row itself as the control (`button` + `expanded`, the whole row
56
108
  * toggles). In the second case, mark the chevron's `nldd-icon-cell` with
57
109
  * `disclosure` and the row turns it with `expanded` — the same affordance,
@@ -84,17 +136,19 @@ import { nlddListItemTranslations } from './list-item.i18n.js';
84
136
  * @element nldd-list-item
85
137
  *
86
138
  * @attr {'sm'|'md'} size - Row size (default: 'md'). Pushed onto the cells whose `size` means the same scale (nldd-text-cell, nldd-drag-handle-cell), so it is written once per row instead of once per cell. A size set on the cell itself wins. Cells where `size` means something else — pixels on nldd-icon-cell / nldd-spacer-cell, a heading scale on nldd-title-cell — are left alone.
87
- * @attr {boolean} selected - Marks the item as selected. Selection is consumer-managed; the list never sets it. In a `navigation` parent it puts `aria-current="page"` on the inner action, in a `listbox` parent it drives `aria-selected`.
88
- * @attr {boolean} button - Renders the item as a `<button>`; ignored when `href` is set (a link wins)
89
- * @attr {boolean} checkbox - Makes the whole row a `role="checkbox"` control; ignored when `href` is set
139
+ * @attr {boolean} selected - Marks the item as selected: it is one of the rows you picked. Selection is consumer-managed; the list never sets it. In a `navigation` parent it puts `aria-current="page"` on the inner action, in a `listbox` parent it drives `aria-selected`.
140
+ * @attr {boolean} current - Marks the item as the one you are on: the page a menu row points at, the record a list has open. Exactly one row in a list carries it, where `selected` may be on many. It paints like `selected` at rest, and takes the highlighted fill while focus is anywhere in the row — including inside a nested `nldd-list-item-segment`, which is what a segmented row needs: the focus never reaches the row's own control, because there is none. In a `navigation` parent it puts `aria-current="page"` on the inner action. On a segmented row set it on the segment that holds the link instead: the row reads `current` off its own segments and paints itself, so it is written once, where `aria-current` belongs.
141
+ * @attr {boolean} button - Renders the item as a `<button>`. Last of the three: `href` and `checkbox` both win over it.
142
+ * @attr {boolean} checkbox - Makes the whole row a `role="checkbox"` control. Wins over `button`, loses to `href`.
90
143
  * @attr {boolean} checked - Checked state of a `checkbox` row; the item toggles it on activation
91
- * @attr {boolean} expanded - Disclosure state. Drives the `children` group's visibility AND supplies `aria-expanded` to the row's own control when the row is interactive, or to the segmented action marked `disclosure`. Written once either way; the item DEV-warns when there is nowhere for it to live.
92
- * @attr {string} href - Renders the item as an `<a>` with this URL. Takes precedence over `button`; without either the item is a plain container with no action.
144
+ * @attr {boolean} disabled - Switches the row's own control off: a `button` or `checkbox` row stops responding and dims, a `href` row gets `aria-disabled` and its click is blocked (a link cannot be disabled natively). A row without a control of its own has nothing to switch off, and segments carry their own `disabled`. The arrow keys skip a disabled row.
145
+ * @attr {boolean} expanded - Disclosure state. Drives the `children` group's visibility AND supplies `aria-expanded` to the row's own control when the row is interactive, or to the segment marked `disclosure`. Written once either way; the item DEV-warns when there is nowhere for it to live.
146
+ * @attr {string} href - Renders the item as an `<a>` with this URL. Wins over `checkbox` and `button`; without any of the three the item is a plain container with no action.
93
147
  * @attr {string} target - Link target forwarded to the `<a>` (e.g. '_blank'); only applies with `href`. With '_blank' a visually hidden "opens in new tab" announcement is added for assistive technology.
94
148
  * @attr {string} rel - Link rel forwarded to the `<a>` (e.g. 'noopener noreferrer'); only applies with `href`
95
149
  * @attr {boolean} reorderable - Set by the parent `nldd-list` when its own `reorderable` is on (with `type="list"`); consumers do not set this. Serves as a CSS hook for drag handle visibility.
96
150
  *
97
- * @slot - Cells and segmented actions, in source order
151
+ * @slot - Cells and segments, in source order
98
152
  * @slot children - Child rows of a branch in an `nldd-list type="tree"`. Rendered as a `role="group"` below the row, hidden while `expanded` is false. The nesting IS the hierarchy, so aria-level / -posinset / -setsize are derived, not authored. The group has no styling of its own: repeat a spacer-cell per level to indent, or show depth some other way.
99
153
  *
100
154
  * @fires change - On a `checkbox` row after it toggles; detail: { checked: boolean }
@@ -104,20 +158,39 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
104
158
  super(...arguments);
105
159
  this.size = 'md';
106
160
  this.selected = false;
161
+ /**
162
+ * The row you are on, as opposed to the rows you picked (`selected`).
163
+ *
164
+ * Kept apart because the two say different things and a list can show both:
165
+ * a checkbox list where three rows are ticked and one is open. At rest they
166
+ * paint the same, so a menu reads the same as it always did; the difference
167
+ * shows when focus is in the row.
168
+ */
169
+ this.current = false;
107
170
  /** When set, renders the item as a button; ignored when href is set. */
108
171
  this.button = false;
109
172
  /** When set, the whole row is the checkbox control. Ignored when href is set. */
110
173
  this.checkbox = false;
111
174
  /** Checked state of a `checkbox` row. Toggled by the item on activation. */
112
175
  this.checked = false;
176
+ /**
177
+ * Switches off the row's own control.
178
+ *
179
+ * Only the row's own: a row built out of segments has no control here, and
180
+ * each segment carries its own `disabled`. A link cannot be disabled the way
181
+ * a button can, so there it is `aria-disabled` plus a blocked click, the
182
+ * same trade `nldd-button` makes.
183
+ */
184
+ this.disabled = false;
113
185
  /** Set by the parent nldd-list when reorderable is enabled. Used as a CSS hook for drag handle visibility. */
114
186
  this.reorderable = false;
115
187
  this._showChildren = false;
116
188
  this._parentType = 'list';
117
- /** Set by the parent nldd-list when `arrow-navigation` (roving tabindex) is on.
189
+ /** Set by the parent nldd-list when it runs the roving tabindex (everywhere
190
+ * except a listbox and a reorderable list).
118
191
  * Switches the inner action from a normal tab stop to a roving one. */
119
192
  this._arrowNavigation = false;
120
- /** Set by the parent nldd-list: when arrow-navigation is on, exactly one item
193
+ /** Set by the parent nldd-list: while the roving runs, exactly one item
121
194
  * is the roving entry point (tabindex 0); the rest are tabindex -1. */
122
195
  this._rovingActive = false;
123
196
  /** Set by the parent nldd-list in `type="listbox"`: marks this option as
@@ -129,6 +202,10 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
129
202
  this._isBoxed = false;
130
203
  this._hasCheckedSegment = false;
131
204
  this._lightDomObserver = null;
205
+ /** The controls in this row the roving takes in and out of the tab order.
206
+ * Rebuilt when the row's light DOM changes, so a keypress does not walk
207
+ * every cell of every row again. */
208
+ this._rovingControlsCache = null;
132
209
  /** Watches the row and its divider-marker cells for size changes; marker
133
210
  * insets are measured, so any reflow can move them. */
134
211
  this._dividerMarkerObserver = null;
@@ -137,6 +214,13 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
137
214
  /** Remembers the size a cell was authored with, so a later push can restore it. */
138
215
  this._ownCellSize = new WeakMap();
139
216
  this._handleClick = (e) => {
217
+ // A disabled <button> never gets here, but a link does: there is no such
218
+ // thing as a disabled <a>, so the row stops the navigation itself.
219
+ if (this.disabled) {
220
+ e.preventDefault();
221
+ e.stopPropagation();
222
+ return;
223
+ }
140
224
  // Safari and Firefox on Mac don't focus buttons on click. Force focus
141
225
  // so :has(.list-item__action:focus) and :focus-within CSS work reliably.
142
226
  this._action?.focus();
@@ -160,7 +244,24 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
160
244
  this._handleFocusOut = () => {
161
245
  this._action?.classList.remove('is-pointer-focus');
162
246
  };
247
+ /**
248
+ * See the same handler in nldd-list-item-segment: Safari hands focus to the
249
+ * body while the mouse is down, which drops a row out of its lit state until
250
+ * you let go. Left alone on a reorderable row and on a row whose press came
251
+ * from a nested one, where the default is doing real work (dragging).
252
+ */
253
+ this._onMouseDown = (e) => {
254
+ if (e.button > 0 || this.reorderable)
255
+ return;
256
+ if (this._isFromNestedRow(e))
257
+ return;
258
+ if (!this._action)
259
+ return;
260
+ e.preventDefault();
261
+ };
163
262
  this._onPointerDown = (e) => {
263
+ if (this.disabled)
264
+ return;
164
265
  // Primary button / touch / pen only (mouse right-click has button > 0).
165
266
  if (e.button > 0)
166
267
  return;
@@ -170,6 +271,11 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
170
271
  if (this._isFromNestedRow(e))
171
272
  return;
172
273
  this._action?.classList.add('is-pressed');
274
+ // Safari does not focus a <button> on click, so a row whose paint follows
275
+ // the focus in it would stay unlit there while the other browsers light
276
+ // up. Focusing on the press makes the three agree; the ring stays hidden
277
+ // because is-pointer-focus already marks a pointer focus.
278
+ this._action?.focus({ preventScroll: true });
173
279
  };
174
280
  this._clearPressed = () => {
175
281
  this._action?.classList.remove('is-pressed');
@@ -193,10 +299,11 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
193
299
  // turns into a scroll (the browser fires pointercancel for that pointer),
194
300
  // so `active` never sticks while the user scrolls the list.
195
301
  this.addEventListener('pointerdown', this._onPointerDown);
302
+ this.addEventListener('mousedown', this._onMouseDown);
196
303
  this.addEventListener('pointerup', this._clearPressed);
197
304
  this.addEventListener('pointercancel', this._clearPressed);
198
305
  // Light-DOM state the row derives styling from: a checked checkbox-
199
- // segment (row-wide selected fill), which segmented actions own a row edge, and
306
+ // segment (row-wide selected fill), which segments own a row edge, and
200
307
  // the divider markers. Attribute observation, not events: consumers
201
308
  // also set these programmatically via setAttribute.
202
309
  this._lightDomObserver = new MutationObserver(() => this._onLightDomChange());
@@ -204,7 +311,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
204
311
  subtree: true,
205
312
  childList: true,
206
313
  attributes: true,
207
- attributeFilter: ['checked', 'divider-start', 'divider-end'],
314
+ attributeFilter: ['checked', 'current', 'divider-start', 'divider-end'],
208
315
  });
209
316
  this._onLightDomChange();
210
317
  }
@@ -214,6 +321,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
214
321
  this.removeEventListener('focusout', this._handleFocusOut);
215
322
  this.removeEventListener('click', this._handleClick);
216
323
  this.removeEventListener('pointerdown', this._onPointerDown);
324
+ this.removeEventListener('mousedown', this._onMouseDown);
217
325
  this.removeEventListener('pointerup', this._clearPressed);
218
326
  this.removeEventListener('pointercancel', this._clearPressed);
219
327
  this._lightDomObserver?.disconnect();
@@ -223,38 +331,118 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
223
331
  }
224
332
  _onLightDomChange() {
225
333
  this._updateCheckedSegment();
334
+ this._updateCurrentSegment();
226
335
  this._updateInteractive();
227
336
  this._measureDividerMarkers();
337
+ this._rovingControlsCache = null;
338
+ // A framework can fill a row in a later pass than the one that put the row
339
+ // in the list, so both have to run again: the tab stops inside this row,
340
+ // and the list's choice of which rows the arrows stop at — a row that just
341
+ // gained its only control was not a stop when that choice was made.
342
+ if (this._arrowNavigation) {
343
+ this._syncRovingTabStops();
344
+ this._parentList?._updateRoving();
345
+ }
346
+ }
347
+ get _parentList() {
348
+ return this.closest('nldd-list');
349
+ }
350
+ /**
351
+ * The controls in this row that own a tab stop: a design-system control that
352
+ * carries `no-tab`, or anything natively focusable. Segments are left
353
+ * out — they have their own channel (`_tabbable`) — but a control slotted
354
+ * inside one is not, so it is managed here.
355
+ *
356
+ * Custom elements that have not upgraded yet cannot answer whether they carry
357
+ * `no-tab`, so the cache is dropped and the row re-syncs once they do.
358
+ */
359
+ _rovingControls() {
360
+ if (this._rovingControlsCache)
361
+ return this._rovingControlsCache;
362
+ const pending = new Set();
363
+ const controls = this._ownDescendants('*').filter((el) => {
364
+ const tag = el.tagName.toLowerCase();
365
+ if (tag === 'nldd-list-item-segment')
366
+ return false;
367
+ if (tag.includes('-') && !customElements.get(tag)) {
368
+ pending.add(tag);
369
+ return false;
370
+ }
371
+ return 'noTab' in el || el.matches(OWN_TAB_STOP);
372
+ });
373
+ this._rovingControlsCache = controls;
374
+ pending.forEach((tag) => {
375
+ customElements.whenDefined(tag).then(() => {
376
+ this._rovingControlsCache = null;
377
+ if (this._arrowNavigation)
378
+ this._syncRovingTabStops();
379
+ });
380
+ });
381
+ return controls;
382
+ }
383
+ /**
384
+ * Whether the arrow keys stop at this row.
385
+ *
386
+ * Anything you can operate makes a row a stop: its own link, button or
387
+ * checkbox, a segment, or a control sitting in one of its cells. A
388
+ * row of nothing but text is skipped, because focus that leads nowhere is a
389
+ * dead end for a keyboard.
390
+ *
391
+ * @internal Read by the parent nldd-list.
392
+ */
393
+ get _isRovingStop() {
394
+ // Nothing to land on: a disabled control is not focusable, so a stop there
395
+ // would swallow the arrow key and leave focus where it was.
396
+ if (this.disabled)
397
+ return false;
398
+ if (this.href || this.button || this.checkbox)
399
+ return true;
400
+ if (this._ownDescendants('nldd-list-item-segment:not([disabled])').length > 0)
401
+ return true;
402
+ return this._rovingControls().some((control) => !control.hasAttribute('disabled'));
403
+ }
404
+ /**
405
+ * A segment marked `current` makes the whole row the current one.
406
+ *
407
+ * A row cut into segments has no control of its own, so `aria-current="page"`
408
+ * belongs on the segment that holds the link. Written there, the row would
409
+ * stay unpainted and the consumer had to set `current` twice, once for the
410
+ * semantics and once for the colour, with nothing to catch the two drifting
411
+ * apart. The row reads it off its own segments instead — reading, not
412
+ * writing: the attributes stay the consumer's.
413
+ */
414
+ _updateCurrentSegment() {
415
+ this.toggleAttribute('data-current', this._ownDescendants('nldd-list-item-segment[current]').length > 0);
228
416
  }
229
417
  /** A checked checkbox-segment reads as "this row is selected", so the fill
230
418
  * covers the whole row — including a disclosure segment — instead of
231
- * stopping at the segmented action boundary. Scoped to own descendants: a checked
419
+ * stopping at the segment boundary. Scoped to own descendants: a checked
232
420
  * row in a nested branch must not light up its ancestors. */
233
421
  _updateCheckedSegment() {
234
422
  this._hasCheckedSegment =
235
- this._ownDescendants('nldd-list-item-action[checkbox][checked]').length > 0;
423
+ this._ownDescendants('nldd-list-item-segment[checkbox][checked]').length > 0;
236
424
  }
237
425
  /** A row with any action — its own href/button/checkbox or a slotted
238
426
  * segment — carries the widened geometry: negative host margin plus the
239
427
  * compensating row padding (see the geometry rules in the class doc).
240
428
  *
241
- * An action segment at a row edge owns the padding for that side itself,
242
- * so the row drops its own there and the segmented action lands on the row's edge.
429
+ * An segment at a row edge owns the padding for that side itself,
430
+ * so the row drops its own there and the segment lands on the row's edge.
243
431
  * Stamped as classes because the CSS cannot ask: `:has()` is not allowed
244
432
  * inside `:host()`, and the row's own padding is what has to change — a
245
- * `::slotted()` rule could only reach the segmented action. */
433
+ * `::slotted()` rule could only reach the segment. */
246
434
  _updateInteractive() {
247
435
  const interactive = !!this.href || this.button || this.checkbox ||
248
- this._ownDescendants('nldd-list-item-action').length > 0;
436
+ this._ownDescendants('nldd-list-item-segment').length > 0;
249
437
  this.classList.toggle('is-interactive', interactive);
250
438
  // The row's own children, not the branch's child rows: those go to the
251
439
  // `children` slot and render BELOW the row, so on a branch row the last
252
440
  // element child is a nested row rather than the row's trailing segment.
253
441
  const ownChildren = Array.from(this.children)
254
442
  .filter(el => el.getAttribute('slot') !== 'children');
255
- const isSegment = (el) => el?.tagName.toLowerCase() === 'nldd-list-item-action';
256
- this.classList.toggle('has-leading-action', isSegment(ownChildren[0]));
257
- this.classList.toggle('has-trailing-action', isSegment(ownChildren[ownChildren.length - 1]));
443
+ const isSegment = (el) => el?.tagName.toLowerCase() === 'nldd-list-item-segment';
444
+ this.classList.toggle('has-leading-segment', isSegment(ownChildren[0]));
445
+ this.classList.toggle('has-trailing-segment', isSegment(ownChildren[ownChildren.length - 1]));
258
446
  }
259
447
  firstUpdated() {
260
448
  // Clones are visual-only copies inside nldd-list's shadow root: their
@@ -271,12 +459,12 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
271
459
  }
272
460
  /**
273
461
  * The row's own descendants for `selector` — everything down to and inside
274
- * its action segmented actions, but NOT inside a nested row. In a tree the child rows
462
+ * its segments, but NOT inside a nested row. In a tree the child rows
275
463
  * are DOM children of this row, so a plain querySelectorAll would push this
276
464
  * row's state into theirs (a parent's `expanded` turning every nested
277
465
  * chevron, its `size` overwriting theirs). A `:scope >` child selector is
278
466
  * too strict the other way: cells legitimately sit one level deeper, inside
279
- * an nldd-list-item-action.
467
+ * an nldd-list-item-segment.
280
468
  */
281
469
  _ownDescendants(selector) {
282
470
  return Array.from(this.querySelectorAll(selector))
@@ -311,7 +499,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
311
499
  });
312
500
  }
313
501
  updated(changed) {
314
- if (changed.has('selected') || changed.has('button') || changed.has('checkbox') || changed.has('href') || changed.has('_parentType')) {
502
+ if (changed.has('selected') || changed.has('current') || changed.has('button') || changed.has('checkbox') || changed.has('href') || changed.has('_parentType')) {
315
503
  this._updateAriaState();
316
504
  }
317
505
  if (changed.has('button') || changed.has('checkbox') || changed.has('href')) {
@@ -324,6 +512,14 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
324
512
  if (changed.has('_arrowNavigation') || changed.has('_rovingActive')) {
325
513
  this._syncRovingTabStops();
326
514
  }
515
+ // A row that switches off mid-flight stops being a stop, but the tab stop it
516
+ // already holds does not move by itself: a disabled anchor keeps its
517
+ // tabindex, so Tab would land on a row that answers to nothing until some
518
+ // unrelated change happened to re-run the roving.
519
+ if (changed.has('disabled')) {
520
+ this._syncRovingTabStops();
521
+ this._parentList?._updateRoving();
522
+ }
327
523
  this._propagateSize();
328
524
  }
329
525
  /**
@@ -342,8 +538,8 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
342
538
  /** Segments marked `disclosure` announce the row's expanded state, so the
343
539
  * consumer writes it once — here, where it also drives the children group. */
344
540
  _relayExpanded() {
345
- this._ownDescendants('nldd-list-item-action[disclosure]')
346
- .forEach(action => { action._rowExpanded = this._resolvedExpanded; });
541
+ this._ownDescendants('nldd-list-item-segment[disclosure]')
542
+ .forEach(segment => { segment._rowExpanded = this._resolvedExpanded; });
347
543
  }
348
544
  /** `expanded` needs a control to sit on; a plain container has none. */
349
545
  _warnOnStrayExpanded() {
@@ -353,9 +549,9 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
353
549
  return;
354
550
  if (this.href || this.button || this.checkbox)
355
551
  return;
356
- if (this._ownDescendants('nldd-list-item-action[disclosure]').length > 0)
552
+ if (this._ownDescendants('nldd-list-item-segment[disclosure]').length > 0)
357
553
  return;
358
- console.warn('nldd-list-item: `expanded` needs somewhere to live — make the row interactive (href, button or checkbox), or mark the segmented action that does the disclosing with `disclosure`.');
554
+ console.warn('nldd-list-item: `expanded` needs somewhere to live — make the row interactive (href, button or checkbox), or mark the segment that does the disclosing with `disclosure`.');
359
555
  }
360
556
  /** True when the item is an option in a `type="listbox"` parent. */
361
557
  get _isListboxOption() {
@@ -388,11 +584,11 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
388
584
  _applyParentType(type) {
389
585
  this._parentType = type;
390
586
  this._relayToChildren(item => item._applyParentType(type));
391
- // Segment actions need the list type too: in a listbox they degrade to a
587
+ // Segments need the list type too: in a listbox they degrade to a
392
588
  // plain container, since an `option` may not hold interactive descendants.
393
- // Own segmented actions only — nested rows get the relay above and push their own.
394
- this._ownDescendants('nldd-list-item-action')
395
- .forEach(action => action._applyParentType?.(type));
589
+ // Own segments only — nested rows get the relay above and push their own.
590
+ this._ownDescendants('nldd-list-item-segment')
591
+ .forEach(segment => segment._applyParentType?.(type));
396
592
  }
397
593
  /** Nested rows sit in this item's `children` slot, not the list's, so a plain
398
594
  * `querySelector` from the list walks past them. */
@@ -423,7 +619,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
423
619
  }
424
620
  // aria-current on the inner action (link/button) — navigation only
425
621
  const action = this._action;
426
- if (this._parentType === 'navigation' && this.selected && action) {
622
+ if (this._parentType === 'navigation' && (this.current || this.selected) && action) {
427
623
  action.setAttribute('aria-current', 'page');
428
624
  }
429
625
  else {
@@ -447,7 +643,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
447
643
  * full-width line back.
448
644
  */
449
645
  _implicitDividerStart() {
450
- // The marker is the CELL, never a segmented action around it: the action
646
+ // The marker is the CELL, never a segment around it: the action
451
647
  // carries its own inline padding, so its edge sits before the text. Own
452
648
  // descendants, so a text inside a segment counts as this row's content
453
649
  // while a nested row's text does not.
@@ -577,7 +773,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
577
773
  * so consumers can call `listItemEl.focus()` without reaching into shadow DOM.
578
774
  */
579
775
  focus(options) {
580
- // A tree row without a control of its own (only segmented actions, or
776
+ // A tree row without a control of its own (only segments, or
581
777
  // none at all) takes focus on the host: that is where role="treeitem"
582
778
  // sits, so assistive technology announces the row rather than a button
583
779
  // inside it. _syncRovingTabStops gives the host the tabindex for it.
@@ -590,8 +786,9 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
590
786
  /**
591
787
  * The roving tab stop, spread over the row and the controls inside it: the
592
788
  * current row is reachable with Tab, everything else only with the arrow
593
- * keys. Within the current row Tab then walks its segmented actions, so a row
594
- * that holds a checkbox and a chevron stays fully operable.
789
+ * keys. Within the current row Tab then walks its segments and any
790
+ * control in its cells, so a row that holds a checkbox and a chevron, or a
791
+ * menu button at its end, stays fully operable.
595
792
  *
596
793
  * A row that is not the current one carries NO tabindex at all rather than
597
794
  * -1. A shadow host with a tabindex is a focus scope of its own, and Chromium
@@ -603,11 +800,12 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
603
800
  * tab stop is in the DOM before focus is handed over.
604
801
  */
605
802
  _syncRovingTabStops() {
606
- const ownActions = Array.from(this.children).filter((el) => el.getAttribute('slot') !== 'children'
607
- && el.tagName.toLowerCase() === 'nldd-list-item-action');
803
+ const ownSegments = Array.from(this.children).filter((el) => el.getAttribute('slot') !== 'children'
804
+ && el.tagName.toLowerCase() === 'nldd-list-item-segment');
608
805
  if (!this._arrowNavigation) {
609
806
  this.removeAttribute('tabindex');
610
- ownActions.forEach((action) => { action._tabbable = undefined; });
807
+ ownSegments.forEach((segment) => { segment._tabbable = undefined; });
808
+ this._rovingControls().forEach(releaseTabbable);
611
809
  return;
612
810
  }
613
811
  // Only a row without its own control needs the host as focus target; with
@@ -618,14 +816,15 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
618
816
  else {
619
817
  this.setAttribute('tabindex', '0');
620
818
  }
621
- ownActions.forEach((action) => { action._tabbable = this._rovingActive; });
819
+ ownSegments.forEach((segment) => { segment._tabbable = this._rovingActive; });
820
+ this._rovingControls().forEach((control) => { setTabbable(control, this._rovingActive); });
622
821
  }
623
- /** The control that opens and closes this row: the segmented action marked
822
+ /** The control that opens and closes this row: the segment marked
624
823
  * `disclosure`, or the row itself when it is a button. Never a link — Left
625
824
  * and Right may not navigate away. */
626
825
  get _disclosureControl() {
627
826
  const action = Array.from(this.children).find((el) => el.getAttribute('slot') !== 'children'
628
- && el.tagName.toLowerCase() === 'nldd-list-item-action'
827
+ && el.tagName.toLowerCase() === 'nldd-list-item-segment'
629
828
  && el.hasAttribute('disclosure'));
630
829
  if (action)
631
830
  return action;
@@ -640,7 +839,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
640
839
  const control = this._disclosureControl;
641
840
  if (!control)
642
841
  return false;
643
- // The click lands on a segmented action, and that action pulls focus into
842
+ // The click lands on a segment, and that segment pulls focus into
644
843
  // its own control — with the pointer-focus flag set, since a click is what
645
844
  // it sees. Focus belongs to the row: that is the roving tab stop, and the
646
845
  // keyboard user has to keep seeing where they are.
@@ -650,6 +849,28 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
650
849
  this.focus();
651
850
  return true;
652
851
  }
852
+ /**
853
+ * Activates what this row is FOR, as opposed to what folds it: the first
854
+ * segment that is not the disclosure. A row that is its own control
855
+ * needs none of this, because the key reaches that control by itself.
856
+ * Returns false when there is nothing but a chevron.
857
+ */
858
+ _activatePrimary() {
859
+ const segments = [...this.querySelectorAll(':scope > nldd-list-item-segment')];
860
+ const primary = segments.find((segment) => !segment.hasAttribute('disclosure'));
861
+ if (!primary)
862
+ return false;
863
+ const control = primary.shadowRoot?.querySelector('a, button');
864
+ if (!control)
865
+ return false;
866
+ // Same trade as _activateDisclosure: the click pulls focus into the
867
+ // segment, and the row has to keep it, because the row is the tab stop.
868
+ const hadFocus = this.matches(':focus-within');
869
+ control.click();
870
+ if (hadFocus)
871
+ this.focus();
872
+ return true;
873
+ }
653
874
  /** The branch this row hangs under, or undefined for a top-level row. */
654
875
  get parentRow() {
655
876
  const parent = this.parentElement;
@@ -665,7 +886,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
665
886
  // search input is in the tab order, and active moves via
666
887
  // aria-activedescendant. Roving tabindex (arrow-navigation) supersedes
667
888
  // nothing here: the two modes are mutually exclusive on the parent list.
668
- // Otherwise, roving tabindex: when the list runs arrow-navigation,
889
+ // Otherwise, roving tabindex: while the list runs the roving,
669
890
  // exactly one item is the tab stop (0) and the rest are reachable only
670
891
  // via the arrow keys (-1). Off → undefined leaves the native
671
892
  // button/link as a normal tab stop.
@@ -674,7 +895,7 @@ let NLDDListItem = NLDDListItem_1 = class NLDDListItem extends withTranslations(
674
895
  : this._arrowNavigation
675
896
  ? (this._rovingActive ? '0' : '-1')
676
897
  : undefined;
677
- return template(this.button, this.href, this.target, this.rel, opensInNewTabLabel, actionTabindex, this._highlighted, this.checkbox, this.checked, this._resolvedExpanded, this._showChildren, this._hasCheckedSegment);
898
+ return template(this.button, this.href, this.target, this.rel, opensInNewTabLabel, actionTabindex, this._highlighted, this.checkbox, this.checked, this._resolvedExpanded, this._showChildren, this._hasCheckedSegment, this.disabled);
678
899
  }
679
900
  };
680
901
  NLDDListItem.styles = [listItemStyles];
@@ -690,6 +911,9 @@ __decorate([
690
911
  __decorate([
691
912
  property({ type: Boolean, reflect: true })
692
913
  ], NLDDListItem.prototype, "selected", void 0);
914
+ __decorate([
915
+ property({ type: Boolean, reflect: true })
916
+ ], NLDDListItem.prototype, "current", void 0);
693
917
  __decorate([
694
918
  property({ type: Boolean, reflect: true })
695
919
  ], NLDDListItem.prototype, "button", void 0);
@@ -699,6 +923,9 @@ __decorate([
699
923
  __decorate([
700
924
  property({ type: Boolean, reflect: true })
701
925
  ], NLDDListItem.prototype, "checked", void 0);
926
+ __decorate([
927
+ property({ type: Boolean, reflect: true })
928
+ ], NLDDListItem.prototype, "disabled", void 0);
702
929
  __decorate([
703
930
  property({ type: Boolean, reflect: true })
704
931
  ], NLDDListItem.prototype, "expanded", void 0);