@justeattakeaway/pie-list 0.2.0 → 0.4.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.
@@ -12,7 +12,7 @@ import {
12
12
  type ContextualAria,
13
13
  } from '@justeattakeaway/pie-webc-core';
14
14
  import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement';
15
- import { type ListItemProps, defaultProps, selectionTypes } from './defs';
15
+ import { type ListItemProps, defaultProps, interactionTypes } from './defs';
16
16
  import styles from './list-item.scss?inline';
17
17
 
18
18
  const componentSelector = 'pie-list-item';
@@ -21,6 +21,7 @@ const componentSelector = 'pie-list-item';
21
21
  * @tagname pie-list-item
22
22
  * @slot leading - Content shown at the start of the item (for example an icon, avatar, or a slotted radio/checkbox).
23
23
  * @slot trailing - Content shown at the end of the item. Mutually exclusive with `metaText`.
24
+ * @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
25
  */
25
26
  @safeCustomElement('pie-list-item')
26
27
  export class PieListItem extends PieElement implements ListItemProps {
@@ -42,9 +43,9 @@ export class PieListItem extends PieElement implements ListItemProps {
42
43
  @property({ type: Boolean })
43
44
  hasMedia = defaultProps.hasMedia;
44
45
 
45
- @property({ type: String, attribute: 'selection-type' })
46
- @validPropertyValues(componentSelector, selectionTypes, defaultProps.selectionType)
47
- selectionType = defaultProps.selectionType;
46
+ @property({ type: String })
47
+ @validPropertyValues(componentSelector, interactionTypes, defaultProps.interactionType)
48
+ interactionType = defaultProps.interactionType;
48
49
 
49
50
  @property({ type: Boolean })
50
51
  disabled = defaultProps.disabled;
@@ -69,17 +70,42 @@ export class PieListItem extends PieElement implements ListItemProps {
69
70
 
70
71
  private _abortController!: AbortController;
71
72
 
72
- private _hasExplicitRole = false;
73
+ // The last role value we wrote ourselves, so `_applyRole` can tell apart our own managed role
74
+ // from one the consumer set (and therefore should not overwrite).
75
+ private _managedRole: string | null = null;
76
+
77
+ // Captured once (in `_applyLinkAria`): whether the consumer already named/described the slotted
78
+ // link anchor themselves. If so, their value always wins and we never touch it.
79
+ private _linkAriaCaptured = false;
80
+
81
+ private _consumerNamedLink = false;
73
82
 
83
+ private _consumerDescribedLink = false;
84
+
85
+ // A selection row hosts a slotted control (radio/checkbox/switch) and makes the whole row a
86
+ // toggle target. `link` is interactive too, but it is a navigation target rather than a
87
+ // selection control, so it is deliberately excluded here.
74
88
  private get _isSelectable (): boolean {
75
- return this.selectionType !== 'none';
89
+ return this.interactionType === 'radio' ||
90
+ this.interactionType === 'checkbox' ||
91
+ this.interactionType === 'switch';
92
+ }
93
+
94
+ private get _isLinkRow (): boolean {
95
+ return this.interactionType === 'link';
96
+ }
97
+
98
+ // True when the item lends its text to a slotted control as that control's accessible
99
+ // name/description: selectable rows (radio/checkbox/switch) and link rows (the slotted anchor).
100
+ private get _providesAria (): boolean {
101
+ return this._isSelectable || this._isLinkRow;
76
102
  }
77
103
 
78
104
  // radio/checkbox are owned by a selection group, which is why the item becomes `presentation`
79
105
  // (so the group owns the controls directly) and the radio is named on its host. A switch has
80
106
  // no group, so the item stays a `listitem`.
81
107
  private get _ownedByGroup (): boolean {
82
- return this.selectionType === 'radio' || this.selectionType === 'checkbox';
108
+ return this.interactionType === 'radio' || this.interactionType === 'checkbox';
83
109
  }
84
110
 
85
111
  // True when the row should be treated as disabled: either its own `disabled` prop is set, or the
@@ -98,9 +124,6 @@ export class PieListItem extends PieElement implements ListItemProps {
98
124
  connectedCallback () {
99
125
  super.connectedCallback();
100
126
 
101
- // Respect a role the consumer set explicitly; otherwise we manage it from `selectionType`.
102
- this._hasExplicitRole = this.hasAttribute('role');
103
-
104
127
  this._abortController = new AbortController();
105
128
  this.addEventListener('click', this._handleHostClick, { signal: this._abortController.signal });
106
129
  }
@@ -113,16 +136,23 @@ export class PieListItem extends PieElement implements ListItemProps {
113
136
  protected updated () {
114
137
  this._applyRole();
115
138
  this._ariaProvider?.setValue(this._providedAria);
139
+ this._applyLinkAria();
116
140
  }
117
141
 
118
142
  /**
119
- * Sets the item's role from `selectionType`: `presentation` for radio/checkbox (so the group
120
- * owns those controls directly), otherwise `listitem` (static items and switches). A role set
121
- * explicitly by the consumer is left untouched.
143
+ * Sets the item's role from `interactionType`: `presentation` for radio/checkbox (so the group
144
+ * owns those controls directly), otherwise `listitem` (static items, switches and links). A role
145
+ * set explicitly by the consumer is left untouched, even if it is added or removed dynamically
146
+ * after the element connects.
122
147
  */
123
148
  private _applyRole (): void {
124
- if (this._hasExplicitRole) return;
125
- this.setAttribute('role', this._ownedByGroup ? 'presentation' : 'listitem');
149
+ const currentRole = this.getAttribute('role');
150
+ // Only manage the role if it is absent or was last set by us — not a consumer-supplied value.
151
+ if (currentRole !== null && currentRole !== this._managedRole) return;
152
+
153
+ const nextRole = this._ownedByGroup ? 'presentation' : 'listitem';
154
+ this._managedRole = nextRole;
155
+ this.setAttribute('role', nextRole);
126
156
  }
127
157
 
128
158
  /**
@@ -134,7 +164,7 @@ export class PieListItem extends PieElement implements ListItemProps {
134
164
  * `aria-labelledby` cannot.
135
165
  */
136
166
  private get _providedAria (): ContextualAria | undefined {
137
- if (!this._isSelectable) return undefined;
167
+ if (!this._providesAria) return undefined;
138
168
 
139
169
  const description = [this.secondaryText, this.metaText].filter(Boolean).join('. ');
140
170
 
@@ -155,6 +185,63 @@ export class PieListItem extends PieElement implements ListItemProps {
155
185
  control.focus();
156
186
  };
157
187
 
188
+ /**
189
+ * When the item is a link, names the slotted (empty) anchor from the item's own text: the parent
190
+ * owns the child's aria. The anchor is plain light-DOM, so it cannot reference the shadow-DOM text
191
+ * via `aria-labelledby`; and it is not a PIE control that consumes `ariaContext`, so we set the
192
+ * attributes imperatively. The visible text is `aria-hidden` in the template, so nothing is
193
+ * announced twice.
194
+ *
195
+ * A consumer-provided name/description always wins: we capture (once) whether the anchor already
196
+ * carries `aria-label`/`aria-labelledby` or `aria-description`/`aria-describedby` and, if so,
197
+ * leave that attribute untouched. Otherwise we keep our generated value in sync with the text.
198
+ */
199
+ private _applyLinkAria (): void {
200
+ if (!this._isLinkRow) {
201
+ // When transitioning away from `link`, remove any attributes we previously set so they
202
+ // are not left orphaned on the anchor. Reset capture state so a future transition back
203
+ // to `link` re-reads the consumer's attrs from the (possibly different) anchor.
204
+ if (this._linkAriaCaptured) {
205
+ const anchor = this.querySelector('a[slot="link"]');
206
+ if (anchor) {
207
+ if (!this._consumerNamedLink) anchor.removeAttribute('aria-label');
208
+ if (!this._consumerDescribedLink) anchor.removeAttribute('aria-description');
209
+ }
210
+ this._linkAriaCaptured = false;
211
+ this._consumerNamedLink = false;
212
+ this._consumerDescribedLink = false;
213
+ }
214
+ return;
215
+ }
216
+
217
+ const anchor = this.querySelector('a[slot="link"]');
218
+ if (!anchor) return;
219
+
220
+ if (!this._linkAriaCaptured) {
221
+ this._consumerNamedLink = anchor.hasAttribute('aria-label') || anchor.hasAttribute('aria-labelledby');
222
+ this._consumerDescribedLink = anchor.hasAttribute('aria-description') || anchor.hasAttribute('aria-describedby');
223
+ this._linkAriaCaptured = true;
224
+ }
225
+
226
+ const aria = this._providedAria;
227
+
228
+ if (!this._consumerNamedLink) {
229
+ if (aria?.label) {
230
+ anchor.setAttribute('aria-label', aria.label);
231
+ } else {
232
+ anchor.removeAttribute('aria-label');
233
+ }
234
+ }
235
+
236
+ if (!this._consumerDescribedLink) {
237
+ if (aria?.description) {
238
+ anchor.setAttribute('aria-description', aria.description);
239
+ } else {
240
+ anchor.removeAttribute('aria-description');
241
+ }
242
+ }
243
+ }
244
+
158
245
  _renderSecondaryText () {
159
246
  const { secondaryText } = this;
160
247
  if (secondaryText) {
@@ -168,7 +255,7 @@ export class PieListItem extends PieElement implements ListItemProps {
168
255
  _renderTrailingContent () {
169
256
  const { metaText } = this;
170
257
  if (metaText) {
171
- return html`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this._isSelectable ? 'true' : nothing}>${metaText}</span>`;
258
+ return html`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this._providesAria ? 'true' : nothing}>${metaText}</span>`;
172
259
  }
173
260
 
174
261
  return html`<div class="c-listItem-trailing"><slot name="trailing"></slot></div>`;
@@ -183,16 +270,18 @@ export class PieListItem extends PieElement implements ListItemProps {
183
270
  'is-bold': this.isBold,
184
271
  'has-media': this.hasMedia,
185
272
  'is-selectable': this._isSelectable,
273
+ 'is-link': this._isLinkRow,
186
274
  'is-disabled': this._isDisabled,
187
275
  };
188
276
 
189
277
  return html`
190
278
  <div class=${classMap(containerClasses)}>
279
+ ${this._isLinkRow ? html`<slot name="link"></slot>` : nothing}
191
280
  <div class="c-listItem-leading">
192
281
  <slot name="leading"></slot>
193
282
  </div>
194
283
 
195
- <div class="c-listItem-text" aria-hidden=${this._isSelectable ? 'true' : nothing}>
284
+ <div class="c-listItem-text" aria-hidden=${this._providesAria ? 'true' : nothing}>
196
285
  <span class="c-listItem-primaryText">${primaryText}</span>
197
286
  ${this._renderSecondaryText()}
198
287
  </div>
@@ -49,13 +49,20 @@
49
49
  min-height: 48px;
50
50
  }
51
51
 
52
+ // Link rows: the whole item is a single navigation target. The container is positioned so the
53
+ // slotted anchor can stretch over it.
54
+ .c-listItem-container.is-link {
55
+ position: relative;
56
+ }
57
+
52
58
  // Interactive row states. When the item is inside a radio/checkbox group the whole row
53
59
  // is a click target (it forwards the click to the slotted control), so it shows hover and
54
60
  // active surface tints, matching the control's own interactive states. These are suppressed
55
61
  // when the slotted control is disabled (the item reflects this as `is-disabled`). The
56
62
  // defaults use the standard PIE interactive overlay tokens; override `--list-item-hover-bg` /
57
63
  // `--list-item-active-bg` if design specifies dedicated list tokens.
58
- .c-listItem-container.is-selectable:not(.is-disabled) {
64
+ .c-listItem-container.is-selectable:not(.is-disabled),
65
+ .c-listItem-container.is-link {
59
66
  --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
67
  --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
68
 
@@ -66,15 +73,38 @@
66
73
 
67
74
  cursor: pointer;
68
75
 
76
+ // Expose the current interaction tint to slotted controls via an inherited custom property (it
77
+ // flows through the `<slot>` into slotted content). This is a generic contract for any control
78
+ // inside an interactive container: a slotted `pie-switch` layers it over its track so the track
79
+ // co-tints with the container; radio/checkbox instead let it show through their transparent
80
+ // fills. Unset when not interacting, so a control outside such a container sees nothing.
69
81
  &:hover {
70
82
  background-color: var(--list-item-hover-bg);
83
+
84
+ --interactive-container-tint: var(--list-item-hover-bg);
71
85
  }
72
86
 
73
87
  &:active {
74
88
  background-color: var(--list-item-active-bg);
89
+
90
+ --interactive-container-tint: var(--list-item-active-bg);
75
91
  }
76
92
  }
77
93
 
94
+ // Stretch the empty slotted anchor over the entire row so the whole block is the link's
95
+ // click/touch target. It anchors to the container (positioned above).
96
+ slot[name="link"]::slotted(a) {
97
+ position: absolute;
98
+ inset: 0;
99
+ z-index: 1;
100
+ border-radius: inherit; // so the focus ring follows any rounding applied to the row
101
+ }
102
+
103
+ // Standard PIE focus ring. The anchor is row-sized, so keyboard focus rings the whole item.
104
+ slot[name="link"]::slotted(a:focus-visible) {
105
+ @include p.focus;
106
+ }
107
+
78
108
  // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only
79
109
  // when the primary text stands alone (`:only-child` == no secondary text). With
80
110
  // secondary text the default padding is kept so two lines of text sit correctly.
@@ -1,265 +0,0 @@
1
- import { unsafeCSS as x, isServer as f, html as p, nothing as m } from "lit";
2
- import { property as c, state as y } from "lit/decorators.js";
3
- import { classMap as k } from "lit/directives/class-map.js";
4
- import { validPropertyValues as _, safeCustomElement as T, ariaContext as I, parentDisabledContext as C } from "@justeattakeaway/pie-webc-core";
5
- import { P as w } from "./PieElement-B3g_8vKy.js";
6
- /**
7
- * @license
8
- * Copyright 2021 Google LLC
9
- * SPDX-License-Identifier: BSD-3-Clause
10
- */
11
- let g = class extends Event {
12
- constructor(t, e, o, s) {
13
- super("context-request", { bubbles: !0, composed: !0 }), this.context = t, this.contextTarget = e, this.callback = o, this.subscribe = s ?? !1;
14
- }
15
- };
16
- /**
17
- * @license
18
- * Copyright 2021 Google LLC
19
- * SPDX-License-Identifier: BSD-3-Clause
20
- */
21
- let v = class {
22
- constructor(t, e, o, s) {
23
- if (this.subscribe = !1, this.provided = !1, this.value = void 0, this.t = (r, a) => {
24
- this.unsubscribe && (this.unsubscribe !== a && (this.provided = !1, this.unsubscribe()), this.subscribe || this.unsubscribe()), this.value = r, this.host.requestUpdate(), this.provided && !this.subscribe || (this.provided = !0, this.callback && this.callback(r, a)), this.unsubscribe = a;
25
- }, this.host = t, e.context !== void 0) {
26
- const r = e;
27
- this.context = r.context, this.callback = r.callback, this.subscribe = r.subscribe ?? !1;
28
- } else this.context = e, this.callback = o, this.subscribe = s ?? !1;
29
- this.host.addController(this);
30
- }
31
- hostConnected() {
32
- this.dispatchRequest();
33
- }
34
- hostDisconnected() {
35
- this.unsubscribe && (this.unsubscribe(), this.unsubscribe = void 0);
36
- }
37
- dispatchRequest() {
38
- this.host.dispatchEvent(new g(this.context, this.host, this.t, this.subscribe));
39
- }
40
- };
41
- /**
42
- * @license
43
- * Copyright 2021 Google LLC
44
- * SPDX-License-Identifier: BSD-3-Clause
45
- */
46
- class P {
47
- get value() {
48
- return this.o;
49
- }
50
- set value(t) {
51
- this.setValue(t);
52
- }
53
- setValue(t, e = !1) {
54
- const o = e || !Object.is(t, this.o);
55
- this.o = t, o && this.updateObservers();
56
- }
57
- constructor(t) {
58
- this.subscriptions = /* @__PURE__ */ new Map(), this.updateObservers = () => {
59
- for (const [e, { disposer: o }] of this.subscriptions) e(this.o, o);
60
- }, t !== void 0 && (this.value = t);
61
- }
62
- addCallback(t, e, o) {
63
- if (!o) return void t(this.value);
64
- this.subscriptions.has(t) || this.subscriptions.set(t, { disposer: () => {
65
- this.subscriptions.delete(t);
66
- }, consumerHost: e });
67
- const { disposer: s } = this.subscriptions.get(t);
68
- t(this.value, s);
69
- }
70
- clearCallbacks() {
71
- this.subscriptions.clear();
72
- }
73
- }
74
- /**
75
- * @license
76
- * Copyright 2021 Google LLC
77
- * SPDX-License-Identifier: BSD-3-Clause
78
- */
79
- class S extends Event {
80
- constructor(t, e) {
81
- super("context-provider", { bubbles: !0, composed: !0 }), this.context = t, this.contextTarget = e;
82
- }
83
- }
84
- class E extends P {
85
- constructor(t, e, o) {
86
- var s, r;
87
- super(e.context !== void 0 ? e.initialValue : o), this.onContextRequest = (a) => {
88
- if (a.context !== this.context) return;
89
- const h = a.contextTarget ?? a.composedPath()[0];
90
- h !== this.host && (a.stopPropagation(), this.addCallback(a.callback, h, a.subscribe));
91
- }, this.onProviderRequest = (a) => {
92
- if (a.context !== this.context || (a.contextTarget ?? a.composedPath()[0]) === this.host) return;
93
- const h = /* @__PURE__ */ new Set();
94
- for (const [b, { consumerHost: u }] of this.subscriptions) h.has(b) || (h.add(b), u.dispatchEvent(new g(this.context, u, b, !0)));
95
- a.stopPropagation();
96
- }, this.host = t, e.context !== void 0 ? this.context = e.context : this.context = e, this.attachListeners(), (r = (s = this.host).addController) == null || r.call(s, this);
97
- }
98
- attachListeners() {
99
- this.host.addEventListener("context-request", this.onContextRequest), this.host.addEventListener("context-provider", this.onProviderRequest);
100
- }
101
- hostConnected() {
102
- this.host.dispatchEvent(new S(this.context, this.host));
103
- }
104
- }
105
- /**
106
- * @license
107
- * Copyright 2022 Google LLC
108
- * SPDX-License-Identifier: BSD-3-Clause
109
- */
110
- function B({ context: i, subscribe: t }) {
111
- return (e, o) => {
112
- typeof o == "object" ? o.addInitializer((function() {
113
- new v(this, { context: i, callback: (s) => {
114
- e.set.call(this, s);
115
- }, subscribe: t });
116
- })) : e.constructor.addInitializer(((s) => {
117
- new v(s, { context: i, callback: (r) => {
118
- s[o] = r;
119
- }, subscribe: t });
120
- }));
121
- };
122
- }
123
- const z = ["none", "radio", "checkbox", "switch"], d = {
124
- isCompact: !1,
125
- isBold: !1,
126
- hasMedia: !1,
127
- selectionType: "none",
128
- disabled: !1
129
- }, R = "*,*:after,*:before{box-sizing:inherit}:host{--list-item-inline-padding: var(--dt-spacing-d);--list-item-alignment: flex-start;--list-item-border: none;display:block}.c-listItem-container{--list-item-block-padding: var(--dt-spacing-d);--list-item-block-end-padding: calc(var(--list-item-block-padding) - var(--list-item-size-modifier));padding-inline-start:var(--list-item-inline-padding);padding-inline-end:var(--list-item-inline-padding);padding-block-start:var(--list-item-block-padding);padding-block-end:var(--list-item-block-end-padding, var(--list-item-block-padding));display:flex;align-items:var(--list-item-alignment);gap:var(--dt-spacing-c);min-height:56px;border-bottom:var(--list-item-border)}.c-listItem-container:has(.c-listItem-secondaryText){align-items:var(--list-item-alignment);min-height:76px}.c-listItem-container.is-compact{--list-item-block-padding: var(--dt-spacing-c);--list-item-alignment: center;align-items:var(--list-item-alignment);min-height:48px}.c-listItem-container.is-selectable:not(.is-disabled){--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));--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))}@supports (background-color: color-mix(in srgb,black,white)){.c-listItem-container.is-selectable:not(.is-disabled){--list-item-hover-bg: color-mix(in srgb, var(--dt-color-hover-01-bg) var(--dt-color-hover-01), transparent);--list-item-active-bg: color-mix(in srgb, var(--dt-color-active-01-bg) var(--dt-color-active-01), transparent)}}.c-listItem-container.is-selectable:not(.is-disabled){cursor:pointer}.c-listItem-container.is-selectable:not(.is-disabled):hover{background-color:var(--list-item-hover-bg)}.c-listItem-container.is-selectable:not(.is-disabled):active{background-color:var(--list-item-active-bg)}.c-listItem-container.has-media:has(.c-listItem-primaryText:only-child){--list-item-block-padding: var(--dt-spacing-b)}.c-listItem-text{flex:1}slot[name=trailing]::slotted(*),.c-listItem-metaText{padding-inline-start:var(--dt-spacing-a)}.c-listItem-leading,slot[name=leading],.c-listItem-trailing,slot[name=trailing]{display:contents}.c-listItem-text>*{display:block}.c-listItem-trailing{margin-inline-start:auto}.c-listItem-metaText{display:block;max-width:33.3333333333%}::slotted(.c-pieIcon){--icon-size-override: 24px }.c-listItem-primaryText{font-family:var(--dt-font-body-l-family);font-weight:var(--dt-font-body-l-weight);font-size:calc(var(--dt-font-body-l-size) * 1px);line-height:calc(var(--dt-font-body-l-line-height) * 1px);color:var(--dt-color-content-default)}.c-listItem-container.is-bold .c-listItem-primaryText{font-family:var(--dt-font-body-strong-l-family);font-weight:var(--dt-font-body-strong-l-weight);font-size:calc(var(--dt-font-body-strong-l-size) * 1px);line-height:calc(var(--dt-font-body-strong-l-line-height) * 1px)}.c-listItem-secondaryText,.c-listItem-metaText{font-family:var(--dt-font-body-s-family);font-weight:var(--dt-font-body-s-weight);font-size:calc(var(--dt-font-body-s-size) * 1px);line-height:calc(var(--dt-font-body-s-line-height) * 1px);color:var(--dt-color-content-subdued)}";
130
- var q = Object.defineProperty, D = Object.getOwnPropertyDescriptor, l = (i, t, e, o) => {
131
- for (var s = o > 1 ? void 0 : o ? D(t, e) : t, r = i.length - 1, a; r >= 0; r--)
132
- (a = i[r]) && (s = (o ? a(t, e, s) : a(s)) || s);
133
- return o && s && q(t, e, s), s;
134
- };
135
- const $ = "pie-list-item";
136
- let n = class extends w {
137
- constructor() {
138
- super(...arguments), this.isCompact = d.isCompact, this.isBold = d.isBold, this.hasMedia = d.hasMedia, this.selectionType = d.selectionType, this.disabled = d.disabled, this._parentDisabled = !1, this._ariaProvider = f ? void 0 : new E(this, { context: I }), this._hasExplicitRole = !1, this._handleHostClick = (i) => {
139
- if (!this._isSelectable || this._isDisabled) return;
140
- const t = this._control;
141
- !t || i.composedPath().includes(t) || (t.click(), t.focus());
142
- };
143
- }
144
- get _isSelectable() {
145
- return this.selectionType !== "none";
146
- }
147
- // radio/checkbox are owned by a selection group, which is why the item becomes `presentation`
148
- // (so the group owns the controls directly) and the radio is named on its host. A switch has
149
- // no group, so the item stays a `listitem`.
150
- get _ownedByGroup() {
151
- return this.selectionType === "radio" || this.selectionType === "checkbox";
152
- }
153
- // True when the row should be treated as disabled: either its own `disabled` prop is set, or the
154
- // containing group is disabled (provided via context). Declarative and reactive, so the styling
155
- // and row-click guard react without observing the slotted control.
156
- get _isDisabled() {
157
- return this.disabled || this._parentDisabled;
158
- }
159
- // The interactive control (radio/checkbox/switch) slotted into this item, if any. Used only by
160
- // the client-side row-click handler.
161
- get _control() {
162
- return this.querySelector("pie-radio, pie-checkbox, pie-switch");
163
- }
164
- connectedCallback() {
165
- super.connectedCallback(), this._hasExplicitRole = this.hasAttribute("role"), this._abortController = new AbortController(), this.addEventListener("click", this._handleHostClick, { signal: this._abortController.signal });
166
- }
167
- disconnectedCallback() {
168
- super.disconnectedCallback(), this._abortController.abort();
169
- }
170
- updated() {
171
- var i;
172
- this._applyRole(), (i = this._ariaProvider) == null || i.setValue(this._providedAria);
173
- }
174
- /**
175
- * Sets the item's role from `selectionType`: `presentation` for radio/checkbox (so the group
176
- * owns those controls directly), otherwise `listitem` (static items and switches). A role set
177
- * explicitly by the consumer is left untouched.
178
- */
179
- _applyRole() {
180
- this._hasExplicitRole || this.setAttribute("role", this._ownedByGroup ? "presentation" : "listitem");
181
- }
182
- /**
183
- * The accessible name and description this item provides to its slotted control, stitched from
184
- * its text. Only provided in a selectable list. Each control consumes this via `ariaContext`
185
- * and decides how to apply it: `pie-radio` names its host (which carries `role="radio"`),
186
- * while `pie-checkbox` / `pie-switch` name their internal input (their host is role-less, so
187
- * `aria-label` there would be invalid). Context reaches across the shadow boundary that
188
- * `aria-labelledby` cannot.
189
- */
190
- get _providedAria() {
191
- if (!this._isSelectable) return;
192
- const i = [this.secondaryText, this.metaText].filter(Boolean).join(". ");
193
- return { label: this.primaryText, description: i || void 0 };
194
- }
195
- _renderSecondaryText() {
196
- const { secondaryText: i } = this;
197
- return i ? p`<span class="c-listItem-secondaryText">${i}</span>` : m;
198
- }
199
- // metaText is another form of trailing content. The component can only ever display either slotted trailing content or the metaText string
200
- _renderTrailingContent() {
201
- const { metaText: i } = this;
202
- return i ? p`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this._isSelectable ? "true" : m}>${i}</span>` : p`<div class="c-listItem-trailing"><slot name="trailing"></slot></div>`;
203
- }
204
- render() {
205
- const { primaryText: i } = this, t = {
206
- "c-listItem-container": !0,
207
- "is-compact": this.isCompact,
208
- "is-bold": this.isBold,
209
- "has-media": this.hasMedia,
210
- "is-selectable": this._isSelectable,
211
- "is-disabled": this._isDisabled
212
- };
213
- return p`
214
- <div class=${k(t)}>
215
- <div class="c-listItem-leading">
216
- <slot name="leading"></slot>
217
- </div>
218
-
219
- <div class="c-listItem-text" aria-hidden=${this._isSelectable ? "true" : m}>
220
- <span class="c-listItem-primaryText">${i}</span>
221
- ${this._renderSecondaryText()}
222
- </div>
223
-
224
- ${this._renderTrailingContent()}
225
- </div>`;
226
- }
227
- };
228
- n.styles = x(R);
229
- l([
230
- c({ type: String })
231
- ], n.prototype, "primaryText", 2);
232
- l([
233
- c({ type: String })
234
- ], n.prototype, "secondaryText", 2);
235
- l([
236
- c({ type: String })
237
- ], n.prototype, "metaText", 2);
238
- l([
239
- c({ type: Boolean })
240
- ], n.prototype, "isCompact", 2);
241
- l([
242
- c({ type: Boolean })
243
- ], n.prototype, "isBold", 2);
244
- l([
245
- c({ type: Boolean })
246
- ], n.prototype, "hasMedia", 2);
247
- l([
248
- c({ type: String, attribute: "selection-type" }),
249
- _($, z, d.selectionType)
250
- ], n.prototype, "selectionType", 2);
251
- l([
252
- c({ type: Boolean })
253
- ], n.prototype, "disabled", 2);
254
- l([
255
- B({ context: C, subscribe: !0 }),
256
- y()
257
- ], n.prototype, "_parentDisabled", 2);
258
- n = l([
259
- T("pie-list-item")
260
- ], n);
261
- export {
262
- n as P,
263
- d,
264
- z as s
265
- };