@justeattakeaway/pie-list 0.3.0 → 0.5.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.
- package/README.md +238 -21
- package/custom-elements.json +111 -13
- package/dist/{PieElement-ab_PEQpW.js → PieElement-DQQMEr70.js} +1 -1
- package/dist/index-BH08tWcm.js +339 -0
- package/dist/index.js +1 -1
- package/dist/pie-list-item/defs.d.ts +28 -9
- package/dist/pie-list-item/defs.d.ts.map +1 -1
- package/dist/pie-list-item/index.d.ts +40 -11
- package/dist/pie-list-item/index.d.ts.map +1 -1
- package/dist/pie-list-item/index.js +4 -3
- package/dist/pie-list-item/react.js +8 -8
- package/package.json +3 -3
- package/src/pie-list-item/defs.ts +30 -10
- package/src/pie-list-item/index.ts +147 -23
- package/src/pie-list-item/list-item.scss +82 -1
- package/dist/index-mqipXuiX.js +0 -265
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type AriaProps = {
|
|
4
|
+
button?: {
|
|
5
|
+
haspopup?: 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true';
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const interactionTypes = ['none', 'radio', 'checkbox', 'switch', 'link', 'button'] as const;
|
|
4
10
|
|
|
5
|
-
export type
|
|
11
|
+
export type InteractionType = typeof interactionTypes[number];
|
|
6
12
|
|
|
7
13
|
export interface ListItemProps {
|
|
8
14
|
/**
|
|
@@ -38,20 +44,27 @@ export interface ListItemProps {
|
|
|
38
44
|
hasMedia?: boolean
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
47
|
+
* How the whole row behaves. A single prop drives the row's role, its accessible naming, whether
|
|
48
|
+
* it forwards clicks, and its interactive (hover/active) states. The item always generates the
|
|
49
|
+
* accessible name and description of whatever it hosts from its own text (`primaryText`,
|
|
50
|
+
* `secondaryText`, `metaText`), so you never set ARIA on the item or its slotted content yourself.
|
|
45
51
|
*
|
|
46
52
|
* - `none` (default) - a static, non-selectable list item (`role="listitem"`).
|
|
47
53
|
* - `radio` - hosts a `pie-radio` (used inside a `pie-radio-group`); the item is `presentation`.
|
|
48
54
|
* - `checkbox` - hosts a `pie-checkbox` (used inside a `pie-checkbox-group`); the item is `presentation`.
|
|
49
55
|
* - `switch` - hosts a `pie-switch`; there is no group, so the item stays `role="listitem"`.
|
|
56
|
+
* - `link` - the whole row is a single navigation link. Slot an empty `<a slot="link" href="...">`;
|
|
57
|
+
* it is stretched over the entire row as the clickable target and named from the item's text.
|
|
58
|
+
* - `button` - the whole row is a single button (for an in-page action rather than navigation).
|
|
59
|
+
* The item renders the button for you (no slotting); it is stretched invisibly over the entire
|
|
60
|
+
* row, named from the item's text, and dispatches a native `click` (pointer and keyboard).
|
|
61
|
+
* Listen for `click` on the `pie-list-item`. It is not a form control.
|
|
50
62
|
*
|
|
51
|
-
* Set this on each
|
|
52
|
-
* them out as a divided list automatically; this prop
|
|
63
|
+
* Set this on each interactive row. When radio/checkbox rows sit inside a `pie-radio-group` /
|
|
64
|
+
* `pie-checkbox-group`, the group lays them out as a divided list automatically; this prop
|
|
65
|
+
* governs the row's own role and behaviour.
|
|
53
66
|
*/
|
|
54
|
-
|
|
67
|
+
interactionType?: typeof interactionTypes[number]
|
|
55
68
|
|
|
56
69
|
/**
|
|
57
70
|
* Marks the row as disabled: it takes on the disabled styling and stops forwarding row clicks to
|
|
@@ -60,6 +73,13 @@ export interface ListItemProps {
|
|
|
60
73
|
* (static) item.
|
|
61
74
|
*/
|
|
62
75
|
disabled?: boolean
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Additional ARIA properties that the item cannot derive from its text props. `button.haspopup`
|
|
79
|
+
* applies when `interactionType="button"`: its value is forwarded to the internal `<button>`.
|
|
80
|
+
* Set it when the button row triggers a popup such as a dialog or menu.
|
|
81
|
+
*/
|
|
82
|
+
aria?: AriaProps
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
export type DefaultProps = ComponentDefaultProps<ListItemProps, keyof Omit<ListItemProps, 'primaryText' | 'secondaryText' | 'metaText'>>;
|
|
@@ -68,6 +88,6 @@ export const defaultProps: DefaultProps = {
|
|
|
68
88
|
isCompact: false,
|
|
69
89
|
isBold: false,
|
|
70
90
|
hasMedia: false,
|
|
71
|
-
|
|
91
|
+
interactionType: 'none',
|
|
72
92
|
disabled: false,
|
|
73
93
|
};
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
} from 'lit';
|
|
4
4
|
import { property, state } from 'lit/decorators.js';
|
|
5
5
|
import { classMap } from 'lit/directives/class-map.js';
|
|
6
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
6
7
|
import { consume, ContextProvider } from '@lit/context';
|
|
7
8
|
import {
|
|
8
9
|
safeCustomElement,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
type ContextualAria,
|
|
13
14
|
} from '@justeattakeaway/pie-webc-core';
|
|
14
15
|
import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement';
|
|
15
|
-
import { type ListItemProps, defaultProps,
|
|
16
|
+
import { type ListItemProps, defaultProps, interactionTypes } from './defs';
|
|
16
17
|
import styles from './list-item.scss?inline';
|
|
17
18
|
|
|
18
19
|
const componentSelector = 'pie-list-item';
|
|
@@ -21,6 +22,7 @@ const componentSelector = 'pie-list-item';
|
|
|
21
22
|
* @tagname pie-list-item
|
|
22
23
|
* @slot leading - Content shown at the start of the item (for example an icon, avatar, or a slotted radio/checkbox).
|
|
23
24
|
* @slot trailing - Content shown at the end of the item. Mutually exclusive with `metaText`.
|
|
25
|
+
* @slot link - Rendered only when `interactionType="link"`. Slot one empty anchor (e.g. `<a slot="link" href="/x">`) stretched over the whole row as the link target. Must contain no text.
|
|
24
26
|
*/
|
|
25
27
|
@safeCustomElement('pie-list-item')
|
|
26
28
|
export class PieListItem extends PieElement implements ListItemProps {
|
|
@@ -43,12 +45,15 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
43
45
|
hasMedia = defaultProps.hasMedia;
|
|
44
46
|
|
|
45
47
|
@property({ type: String })
|
|
46
|
-
@validPropertyValues(componentSelector,
|
|
47
|
-
|
|
48
|
+
@validPropertyValues(componentSelector, interactionTypes, defaultProps.interactionType)
|
|
49
|
+
interactionType = defaultProps.interactionType;
|
|
48
50
|
|
|
49
51
|
@property({ type: Boolean })
|
|
50
52
|
disabled = defaultProps.disabled;
|
|
51
53
|
|
|
54
|
+
@property({ type: Object })
|
|
55
|
+
aria: ListItemProps['aria'];
|
|
56
|
+
|
|
52
57
|
// Whether a disabling ancestor (e.g. `pie-radio-group`) has provided its disabled state.
|
|
53
58
|
// Defaults to false when there is no provider (a standalone item or a static list).
|
|
54
59
|
@consume({ context: parentDisabledContext, subscribe: true })
|
|
@@ -69,17 +74,53 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
69
74
|
|
|
70
75
|
private _abortController!: AbortController;
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
// The last role value we wrote ourselves, so `_applyRole` can tell apart our own managed role
|
|
78
|
+
// from one the consumer set (and therefore should not overwrite).
|
|
79
|
+
private _managedRole: string | null = null;
|
|
80
|
+
|
|
81
|
+
// Captured once (in `_applyLinkAria`): whether the consumer already named/described the slotted
|
|
82
|
+
// link anchor themselves. If so, their value always wins and we never touch it.
|
|
83
|
+
private _linkAriaCaptured = false;
|
|
84
|
+
|
|
85
|
+
private _consumerNamedLink = false;
|
|
86
|
+
|
|
87
|
+
private _consumerDescribedLink = false;
|
|
73
88
|
|
|
89
|
+
// A selection row hosts a slotted control (radio/checkbox/switch) and makes the whole row a
|
|
90
|
+
// toggle target. `link`/`button` are interactive too, but they are single navigation/action
|
|
91
|
+
// targets rather than selection controls, so they are deliberately excluded here.
|
|
74
92
|
private get _isSelectable (): boolean {
|
|
75
|
-
return this.
|
|
93
|
+
return this.interactionType === 'radio' ||
|
|
94
|
+
this.interactionType === 'checkbox' ||
|
|
95
|
+
this.interactionType === 'switch';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private get _isLinkRow (): boolean {
|
|
99
|
+
return this.interactionType === 'link';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private get _isButtonRow (): boolean {
|
|
103
|
+
return this.interactionType === 'button';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A link and a button row share the same visual mechanic: an empty element stretched over the
|
|
107
|
+
// whole row as the interactive target, named by the item from its own text. They differ in who
|
|
108
|
+
// owns that element - a link is a consumer-slotted anchor, a button is one the item renders.
|
|
109
|
+
private get _isOverlayRow (): boolean {
|
|
110
|
+
return this._isLinkRow || this._isButtonRow;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// True when the item lends its text as an interactive element's accessible name/description:
|
|
114
|
+
// selectable rows (radio/checkbox/switch) and overlay rows (link anchor / action button).
|
|
115
|
+
private get _providesAria (): boolean {
|
|
116
|
+
return this._isSelectable || this._isOverlayRow;
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
// radio/checkbox are owned by a selection group, which is why the item becomes `presentation`
|
|
79
120
|
// (so the group owns the controls directly) and the radio is named on its host. A switch has
|
|
80
121
|
// no group, so the item stays a `listitem`.
|
|
81
122
|
private get _ownedByGroup (): boolean {
|
|
82
|
-
return this.
|
|
123
|
+
return this.interactionType === 'radio' || this.interactionType === 'checkbox';
|
|
83
124
|
}
|
|
84
125
|
|
|
85
126
|
// True when the row should be treated as disabled: either its own `disabled` prop is set, or the
|
|
@@ -98,9 +139,6 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
98
139
|
connectedCallback () {
|
|
99
140
|
super.connectedCallback();
|
|
100
141
|
|
|
101
|
-
// Respect a role the consumer set explicitly; otherwise we manage it from `selectionType`.
|
|
102
|
-
this._hasExplicitRole = this.hasAttribute('role');
|
|
103
|
-
|
|
104
142
|
this._abortController = new AbortController();
|
|
105
143
|
this.addEventListener('click', this._handleHostClick, { signal: this._abortController.signal });
|
|
106
144
|
}
|
|
@@ -113,28 +151,33 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
113
151
|
protected updated () {
|
|
114
152
|
this._applyRole();
|
|
115
153
|
this._ariaProvider?.setValue(this._providedAria);
|
|
154
|
+
this._applyLinkAria();
|
|
116
155
|
}
|
|
117
156
|
|
|
118
157
|
/**
|
|
119
|
-
* Sets the item's role from `
|
|
120
|
-
* owns those controls directly), otherwise `listitem` (static items
|
|
121
|
-
* explicitly by the consumer is left untouched
|
|
158
|
+
* Sets the item's role from `interactionType`: `presentation` for radio/checkbox (so the group
|
|
159
|
+
* owns those controls directly), otherwise `listitem` (static items, switches, links and
|
|
160
|
+
* buttons). A role set explicitly by the consumer is left untouched, even if it is added or
|
|
161
|
+
* removed dynamically after the element connects.
|
|
122
162
|
*/
|
|
123
163
|
private _applyRole (): void {
|
|
124
|
-
|
|
125
|
-
|
|
164
|
+
const currentRole = this.getAttribute('role');
|
|
165
|
+
// Only manage the role if it is absent or was last set by us — not a consumer-supplied value.
|
|
166
|
+
if (currentRole !== null && currentRole !== this._managedRole) return;
|
|
167
|
+
|
|
168
|
+
const nextRole = this._ownedByGroup ? 'presentation' : 'listitem';
|
|
169
|
+
this._managedRole = nextRole;
|
|
170
|
+
this.setAttribute('role', nextRole);
|
|
126
171
|
}
|
|
127
172
|
|
|
128
173
|
/**
|
|
129
|
-
* The accessible name and description this item provides to its
|
|
130
|
-
* its text.
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* `aria-label` there would be invalid). Context reaches across the shadow boundary that
|
|
134
|
-
* `aria-labelledby` cannot.
|
|
174
|
+
* The accessible name and description this item provides to its interactive element, stitched
|
|
175
|
+
* from its text. Selection controls consume it via `ariaContext` (each applying it to the
|
|
176
|
+
* element that carries its role); the action button of a `button` row takes it directly as
|
|
177
|
+
* `aria-label`/`aria-description` in the template; a link anchor takes it via `_applyLinkAria`.
|
|
135
178
|
*/
|
|
136
179
|
private get _providedAria (): ContextualAria | undefined {
|
|
137
|
-
if (!this.
|
|
180
|
+
if (!this._providesAria) return undefined;
|
|
138
181
|
|
|
139
182
|
const description = [this.secondaryText, this.metaText].filter(Boolean).join('. ');
|
|
140
183
|
|
|
@@ -155,6 +198,63 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
155
198
|
control.focus();
|
|
156
199
|
};
|
|
157
200
|
|
|
201
|
+
/**
|
|
202
|
+
* On a link row, names the slotted (empty) anchor from the item's own text: the parent owns the
|
|
203
|
+
* child's aria. The anchor is plain light-DOM, so it cannot reference the shadow-DOM text via
|
|
204
|
+
* `aria-labelledby`; and it is not a PIE control that consumes `ariaContext`, so we set the
|
|
205
|
+
* attributes imperatively. The visible text is `aria-hidden` in the template, so nothing is
|
|
206
|
+
* announced twice. (A `button` row names its own rendered element in the template instead.)
|
|
207
|
+
*
|
|
208
|
+
* A consumer-provided name/description always wins: we capture (once) whether the anchor already
|
|
209
|
+
* carries `aria-label`/`aria-labelledby` or `aria-description`/`aria-describedby` and, if so,
|
|
210
|
+
* leave that attribute untouched. Otherwise we keep our generated value in sync with the text.
|
|
211
|
+
*/
|
|
212
|
+
private _applyLinkAria (): void {
|
|
213
|
+
if (!this._isLinkRow) {
|
|
214
|
+
// When transitioning away from `link`, remove any attributes we previously set so they
|
|
215
|
+
// are not left orphaned on the anchor. Reset capture state so a future transition back
|
|
216
|
+
// to `link` re-reads the consumer's attrs from the (possibly different) anchor.
|
|
217
|
+
if (this._linkAriaCaptured) {
|
|
218
|
+
const anchor = this.querySelector('a[slot="link"]');
|
|
219
|
+
if (anchor) {
|
|
220
|
+
if (!this._consumerNamedLink) anchor.removeAttribute('aria-label');
|
|
221
|
+
if (!this._consumerDescribedLink) anchor.removeAttribute('aria-description');
|
|
222
|
+
}
|
|
223
|
+
this._linkAriaCaptured = false;
|
|
224
|
+
this._consumerNamedLink = false;
|
|
225
|
+
this._consumerDescribedLink = false;
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const anchor = this.querySelector('a[slot="link"]');
|
|
231
|
+
if (!anchor) return;
|
|
232
|
+
|
|
233
|
+
if (!this._linkAriaCaptured) {
|
|
234
|
+
this._consumerNamedLink = anchor.hasAttribute('aria-label') || anchor.hasAttribute('aria-labelledby');
|
|
235
|
+
this._consumerDescribedLink = anchor.hasAttribute('aria-description') || anchor.hasAttribute('aria-describedby');
|
|
236
|
+
this._linkAriaCaptured = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const aria = this._providedAria;
|
|
240
|
+
|
|
241
|
+
if (!this._consumerNamedLink) {
|
|
242
|
+
if (aria?.label) {
|
|
243
|
+
anchor.setAttribute('aria-label', aria.label);
|
|
244
|
+
} else {
|
|
245
|
+
anchor.removeAttribute('aria-label');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!this._consumerDescribedLink) {
|
|
250
|
+
if (aria?.description) {
|
|
251
|
+
anchor.setAttribute('aria-description', aria.description);
|
|
252
|
+
} else {
|
|
253
|
+
anchor.removeAttribute('aria-description');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
158
258
|
_renderSecondaryText () {
|
|
159
259
|
const { secondaryText } = this;
|
|
160
260
|
if (secondaryText) {
|
|
@@ -168,12 +268,32 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
168
268
|
_renderTrailingContent () {
|
|
169
269
|
const { metaText } = this;
|
|
170
270
|
if (metaText) {
|
|
171
|
-
return html`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this.
|
|
271
|
+
return html`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this._providesAria ? 'true' : nothing}>${metaText}</span>`;
|
|
172
272
|
}
|
|
173
273
|
|
|
174
274
|
return html`<div class="c-listItem-trailing"><slot name="trailing"></slot></div>`;
|
|
175
275
|
}
|
|
176
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The interactive target for a `button` row: an empty, native `<button>` stretched invisibly
|
|
279
|
+
* over the whole row (see the SCSS). The item renders and owns it - rather than the consumer
|
|
280
|
+
* slotting one - so it can name it from its own text and keep its attributes private (it is just
|
|
281
|
+
* an action box, not a configurable control). Being a real button, activation is native: pointer
|
|
282
|
+
* clicks and keyboard (Enter/Space) both fire a `click` that bubbles out, so consumers listen for
|
|
283
|
+
* `click` on the `pie-list-item`. It is `type="button"`, so it never submits a form.
|
|
284
|
+
*/
|
|
285
|
+
_renderActionButton () {
|
|
286
|
+
const aria = this._providedAria;
|
|
287
|
+
|
|
288
|
+
return html`<button
|
|
289
|
+
class="c-listItem-action"
|
|
290
|
+
type="button"
|
|
291
|
+
?disabled=${this._isDisabled}
|
|
292
|
+
aria-label=${ifDefined(aria?.label)}
|
|
293
|
+
aria-description=${ifDefined(aria?.description)}
|
|
294
|
+
aria-haspopup=${ifDefined(this.aria?.button?.haspopup)}></button>`;
|
|
295
|
+
}
|
|
296
|
+
|
|
177
297
|
render () {
|
|
178
298
|
const { primaryText } = this;
|
|
179
299
|
|
|
@@ -183,16 +303,20 @@ export class PieListItem extends PieElement implements ListItemProps {
|
|
|
183
303
|
'is-bold': this.isBold,
|
|
184
304
|
'has-media': this.hasMedia,
|
|
185
305
|
'is-selectable': this._isSelectable,
|
|
306
|
+
'is-link': this._isLinkRow,
|
|
307
|
+
'is-button': this._isButtonRow,
|
|
186
308
|
'is-disabled': this._isDisabled,
|
|
187
309
|
};
|
|
188
310
|
|
|
189
311
|
return html`
|
|
190
312
|
<div class=${classMap(containerClasses)}>
|
|
313
|
+
${this._isLinkRow ? html`<slot name="link"></slot>` : nothing}
|
|
314
|
+
${this._isButtonRow ? this._renderActionButton() : nothing}
|
|
191
315
|
<div class="c-listItem-leading">
|
|
192
316
|
<slot name="leading"></slot>
|
|
193
317
|
</div>
|
|
194
318
|
|
|
195
|
-
<div class="c-listItem-text" aria-hidden=${this.
|
|
319
|
+
<div class="c-listItem-text" aria-hidden=${this._providesAria ? 'true' : nothing}>
|
|
196
320
|
<span class="c-listItem-primaryText">${primaryText}</span>
|
|
197
321
|
${this._renderSecondaryText()}
|
|
198
322
|
</div>
|
|
@@ -49,13 +49,22 @@
|
|
|
49
49
|
min-height: 48px;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Overlay rows (link/button): the whole item is a single interactive target. The container is
|
|
53
|
+
// positioned so the slotted anchor/button can stretch over it.
|
|
54
|
+
.c-listItem-container.is-link,
|
|
55
|
+
.c-listItem-container.is-button {
|
|
56
|
+
position: relative;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
// Interactive row states. When the item is inside a radio/checkbox group the whole row
|
|
53
60
|
// is a click target (it forwards the click to the slotted control), so it shows hover and
|
|
54
61
|
// active surface tints, matching the control's own interactive states. These are suppressed
|
|
55
62
|
// when the slotted control is disabled (the item reflects this as `is-disabled`). The
|
|
56
63
|
// defaults use the standard PIE interactive overlay tokens; override `--list-item-hover-bg` /
|
|
57
64
|
// `--list-item-active-bg` if design specifies dedicated list tokens.
|
|
58
|
-
.c-listItem-container.is-selectable:not(.is-disabled)
|
|
65
|
+
.c-listItem-container.is-selectable:not(.is-disabled),
|
|
66
|
+
.c-listItem-container.is-link,
|
|
67
|
+
.c-listItem-container.is-button:not(.is-disabled) {
|
|
59
68
|
--list-item-hover-bg: hsl(var(--dt-color-black-h), var(--dt-color-black-s), var(--dt-color-black-l), var(--dt-color-hover-01));
|
|
60
69
|
--list-item-active-bg: hsl(var(--dt-color-black-h), var(--dt-color-black-s), var(--dt-color-black-l), var(--dt-color-active-01));
|
|
61
70
|
|
|
@@ -66,15 +75,73 @@
|
|
|
66
75
|
|
|
67
76
|
cursor: pointer;
|
|
68
77
|
|
|
78
|
+
// Expose the current interaction tint to slotted controls via an inherited custom property (it
|
|
79
|
+
// flows through the `<slot>` into slotted content). This is a generic contract for any control
|
|
80
|
+
// inside an interactive container: a slotted `pie-switch` layers it over its track so the track
|
|
81
|
+
// co-tints with the container; radio/checkbox instead let it show through their transparent
|
|
82
|
+
// fills. Unset when not interacting, so a control outside such a container sees nothing.
|
|
69
83
|
&:hover {
|
|
70
84
|
background-color: var(--list-item-hover-bg);
|
|
85
|
+
|
|
86
|
+
--interactive-container-tint: var(--list-item-hover-bg);
|
|
71
87
|
}
|
|
72
88
|
|
|
89
|
+
// The pressed tint for a pointer press: the container is an ancestor of the pressed element, so
|
|
90
|
+
// it matches `:active`. (A keyboard press is handled by the `:has()` rule below, because keyboard
|
|
91
|
+
// `:active` does not propagate up to the container.)
|
|
73
92
|
&:active {
|
|
74
93
|
background-color: var(--list-item-active-bg);
|
|
94
|
+
|
|
95
|
+
--interactive-container-tint: var(--list-item-active-bg);
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
// Pressed tint for a `button` row's keyboard press. A native `<button>` is `:active` while Space is
|
|
100
|
+
// held, but that state does not propagate to the container, so `&:active` above misses it. The
|
|
101
|
+
// action button is a real shadow-DOM descendant (not slotted), so `:has()` can react to its
|
|
102
|
+
// `:active` directly - no JS needed. A pointer press satisfies both this and `&:active` with the
|
|
103
|
+
// same value, so there is no double up.
|
|
104
|
+
.c-listItem-container.is-button:has(.c-listItem-action:active) {
|
|
105
|
+
background-color: var(--list-item-active-bg);
|
|
106
|
+
|
|
107
|
+
--interactive-container-tint: var(--list-item-active-bg);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Stretch the interactive target over the entire row so the whole block is the click/touch target.
|
|
111
|
+
// For a link it is the consumer's slotted anchor; for a button it is the native <button> the item
|
|
112
|
+
// renders. Both anchor to the container (positioned above).
|
|
113
|
+
slot[name="link"]::slotted(a),
|
|
114
|
+
.c-listItem-action {
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: 0;
|
|
117
|
+
z-index: 1;
|
|
118
|
+
border-radius: inherit; // so the focus ring follows any rounding applied to the row
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The action element of a `button` row is a native <button> the item renders. Strip its chrome so
|
|
122
|
+
// it is invisible (the row provides the styling); it stays a real, focusable button with native
|
|
123
|
+
// pointer/keyboard activation and screen-reader semantics.
|
|
124
|
+
.c-listItem-action {
|
|
125
|
+
padding: 0;
|
|
126
|
+
border: none;
|
|
127
|
+
background: transparent;
|
|
128
|
+
appearance: none;
|
|
129
|
+
font: inherit;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A disabled action button shows the default cursor (it already ignores clicks and is unfocusable
|
|
134
|
+
// natively via the `disabled` attribute).
|
|
135
|
+
.c-listItem-action:disabled {
|
|
136
|
+
cursor: default;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Standard PIE focus ring. The anchor/action is row-sized, so keyboard focus rings the whole item.
|
|
140
|
+
slot[name="link"]::slotted(a:focus-visible),
|
|
141
|
+
.c-listItem-action:focus-visible {
|
|
142
|
+
@include p.focus;
|
|
143
|
+
}
|
|
144
|
+
|
|
78
145
|
// Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only
|
|
79
146
|
// when the primary text stands alone (`:only-child` == no secondary text). With
|
|
80
147
|
// secondary text the default padding is kept so two lines of text sit correctly.
|
|
@@ -117,6 +184,20 @@ slot[name="trailing"]::slotted(*),
|
|
|
117
184
|
--icon-size-override: 24px
|
|
118
185
|
}
|
|
119
186
|
|
|
187
|
+
// Disabled button row: mute text and icons. Icons use `fill: currentColor`, so setting `color`
|
|
188
|
+
// on the host element cascades through the shadow boundary and changes their rendered fill.
|
|
189
|
+
.c-listItem-container.is-button.is-disabled {
|
|
190
|
+
.c-listItem-primaryText,
|
|
191
|
+
.c-listItem-secondaryText,
|
|
192
|
+
.c-listItem-metaText {
|
|
193
|
+
color: var(--dt-color-content-disabled);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.c-listItem-container.is-button.is-disabled ::slotted(.c-pieIcon) {
|
|
198
|
+
color: var(--dt-color-disabled-01);
|
|
199
|
+
}
|
|
200
|
+
|
|
120
201
|
// typography
|
|
121
202
|
|
|
122
203
|
.c-listItem-primaryText {
|