@justeattakeaway/pie-list 0.1.1 → 0.2.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 CHANGED
@@ -55,6 +55,8 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc
55
55
  | `isCompact` | `true`, `false` | Decreases the item height to save vertical space. See the [rules](#usage-notes-and-rules) below. | `false` |
56
56
  | `isBold` | `true`, `false` | Sets the primary text to a bold font-weight. | `false` |
57
57
  | `hasMedia` | `true`, `false` | **Required whenever you slot a media element (e.g. `pie-thumbnail`) into the item.** Reduces the block padding so single-line media sits correctly (this padding adjustment has no effect when `secondaryText` is set, but you should still set `hasMedia`). | `false` |
58
+ | `selectionType` | `"none"`, `"radio"`, `"checkbox"`, `"switch"` | Declares that the item hosts an interactive control in its `leading`/`trailing` slot, making the **whole row** a selectable target. Set it on each selectable row. Inside a `pie-radio-group`, the group lays the rows out as a divided list automatically. See [Selectable lists](#selectable-lists). | `"none"` |
59
+ | `disabled` | `true`, `false` | Marks the row as disabled: it takes the disabled styling and stops forwarding row clicks to its control. Set it alongside the slotted control's own `disabled` (the control still governs its own interactivity). No visible effect on a non-selectable (static) item. | `false` |
58
60
 
59
61
  ### Slots
60
62
 
@@ -68,6 +70,9 @@ Slots are provided by `pie-list-item`.
68
70
  The permitted slotted elements are: a PIE WEBC icon, `pie-tag`, `pie-thumbnail`, `pie-avatar`*, `pie-switch`, and native HTML radio/checkbox inputs.
69
71
  Some slotted content is designed with specific properties being used. So please read the entire readme to understand correct slot usage.
70
72
 
73
+ > [!NOTE]
74
+ > `pie-list` is a **static** container: it has no selection state or keyboard behaviour. For a **selectable** list, do not slot the controls into `pie-list`. Place `pie-list-item`s inside a [`pie-radio-group`](https://webc.pie.design/?path=/docs/components-radio-group--overview) (single-select) or a [`pie-checkbox-group`](https://webc.pie.design/?path=/docs/components-checkbox-group--overview) (multi-select) instead. See [Selectable lists](#selectable-lists) below.
75
+
71
76
  > Slotted PIE icons are always sized by `pie-list-item` (24px). Consumers cannot override this size.
72
77
 
73
78
  > \* `pie-avatar` is a permitted slot element but is not covered by usage examples here yet, as it is not ready for use in lists.
@@ -241,6 +246,40 @@ import '@justeattakeaway/pie-webc/components/thumbnail.js';
241
246
  </pie-list>
242
247
  ```
243
248
 
249
+ ### Selectable lists
250
+
251
+ `pie-list` itself is a static container with no selection or keyboard behaviour. To build a **selectable** list, do **not** put the controls in `pie-list`. Instead, place `pie-list-item`s inside a [`pie-radio-group`](https://webc.pie.design/?path=/docs/components-radio-group--overview) (single-select) with `selection-type="radio"`, and slot the control into each item's `leading` (or `trailing`) slot. The group lays the rows out as a divided list automatically.
252
+
253
+ `selection-type` on each **item** makes that row selectable:
254
+
255
+ - takes the correct **role** — `presentation` for `radio`/`checkbox` (so the group owns the controls directly), `listitem` for `switch` and `none`;
256
+ - provides its `primaryText`, `secondaryText` and `metaText` as the slotted control's **accessible name and description** (and `aria-hidden`s the now-duplicated visible text);
257
+ - **forwards a click** anywhere on the row to the control, so the whole row is a hit target;
258
+ - shows **hover and active** states on the row (suppressed when the item's `disabled` is set, or the group is disabled).
259
+
260
+ Provide the label through the item's `primaryText` — not as the control's own content. The group still owns the group-level semantics (its own role, shared `name`, selection coordination and group-disable).
261
+
262
+ Single-select (radios):
263
+
264
+ ```js
265
+ import '@justeattakeaway/pie-webc/components/radio-group.js';
266
+ import '@justeattakeaway/pie-webc/components/radio.js';
267
+ import '@justeattakeaway/pie-webc/components/list-item.js';
268
+ ```
269
+
270
+ ```html
271
+ <!-- `selection-type="radio"` makes each row selectable and the group lays them out as a divided
272
+ list. `value` on the group selects the matching radio (here, Express). -->
273
+ <pie-radio-group name="delivery" value="express">
274
+ <pie-list-item selection-type="radio" primaryText="Standard delivery" secondaryText="3 to 5 working days" metaText="Free">
275
+ <pie-radio slot="leading" value="standard"></pie-radio>
276
+ </pie-list-item>
277
+ <pie-list-item selection-type="radio" primaryText="Express delivery" secondaryText="Next working day" metaText="£4.99">
278
+ <pie-radio slot="leading" value="express"></pie-radio>
279
+ </pie-list-item>
280
+ </pie-radio-group>
281
+ ```
282
+
244
283
  **For Native JS Applications, Vue, Angular, Svelte etc.:**
245
284
 
246
285
  ```js
@@ -272,6 +311,7 @@ To keep lists consistent and correct, follow these rules:
272
311
 
273
312
  - **Always give `pie-list` an accessible name** with `aria-label` or `aria-labelledby` (use `aria-labelledby` when a visible heading exists). This is required for screen reader users to understand the list. See [Accessibility](#accessibility).
274
313
  - **Provide `primaryText`** on every `pie-list-item`; it is the item's main line of content.
314
+ - **For selectable lists, use `pie-list-item` inside `pie-radio-group` or `pie-checkbox-group`, not `pie-list`.** `pie-list` is a static container with no selection or keyboard behaviour. See [Selectable lists](#selectable-lists).
275
315
  - **`metaText` and the `trailing` slot are mutually exclusive.** If `metaText` is set, any `trailing` slot content is ignored. Choose one.
276
316
  - **Slotted `pie-thumbnail` must use `size="40"`.** This is the only size that fits the list-item layout correctly.
277
317
  - **Always set `hasMedia` when slotting media** (`pie-thumbnail`, and `pie-avatar` in future), whether or not the item has `secondaryText`. This guarantees the item has the correct block padding.
@@ -66,16 +66,32 @@
66
66
  "kind": "javascript-module",
67
67
  "path": "src/pie-list-item/defs.js",
68
68
  "declarations": [
69
+ {
70
+ "kind": "variable",
71
+ "name": "selectionTypes",
72
+ "type": {
73
+ "text": "['none', 'radio', 'checkbox', 'switch']"
74
+ },
75
+ "default": "['none', 'radio', 'checkbox', 'switch']"
76
+ },
69
77
  {
70
78
  "kind": "variable",
71
79
  "name": "defaultProps",
72
80
  "type": {
73
81
  "text": "DefaultProps"
74
82
  },
75
- "default": "{ isCompact: false, isBold: false, hasMedia: false, }"
83
+ "default": "{ isCompact: false, isBold: false, hasMedia: false, selectionType: 'none', disabled: false, }"
76
84
  }
77
85
  ],
78
86
  "exports": [
87
+ {
88
+ "kind": "js",
89
+ "name": "selectionTypes",
90
+ "declaration": {
91
+ "name": "selectionTypes",
92
+ "module": "src/pie-list-item/defs.js"
93
+ }
94
+ },
79
95
  {
80
96
  "kind": "js",
81
97
  "name": "defaultProps",
@@ -94,6 +110,16 @@
94
110
  "kind": "class",
95
111
  "description": "",
96
112
  "name": "PieListItem",
113
+ "slots": [
114
+ {
115
+ "description": "Content shown at the start of the item (for example an icon, avatar, or a slotted radio/checkbox).",
116
+ "name": "leading"
117
+ },
118
+ {
119
+ "description": "Content shown at the end of the item. Mutually exclusive with `metaText`.",
120
+ "name": "trailing"
121
+ }
122
+ ],
97
123
  "members": [
98
124
  {
99
125
  "kind": "field",
@@ -134,6 +160,110 @@
134
160
  "name": "hasMedia",
135
161
  "attribute": "hasMedia"
136
162
  },
163
+ {
164
+ "kind": "field",
165
+ "name": "selectionType",
166
+ "attribute": "selection-type"
167
+ },
168
+ {
169
+ "kind": "field",
170
+ "name": "disabled",
171
+ "attribute": "disabled"
172
+ },
173
+ {
174
+ "kind": "field",
175
+ "name": "_parentDisabled",
176
+ "type": {
177
+ "text": "boolean"
178
+ },
179
+ "privacy": "private",
180
+ "default": "false"
181
+ },
182
+ {
183
+ "kind": "field",
184
+ "name": "_ariaProvider",
185
+ "privacy": "private"
186
+ },
187
+ {
188
+ "kind": "field",
189
+ "name": "_abortController",
190
+ "type": {
191
+ "text": "AbortController"
192
+ },
193
+ "privacy": "private"
194
+ },
195
+ {
196
+ "kind": "field",
197
+ "name": "_hasExplicitRole",
198
+ "type": {
199
+ "text": "boolean"
200
+ },
201
+ "privacy": "private",
202
+ "default": "false"
203
+ },
204
+ {
205
+ "kind": "field",
206
+ "name": "_isSelectable",
207
+ "type": {
208
+ "text": "boolean"
209
+ },
210
+ "privacy": "private",
211
+ "readonly": true
212
+ },
213
+ {
214
+ "kind": "field",
215
+ "name": "_ownedByGroup",
216
+ "type": {
217
+ "text": "boolean"
218
+ },
219
+ "privacy": "private",
220
+ "readonly": true
221
+ },
222
+ {
223
+ "kind": "field",
224
+ "name": "_isDisabled",
225
+ "type": {
226
+ "text": "boolean"
227
+ },
228
+ "privacy": "private",
229
+ "readonly": true
230
+ },
231
+ {
232
+ "kind": "field",
233
+ "name": "_control",
234
+ "type": {
235
+ "text": "HTMLElement | null"
236
+ },
237
+ "privacy": "private",
238
+ "readonly": true
239
+ },
240
+ {
241
+ "kind": "method",
242
+ "name": "_applyRole",
243
+ "privacy": "private",
244
+ "return": {
245
+ "type": {
246
+ "text": "void"
247
+ }
248
+ },
249
+ "description": "Sets the item's role from `selectionType`: `presentation` for radio/checkbox (so the group\nowns those controls directly), otherwise `listitem` (static items and switches). A role set\nexplicitly by the consumer is left untouched."
250
+ },
251
+ {
252
+ "kind": "field",
253
+ "name": "_providedAria",
254
+ "type": {
255
+ "text": "ContextualAria | undefined"
256
+ },
257
+ "privacy": "private",
258
+ "description": "The accessible name and description this item provides to its slotted control, stitched from\nits text. Only provided in a selectable list. Each control consumes this via `ariaContext`\nand decides how to apply it: `pie-radio` names its host (which carries `role=\"radio\"`),\nwhile `pie-checkbox` / `pie-switch` name their internal input (their host is role-less, so\n`aria-label` there would be invalid). Context reaches across the shadow boundary that\n`aria-labelledby` cannot.",
259
+ "readonly": true
260
+ },
261
+ {
262
+ "kind": "field",
263
+ "name": "_handleHostClick",
264
+ "privacy": "private",
265
+ "description": "Forwards a click anywhere on the row to the slotted control, so the whole item is a hit\ntarget. Skips clicks that already landed on the control, to avoid a double toggle."
266
+ },
137
267
  {
138
268
  "kind": "method",
139
269
  "name": "_renderSecondaryText"
@@ -176,6 +306,14 @@
176
306
  {
177
307
  "name": "hasMedia",
178
308
  "fieldName": "hasMedia"
309
+ },
310
+ {
311
+ "name": "selection-type",
312
+ "fieldName": "selectionType"
313
+ },
314
+ {
315
+ "name": "disabled",
316
+ "fieldName": "disabled"
179
317
  }
180
318
  ],
181
319
  "superclass": {
@@ -4,7 +4,7 @@ const t = class t extends i {
4
4
  this.getAttribute("v") || this.setAttribute("v", t.v);
5
5
  }
6
6
  };
7
- t.v = "0.1.1";
7
+ t.v = "0.2.0";
8
8
  let s = t;
9
9
  export {
10
10
  s as P
@@ -0,0 +1,265 @@
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
+ };
package/dist/index.js CHANGED
@@ -1,23 +1,23 @@
1
- import { html as a, unsafeCSS as d } from "lit";
2
- import { P as m } from "./PieElement-Y2VnHR4y.js";
3
- import { RtlMixin as c, safeCustomElement as f } from "@justeattakeaway/pie-webc-core";
4
- const p = "*,*:after,*:before{box-sizing:inherit}:host{display:block}:host ::slotted(*:nth-last-child(n+2)){--list-item-size-modifier: 1px;--list-item-border-override: var(--list-item-size-modifier) solid var(--dt-color-divider-default)}";
5
- var h = Object.getOwnPropertyDescriptor, b = (r, s, n, l) => {
6
- for (var e = l > 1 ? void 0 : l ? h(s, n) : s, t = r.length - 1, o; t >= 0; t--)
7
- (o = r[t]) && (e = o(e) || e);
8
- return e;
1
+ import { html as n, unsafeCSS as m } from "lit";
2
+ import { P as f } from "./PieElement-B3g_8vKy.js";
3
+ import { RtlMixin as d, safeCustomElement as p } from "@justeattakeaway/pie-webc-core";
4
+ const c = "*,*:after,*:before{box-sizing:inherit}:host{display:block}:host ::slotted(pie-list-item:not(:last-of-type)){--list-item-size-modifier: 1px;--list-item-border: var(--list-item-size-modifier) solid var(--dt-color-divider-default)}";
5
+ var b = Object.getOwnPropertyDescriptor, u = (s, r, a, l) => {
6
+ for (var t = l > 1 ? void 0 : l ? b(r, a) : r, e = s.length - 1, o; e >= 0; e--)
7
+ (o = s[e]) && (t = o(t) || t);
8
+ return t;
9
9
  };
10
- let i = class extends c(m) {
10
+ let i = class extends d(f) {
11
11
  connectedCallback() {
12
12
  super.connectedCallback(), this.hasAttribute("role") || this.setAttribute("role", "list");
13
13
  }
14
14
  render() {
15
- return a`<slot></slot>`;
15
+ return n`<slot></slot>`;
16
16
  }
17
17
  };
18
- i.styles = d(p);
19
- i = b([
20
- f("pie-list")
18
+ i.styles = m(c);
19
+ i = u([
20
+ p("pie-list")
21
21
  ], i);
22
22
  export {
23
23
  i as PieList
@@ -1,4 +1,6 @@
1
1
  import { ComponentDefaultProps } from '@justeattakeaway/pie-webc-core';
2
+ export declare const selectionTypes: readonly ["none", "radio", "checkbox", "switch"];
3
+ export type SelectionType = typeof selectionTypes[number];
2
4
  export interface ListItemProps {
3
5
  /**
4
6
  * **Required:** Provides an overview of the content.
@@ -28,6 +30,28 @@ export interface ListItemProps {
28
30
  * **Note**: This has no effect when `secondaryText` is set, and should not be combined with `isCompact`.
29
31
  */
30
32
  hasMedia?: boolean;
33
+ /**
34
+ * The kind of interactive control this item hosts in its `leading` or `trailing` slot. This
35
+ * makes the whole row a selectable target: the item takes the appropriate role, provides its
36
+ * text as the control's accessible name/description, hides the duplicated visible text, and
37
+ * forwards row clicks to the control.
38
+ *
39
+ * - `none` (default) - a static, non-selectable list item (`role="listitem"`).
40
+ * - `radio` - hosts a `pie-radio` (used inside a `pie-radio-group`); the item is `presentation`.
41
+ * - `checkbox` - hosts a `pie-checkbox` (used inside a `pie-checkbox-group`); the item is `presentation`.
42
+ * - `switch` - hosts a `pie-switch`; there is no group, so the item stays `role="listitem"`.
43
+ *
44
+ * Set this on each selectable row. When the rows sit inside a `pie-radio-group`, the group lays
45
+ * them out as a divided list automatically; this prop governs the row's role and behaviour.
46
+ */
47
+ selectionType?: typeof selectionTypes[number];
48
+ /**
49
+ * Marks the row as disabled: it takes on the disabled styling and stops forwarding row clicks to
50
+ * its control. Set it alongside the slotted control's own `disabled` (the control still governs
51
+ * its own interactivity and keyboard behaviour). Has no visible effect on a non-selectable
52
+ * (static) item.
53
+ */
54
+ disabled?: boolean;
31
55
  }
32
56
  export type DefaultProps = ComponentDefaultProps<ListItemProps, keyof Omit<ListItemProps, 'primaryText' | 'secondaryText' | 'metaText'>>;
33
57
  export declare const defaultProps: DefaultProps;
@@ -1 +1 @@
1
- {"version":3,"file":"defs.d.ts","sourceRoot":"","sources":["../../src/pie-list-item/defs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC1B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAAG,qBAAqB,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,eAAe,GAAG,UAAU,CAAC,CAAC,CAAC;AAEzI,eAAO,MAAM,YAAY,EAAE,YAI1B,CAAC"}
1
+ {"version":3,"file":"defs.d.ts","sourceRoot":"","sources":["../../src/pie-list-item/defs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,eAAO,MAAM,cAAc,kDAAmD,CAAC;AAE/E,MAAM,MAAM,aAAa,GAAG,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;AAE1D,MAAM,WAAW,aAAa;IAC1B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAA;IAE7C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAAG,qBAAqB,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,eAAe,GAAG,UAAU,CAAC,CAAC,CAAC;AAEzI,eAAO,MAAM,YAAY,EAAE,YAM1B,CAAC"}
@@ -4,6 +4,8 @@ import { ListItemProps } from './defs';
4
4
  declare const componentSelector = "pie-list-item";
5
5
  /**
6
6
  * @tagname pie-list-item
7
+ * @slot leading - Content shown at the start of the item (for example an icon, avatar, or a slotted radio/checkbox).
8
+ * @slot trailing - Content shown at the end of the item. Mutually exclusive with `metaText`.
7
9
  */
8
10
  export declare class PieListItem extends PieElement implements ListItemProps {
9
11
  primaryText: ListItemProps['primaryText'];
@@ -12,7 +14,39 @@ export declare class PieListItem extends PieElement implements ListItemProps {
12
14
  isCompact: boolean;
13
15
  isBold: boolean;
14
16
  hasMedia: boolean;
17
+ selectionType: "checkbox" | "none" | "radio" | "switch";
18
+ disabled: boolean;
19
+ private _parentDisabled;
20
+ private _ariaProvider;
21
+ private _abortController;
22
+ private _hasExplicitRole;
23
+ private get _isSelectable();
24
+ private get _ownedByGroup();
25
+ private get _isDisabled();
26
+ private get _control();
15
27
  connectedCallback(): void;
28
+ disconnectedCallback(): void;
29
+ protected updated(): void;
30
+ /**
31
+ * Sets the item's role from `selectionType`: `presentation` for radio/checkbox (so the group
32
+ * owns those controls directly), otherwise `listitem` (static items and switches). A role set
33
+ * explicitly by the consumer is left untouched.
34
+ */
35
+ private _applyRole;
36
+ /**
37
+ * The accessible name and description this item provides to its slotted control, stitched from
38
+ * its text. Only provided in a selectable list. Each control consumes this via `ariaContext`
39
+ * and decides how to apply it: `pie-radio` names its host (which carries `role="radio"`),
40
+ * while `pie-checkbox` / `pie-switch` name their internal input (their host is role-less, so
41
+ * `aria-label` there would be invalid). Context reaches across the shadow boundary that
42
+ * `aria-labelledby` cannot.
43
+ */
44
+ private get _providedAria();
45
+ /**
46
+ * Forwards a click anywhere on the row to the slotted control, so the whole item is a hit
47
+ * target. Skips clicks that already landed on the control, to avoid a double toggle.
48
+ */
49
+ private _handleHostClick;
16
50
  _renderSecondaryText(): import('lit-html').TemplateResult<1> | typeof nothing;
17
51
  _renderTrailingContent(): import('lit-html').TemplateResult<1>;
18
52
  render(): import('lit-html').TemplateResult<1>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pie-list-item/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,OAAO,EAAa,MAAM,KAAK,CAAC;AAG/C,OAAO,EAAE,UAAU,EAAE,MAAM,yDAAyD,CAAC;AAErF,OAAO,EAAE,KAAK,aAAa,EAAgB,MAAM,QAAQ,CAAC;AAG1D,QAAA,MAAM,iBAAiB,kBAAkB,CAAC;AAE1C;;GAEG;AACH,qBACa,WAAY,SAAQ,UAAW,YAAW,aAAa;IAE5D,WAAW,EAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAG3C,aAAa,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAG9C,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAGpC,SAAS,UAA0B;IAGnC,MAAM,UAAuB;IAG7B,QAAQ,UAAyB;IAErC,iBAAiB;IAQjB,oBAAoB;IAUpB,sBAAsB;IAStB,MAAM;IA0BN,MAAM,CAAC,MAAM,0BAAqB;CACrC;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC;KACpC;CACJ"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pie-list-item/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACa,OAAO,EAC1B,MAAM,KAAK,CAAC;AAWb,OAAO,EAAE,UAAU,EAAE,MAAM,yDAAyD,CAAC;AACrF,OAAO,EAAE,KAAK,aAAa,EAAgC,MAAM,QAAQ,CAAC;AAG1E,QAAA,MAAM,iBAAiB,kBAAkB,CAAC;AAE1C;;;;GAIG;AACH,qBACa,WAAY,SAAQ,UAAW,YAAW,aAAa;IAE5D,WAAW,EAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAG3C,aAAa,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAG9C,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAGpC,SAAS,UAA0B;IAGnC,MAAM,UAAuB;IAG7B,QAAQ,UAAyB;IAIjC,aAAa,2CAA8B;IAG3C,QAAQ,UAAyB;IAMrC,OAAO,CAAC,eAAe,CAAS;IAYhC,OAAO,CAAC,aAAa,CAA8E;IAEnG,OAAO,CAAC,gBAAgB,CAAmB;IAE3C,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,KAAK,aAAa,GAExB;IAKD,OAAO,KAAK,aAAa,GAExB;IAKD,OAAO,KAAK,WAAW,GAEtB;IAID,OAAO,KAAK,QAAQ,GAEnB;IAED,iBAAiB;IAUjB,oBAAoB;IAKpB,SAAS,CAAC,OAAO;IAKjB;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAKlB;;;;;;;OAOG;IACH,OAAO,KAAK,aAAa,GAMxB;IAED;;;OAGG;IACH,OAAO,CAAC,gBAAgB,CAQtB;IAEF,oBAAoB;IAUpB,sBAAsB;IAStB,MAAM;IA4BN,MAAM,CAAC,MAAM,0BAAqB;CACrC;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC;KACpC;CACJ"}
@@ -1,9 +1,9 @@
1
1
  import "lit";
2
2
  import "lit/decorators.js";
3
- import "@justeattakeaway/pie-webc-core";
4
- import "../PieElement-Y2VnHR4y.js";
5
3
  import "lit/directives/class-map.js";
6
- import { P as s } from "../index-D4OaeeiJ.js";
4
+ import { P as s } from "../index-KnN5fPmj.js";
5
+ import "@justeattakeaway/pie-webc-core";
6
+ import "../PieElement-B3g_8vKy.js";
7
7
  export {
8
8
  s as PieListItem
9
9
  };
@@ -1,15 +1,16 @@
1
1
  import * as e from "react";
2
2
  import { createComponent as t } from "@lit/react";
3
- import { P as m } from "../index-D4OaeeiJ.js";
4
- import { d as c } from "../index-D4OaeeiJ.js";
3
+ import { P as s } from "../index-KnN5fPmj.js";
4
+ import { d as c, s as n } from "../index-KnN5fPmj.js";
5
5
  const i = t({
6
6
  displayName: "PieListItem",
7
- elementClass: m,
7
+ elementClass: s,
8
8
  react: e,
9
9
  tagName: "pie-list-item",
10
10
  events: {}
11
11
  }), a = i;
12
12
  export {
13
13
  a as PieListItem,
14
- c as defaultProps
14
+ c as defaultProps,
15
+ n as selectionTypes
15
16
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-list",
3
3
  "description": "PIE Design System List built using Web Components",
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -38,11 +38,12 @@
38
38
  "license": "Apache-2.0",
39
39
  "devDependencies": {
40
40
  "@justeattakeaway/pie-components-config": "0.21.3",
41
- "@justeattakeaway/pie-css": "1.3.0",
42
- "@justeattakeaway/pie-monorepo-utils": "0.9.6"
41
+ "@justeattakeaway/pie-css": "1.3.1",
42
+ "@justeattakeaway/pie-monorepo-utils": "0.9.7"
43
43
  },
44
44
  "dependencies": {
45
- "@justeattakeaway/pie-webc-core": "16.0.0"
45
+ "@justeattakeaway/pie-webc-core": "16.1.0",
46
+ "@lit/context": "1.1.6"
46
47
  },
47
48
  "customElements": "custom-elements.json",
48
49
  "sideEffects": [
package/src/list.scss CHANGED
@@ -3,9 +3,5 @@
3
3
  :host {
4
4
  display: block;
5
5
 
6
- ::slotted(*:nth-last-child(n+2)) {
7
- // This allows list-items following other list-items to have 1px stripped off of their padding to account for the added border.
8
- --list-item-size-modifier: 1px;
9
- --list-item-border-override: var(--list-item-size-modifier) solid var(--dt-color-divider-default);
10
- }
6
+ @include p.list-item-divider;
11
7
  }
@@ -1,5 +1,9 @@
1
1
  import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core';
2
2
 
3
+ export const selectionTypes = ['none', 'radio', 'checkbox', 'switch'] as const;
4
+
5
+ export type SelectionType = typeof selectionTypes[number];
6
+
3
7
  export interface ListItemProps {
4
8
  /**
5
9
  * **Required:** Provides an overview of the content.
@@ -32,6 +36,30 @@ export interface ListItemProps {
32
36
  * **Note**: This has no effect when `secondaryText` is set, and should not be combined with `isCompact`.
33
37
  */
34
38
  hasMedia?: boolean
39
+
40
+ /**
41
+ * The kind of interactive control this item hosts in its `leading` or `trailing` slot. This
42
+ * makes the whole row a selectable target: the item takes the appropriate role, provides its
43
+ * text as the control's accessible name/description, hides the duplicated visible text, and
44
+ * forwards row clicks to the control.
45
+ *
46
+ * - `none` (default) - a static, non-selectable list item (`role="listitem"`).
47
+ * - `radio` - hosts a `pie-radio` (used inside a `pie-radio-group`); the item is `presentation`.
48
+ * - `checkbox` - hosts a `pie-checkbox` (used inside a `pie-checkbox-group`); the item is `presentation`.
49
+ * - `switch` - hosts a `pie-switch`; there is no group, so the item stays `role="listitem"`.
50
+ *
51
+ * Set this on each selectable row. When the rows sit inside a `pie-radio-group`, the group lays
52
+ * them out as a divided list automatically; this prop governs the row's role and behaviour.
53
+ */
54
+ selectionType?: typeof selectionTypes[number]
55
+
56
+ /**
57
+ * Marks the row as disabled: it takes on the disabled styling and stops forwarding row clicks to
58
+ * its control. Set it alongside the slotted control's own `disabled` (the control still governs
59
+ * its own interactivity and keyboard behaviour). Has no visible effect on a non-selectable
60
+ * (static) item.
61
+ */
62
+ disabled?: boolean
35
63
  }
36
64
 
37
65
  export type DefaultProps = ComponentDefaultProps<ListItemProps, keyof Omit<ListItemProps, 'primaryText' | 'secondaryText' | 'metaText'>>;
@@ -40,4 +68,6 @@ export const defaultProps: DefaultProps = {
40
68
  isCompact: false,
41
69
  isBold: false,
42
70
  hasMedia: false,
71
+ selectionType: 'none',
72
+ disabled: false,
43
73
  };
@@ -1,15 +1,26 @@
1
- import { html, nothing, unsafeCSS } from 'lit';
2
- import { property } from 'lit/decorators.js';
3
- import { safeCustomElement } from '@justeattakeaway/pie-webc-core';
4
- import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement';
1
+ import {
2
+ html, isServer, nothing, unsafeCSS,
3
+ } from 'lit';
4
+ import { property, state } from 'lit/decorators.js';
5
5
  import { classMap } from 'lit/directives/class-map.js';
6
- import { type ListItemProps, defaultProps } from './defs';
6
+ import { consume, ContextProvider } from '@lit/context';
7
+ import {
8
+ safeCustomElement,
9
+ validPropertyValues,
10
+ parentDisabledContext,
11
+ ariaContext,
12
+ type ContextualAria,
13
+ } from '@justeattakeaway/pie-webc-core';
14
+ import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement';
15
+ import { type ListItemProps, defaultProps, selectionTypes } from './defs';
7
16
  import styles from './list-item.scss?inline';
8
17
 
9
18
  const componentSelector = 'pie-list-item';
10
19
 
11
20
  /**
12
21
  * @tagname pie-list-item
22
+ * @slot leading - Content shown at the start of the item (for example an icon, avatar, or a slotted radio/checkbox).
23
+ * @slot trailing - Content shown at the end of the item. Mutually exclusive with `metaText`.
13
24
  */
14
25
  @safeCustomElement('pie-list-item')
15
26
  export class PieListItem extends PieElement implements ListItemProps {
@@ -31,14 +42,119 @@ export class PieListItem extends PieElement implements ListItemProps {
31
42
  @property({ type: Boolean })
32
43
  hasMedia = defaultProps.hasMedia;
33
44
 
45
+ @property({ type: String, attribute: 'selection-type' })
46
+ @validPropertyValues(componentSelector, selectionTypes, defaultProps.selectionType)
47
+ selectionType = defaultProps.selectionType;
48
+
49
+ @property({ type: Boolean })
50
+ disabled = defaultProps.disabled;
51
+
52
+ // Whether a disabling ancestor (e.g. `pie-radio-group`) has provided its disabled state.
53
+ // Defaults to false when there is no provider (a standalone item or a static list).
54
+ @consume({ context: parentDisabledContext, subscribe: true })
55
+ @state()
56
+ private _parentDisabled = false;
57
+
58
+ // Provides this item's accessible name/description down to its slotted control via the shared
59
+ // aria context, which the control consumes and applies to the element carrying its semantics
60
+ // (the internal input for pie-checkbox / pie-switch). See `ariaContext` in pie-webc-core.
61
+ //
62
+ // Guarded with `isServer`: `@lit/context`'s ContextProvider attaches `context-request`
63
+ // listeners to the host in its constructor (via `host.addEventListener`). During SSR/prerender
64
+ // the element is constructed without a DOM host, so that call throws
65
+ // ("host.addEventListener is not a function") and breaks the build. The provider is client-only
66
+ // anyway (context is delivered after `connectedCallback`, which SSR never runs), so it is safe
67
+ // to skip on the server.
68
+ private _ariaProvider = isServer ? undefined : new ContextProvider(this, { context: ariaContext });
69
+
70
+ private _abortController!: AbortController;
71
+
72
+ private _hasExplicitRole = false;
73
+
74
+ private get _isSelectable (): boolean {
75
+ return this.selectionType !== 'none';
76
+ }
77
+
78
+ // radio/checkbox are owned by a selection group, which is why the item becomes `presentation`
79
+ // (so the group owns the controls directly) and the radio is named on its host. A switch has
80
+ // no group, so the item stays a `listitem`.
81
+ private get _ownedByGroup (): boolean {
82
+ return this.selectionType === 'radio' || this.selectionType === 'checkbox';
83
+ }
84
+
85
+ // True when the row should be treated as disabled: either its own `disabled` prop is set, or the
86
+ // containing group is disabled (provided via context). Declarative and reactive, so the styling
87
+ // and row-click guard react without observing the slotted control.
88
+ private get _isDisabled (): boolean {
89
+ return this.disabled || this._parentDisabled;
90
+ }
91
+
92
+ // The interactive control (radio/checkbox/switch) slotted into this item, if any. Used only by
93
+ // the client-side row-click handler.
94
+ private get _control (): HTMLElement | null {
95
+ return this.querySelector('pie-radio, pie-checkbox, pie-switch');
96
+ }
97
+
34
98
  connectedCallback () {
35
99
  super.connectedCallback();
36
100
 
37
- if (!this.hasAttribute('role')) {
38
- this.setAttribute('role', 'listitem');
39
- }
101
+ // Respect a role the consumer set explicitly; otherwise we manage it from `selectionType`.
102
+ this._hasExplicitRole = this.hasAttribute('role');
103
+
104
+ this._abortController = new AbortController();
105
+ this.addEventListener('click', this._handleHostClick, { signal: this._abortController.signal });
40
106
  }
41
107
 
108
+ disconnectedCallback () {
109
+ super.disconnectedCallback();
110
+ this._abortController.abort();
111
+ }
112
+
113
+ protected updated () {
114
+ this._applyRole();
115
+ this._ariaProvider?.setValue(this._providedAria);
116
+ }
117
+
118
+ /**
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.
122
+ */
123
+ private _applyRole (): void {
124
+ if (this._hasExplicitRole) return;
125
+ this.setAttribute('role', this._ownedByGroup ? 'presentation' : 'listitem');
126
+ }
127
+
128
+ /**
129
+ * The accessible name and description this item provides to its slotted control, stitched from
130
+ * its text. Only provided in a selectable list. Each control consumes this via `ariaContext`
131
+ * and decides how to apply it: `pie-radio` names its host (which carries `role="radio"`),
132
+ * while `pie-checkbox` / `pie-switch` name their internal input (their host is role-less, so
133
+ * `aria-label` there would be invalid). Context reaches across the shadow boundary that
134
+ * `aria-labelledby` cannot.
135
+ */
136
+ private get _providedAria (): ContextualAria | undefined {
137
+ if (!this._isSelectable) return undefined;
138
+
139
+ const description = [this.secondaryText, this.metaText].filter(Boolean).join('. ');
140
+
141
+ return { label: this.primaryText, description: description || undefined };
142
+ }
143
+
144
+ /**
145
+ * Forwards a click anywhere on the row to the slotted control, so the whole item is a hit
146
+ * target. Skips clicks that already landed on the control, to avoid a double toggle.
147
+ */
148
+ private _handleHostClick = (event: MouseEvent): void => {
149
+ if (!this._isSelectable || this._isDisabled) return;
150
+
151
+ const control = this._control;
152
+ if (!control || event.composedPath().includes(control)) return;
153
+
154
+ control.click();
155
+ control.focus();
156
+ };
157
+
42
158
  _renderSecondaryText () {
43
159
  const { secondaryText } = this;
44
160
  if (secondaryText) {
@@ -52,7 +168,7 @@ export class PieListItem extends PieElement implements ListItemProps {
52
168
  _renderTrailingContent () {
53
169
  const { metaText } = this;
54
170
  if (metaText) {
55
- return html`<span class="c-listItem-metaText c-listItem-trailing">${metaText}</span>`;
171
+ return html`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this._isSelectable ? 'true' : nothing}>${metaText}</span>`;
56
172
  }
57
173
 
58
174
  return html`<div class="c-listItem-trailing"><slot name="trailing"></slot></div>`;
@@ -66,6 +182,8 @@ export class PieListItem extends PieElement implements ListItemProps {
66
182
  'is-compact': this.isCompact,
67
183
  'is-bold': this.isBold,
68
184
  'has-media': this.hasMedia,
185
+ 'is-selectable': this._isSelectable,
186
+ 'is-disabled': this._isDisabled,
69
187
  };
70
188
 
71
189
  return html`
@@ -74,7 +192,7 @@ export class PieListItem extends PieElement implements ListItemProps {
74
192
  <slot name="leading"></slot>
75
193
  </div>
76
194
 
77
- <div class="c-listItem-text">
195
+ <div class="c-listItem-text" aria-hidden=${this._isSelectable ? 'true' : nothing}>
78
196
  <span class="c-listItem-primaryText">${primaryText}</span>
79
197
  ${this._renderSecondaryText()}
80
198
  </div>
@@ -6,6 +6,7 @@
6
6
  // host declaration wins over a value inherited from an ancestor.
7
7
  --list-item-inline-padding: var(--dt-spacing-d);
8
8
  --list-item-alignment: flex-start;
9
+ --list-item-border: none;
9
10
 
10
11
  // Using contents to remove the host from the box model so that the internal container element fully acts as the component shell
11
12
  display: block;
@@ -29,7 +30,7 @@
29
30
  align-items: var(--list-item-alignment);
30
31
  gap: var(--dt-spacing-c);
31
32
  min-height: 56px;
32
- border-bottom: var(--list-item-border-override, none);
33
+ border-bottom: var(--list-item-border);
33
34
  }
34
35
 
35
36
  // Two lines of text (primary + secondary) need top-alignment so leading/trailing
@@ -48,6 +49,32 @@
48
49
  min-height: 48px;
49
50
  }
50
51
 
52
+ // Interactive row states. When the item is inside a radio/checkbox group the whole row
53
+ // is a click target (it forwards the click to the slotted control), so it shows hover and
54
+ // active surface tints, matching the control's own interactive states. These are suppressed
55
+ // when the slotted control is disabled (the item reflects this as `is-disabled`). The
56
+ // defaults use the standard PIE interactive overlay tokens; override `--list-item-hover-bg` /
57
+ // `--list-item-active-bg` if design specifies dedicated list tokens.
58
+ .c-listItem-container.is-selectable:not(.is-disabled) {
59
+ --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
+ --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
+
62
+ @supports (background-color: color-mix(in srgb, black, white)) {
63
+ --list-item-hover-bg: color-mix(in srgb, var(--dt-color-hover-01-bg) var(--dt-color-hover-01), transparent);
64
+ --list-item-active-bg: color-mix(in srgb, var(--dt-color-active-01-bg) var(--dt-color-active-01), transparent);
65
+ }
66
+
67
+ cursor: pointer;
68
+
69
+ &:hover {
70
+ background-color: var(--list-item-hover-bg);
71
+ }
72
+
73
+ &:active {
74
+ background-color: var(--list-item-active-bg);
75
+ }
76
+ }
77
+
51
78
  // Larger slotted media (e.g. pie-thumbnail) reduces the block padding, but only
52
79
  // when the primary text stands alone (`:only-child` == no secondary text). With
53
80
  // secondary text the default padding is kept so two lines of text sit correctly.
@@ -1,79 +0,0 @@
1
- import { unsafeCSS as p, html as o, nothing as g } from "lit";
2
- import { property as n } from "lit/decorators.js";
3
- import { safeCustomElement as h } from "@justeattakeaway/pie-webc-core";
4
- import { P as f } from "./PieElement-Y2VnHR4y.js";
5
- import { classMap as y } from "lit/directives/class-map.js";
6
- const m = {
7
- isCompact: !1,
8
- isBold: !1,
9
- hasMedia: !1
10
- }, v = "*,*:after,*:before{box-sizing:inherit}:host{--list-item-inline-padding: var(--dt-spacing-d);--list-item-alignment: flex-start;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-override, none)}.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.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)}";
11
- var b = Object.defineProperty, x = Object.getOwnPropertyDescriptor, a = (t, s, r, l) => {
12
- for (var e = l > 1 ? void 0 : l ? x(s, r) : s, d = t.length - 1, c; d >= 0; d--)
13
- (c = t[d]) && (e = (l ? c(s, r, e) : c(e)) || e);
14
- return l && e && b(s, r, e), e;
15
- };
16
- let i = class extends f {
17
- constructor() {
18
- super(...arguments), this.isCompact = m.isCompact, this.isBold = m.isBold, this.hasMedia = m.hasMedia;
19
- }
20
- connectedCallback() {
21
- super.connectedCallback(), this.hasAttribute("role") || this.setAttribute("role", "listitem");
22
- }
23
- _renderSecondaryText() {
24
- const { secondaryText: t } = this;
25
- return t ? o`<span class="c-listItem-secondaryText">${t}</span>` : g;
26
- }
27
- // metaText is another form of trailing content. The component can only ever display either slotted trailing content or the metaText string
28
- _renderTrailingContent() {
29
- const { metaText: t } = this;
30
- return t ? o`<span class="c-listItem-metaText c-listItem-trailing">${t}</span>` : o`<div class="c-listItem-trailing"><slot name="trailing"></slot></div>`;
31
- }
32
- render() {
33
- const { primaryText: t } = this, s = {
34
- "c-listItem-container": !0,
35
- "is-compact": this.isCompact,
36
- "is-bold": this.isBold,
37
- "has-media": this.hasMedia
38
- };
39
- return o`
40
- <div class=${y(s)}>
41
- <div class="c-listItem-leading">
42
- <slot name="leading"></slot>
43
- </div>
44
-
45
- <div class="c-listItem-text">
46
- <span class="c-listItem-primaryText">${t}</span>
47
- ${this._renderSecondaryText()}
48
- </div>
49
-
50
- ${this._renderTrailingContent()}
51
- </div>`;
52
- }
53
- };
54
- i.styles = p(v);
55
- a([
56
- n({ type: String })
57
- ], i.prototype, "primaryText", 2);
58
- a([
59
- n({ type: String })
60
- ], i.prototype, "secondaryText", 2);
61
- a([
62
- n({ type: String })
63
- ], i.prototype, "metaText", 2);
64
- a([
65
- n({ type: Boolean })
66
- ], i.prototype, "isCompact", 2);
67
- a([
68
- n({ type: Boolean })
69
- ], i.prototype, "isBold", 2);
70
- a([
71
- n({ type: Boolean })
72
- ], i.prototype, "hasMedia", 2);
73
- i = a([
74
- h("pie-list-item")
75
- ], i);
76
- export {
77
- i as P,
78
- m as d
79
- };