@justeattakeaway/pie-list 0.4.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 CHANGED
@@ -37,6 +37,8 @@
37
37
  - [Multi-select (checkboxes)](#multi-select-checkboxes)
38
38
  - [Independent toggles (switches)](#independent-toggles-switches)
39
39
  - [Link items](#link-items)
40
+ - [Indicating the current page](#indicating-the-current-page)
41
+ - [Button items](#button-items)
40
42
  - [Framework variants](#framework-variants)
41
43
  - [Usage Notes and Rules](#usage-notes-and-rules)
42
44
  - [Questions and Support](#questions-and-support)
@@ -71,8 +73,9 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc
71
73
  | `isCompact` | `true`, `false` | Decreases the item height to save vertical space. See the [rules](#usage-notes-and-rules) below. | `false` |
72
74
  | `isBold` | `true`, `false` | Sets the primary text to a bold font-weight. | `false` |
73
75
  | `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` |
74
- | `interactionType` | `"none"`, `"radio"`, `"checkbox"`, `"switch"`, `"link"` | Declares how the **whole row** behaves, and drives its role, accessible naming, click forwarding and interactive states from this one prop. `"none"` is a static item; `"radio"`/`"checkbox"`/`"switch"` host the matching control in the `leading`/`trailing` slot; `"link"` turns the row into a single navigation link (slot an empty `<a slot="link" href="...">`). See [Selectable lists](#selectable-lists) and [Link items](#link-items). | `"none"` |
76
+ | `interactionType` | `"none"`, `"radio"`, `"checkbox"`, `"switch"`, `"link"`, `"button"` | Declares how the **whole row** behaves, and drives its role, accessible naming, click forwarding and interactive states from this one prop. `"none"` is a static item; `"radio"`/`"checkbox"`/`"switch"` host the matching control in the `leading`/`trailing` slot; `"link"` turns the row into a single navigation link (slot an empty `<a slot="link" href="...">`); `"button"` turns the row into a single button for an in-page action (the item renders the button for you - listen for `click`). See [Selectable lists](#selectable-lists), [Link items](#link-items) and [Button items](#button-items). | `"none"` |
75
77
  | `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` |
78
+ | `aria` | `{ button?: { haspopup?: "menu" \| "listbox" \| "tree" \| "grid" \| "dialog" \| "true" } }` | Additional ARIA properties that the item cannot derive from its text props. `button.haspopup` only applies when `interactionType="button"` and is forwarded to the internal `<button>` element. Set it when the button row triggers a popup such as a dialog or menu. | `undefined` |
76
79
 
77
80
  ### Slots
78
81
 
@@ -141,22 +144,22 @@ This component does not emit any custom events. To listen for interactions, trea
141
144
  Specifically, for **every** interaction type the item takes care of:
142
145
 
143
146
  - the **role** on the item itself (`listitem`, or `presentation` for radio/checkbox rows so the group owns the control directly) - you never write `role="..."`;
144
- - the **accessible name and description** of whatever it hosts - you never write `aria-label`, `aria-labelledby`, `aria-description` or `aria-describedby` on the control or anchor;
147
+ - the **accessible name and description** of whatever it hosts - you never write `aria-label`, `aria-labelledby`, `aria-description` or `aria-describedby` on the control, anchor or button;
145
148
  - **hiding the duplicated visible text** from assistive technology (the item `aria-hidden`s its own text once the name/description have been mirrored onto the interactive element), so nothing is announced twice.
146
149
 
147
150
  The one accessibility attribute you **do** own is the accessible name of the `pie-list` container itself (`aria-label` / `aria-labelledby`), because only you know what the list represents. See the note above.
148
151
 
149
152
  ### How the naming is derived
150
153
 
151
- When a `pie-list-item` hosts an interactive element (a slotted control set via `interactionType="radio" | "checkbox" | "switch"`, or a slotted link anchor via `interactionType="link"`) it generates that element's accessibility naming from its own text:
154
+ When a `pie-list-item` hosts an interactive element (a slotted control set via `interactionType="radio" | "checkbox" | "switch"`, a slotted link anchor via `interactionType="link"`, or the button it renders for `interactionType="button"`) it generates that element's accessibility naming from its own text:
152
155
 
153
- - `primaryText` becomes the control's **accessible name**;
156
+ - `primaryText` becomes the element's **accessible name**;
154
157
  - `secondaryText` and `metaText` become its **accessible description** (combined with a full stop when both are present);
155
- - the visible text rendered inside the item is `aria-hidden`, so a screen reader announces the name and description once (via the control) rather than twice.
158
+ - the visible text rendered inside the item is `aria-hidden`, so a screen reader announces the name and description once (via the interactive element) rather than twice.
156
159
 
157
- How the naming reaches the element differs by type, but this is entirely internal - you never do it yourself. Selection controls receive it through a shared context and each applies it to the element that carries its role: `pie-radio` names its host, while `pie-checkbox` and `pie-switch` name their internal `input` (their host is role-less). A link anchor is a plain light-DOM element (not a PIE control), so `pie-list-item` sets `aria-label` and `aria-description` on it directly.
160
+ How the naming reaches the element differs by type, but this is entirely internal - you never do it yourself. Selection controls receive it through a shared context and each applies it to the element that carries its role: `pie-radio` names its host, while `pie-checkbox` and `pie-switch` name their internal `input` (their host is role-less). A link anchor is a plain light-DOM element you slot (not a PIE control), so `pie-list-item` sets `aria-label` and `aria-description` on it directly. A `button` row's button is one the item renders itself, so it is named the same way from within the item.
158
161
 
159
- **If you do set something yourself, it always wins.** If a slotted control already has its own name (its `label` or `aria` prop), or the slotted anchor already has an `aria-label`/`aria-labelledby` (or `aria-description`/`aria-describedby`), the item leaves it untouched and does not overwrite it. This is an escape hatch for the rare case that needs it, not something you normally reach for.
162
+ **For slotted elements, anything you set yourself wins.** If a slotted control already has its own name (its `label` or `aria` prop), or the slotted link anchor already has an `aria-label`/`aria-labelledby` (or `aria-description`/`aria-describedby`), the item leaves it untouched and does not overwrite it. This is an escape hatch for the rare case that needs it, not something you normally reach for. (A `button` row's button is rendered and named by the item, so there is nothing for you to set.)
160
163
 
161
164
  ## Usage Examples
162
165
 
@@ -415,6 +418,104 @@ import { PieListItem } from '@justeattakeaway/pie-webc/react/list-item.js';
415
418
  </PieListItem>
416
419
  ```
417
420
 
421
+ #### Indicating the current page
422
+
423
+ To highlight the active link in a navigation list, combine `isBold` on the `pie-list-item` with `aria-current="page"` on the slotted anchor. `isBold` gives the active item a heavier weight visually; `aria-current="page"` communicates the current page to assistive technology. `pie-list-item` only manages the anchor's `aria-label` and `aria-description`, so `aria-current` does not conflict with the item's naming logic and is safe to set yourself.
424
+
425
+ You can also slot a tick icon into the `trailing` slot of the active item as an additional visual indicator. PIE icons render with `role="presentation"` on their SVG, so they are already hidden from assistive technology and no extra `aria-hidden` is needed.
426
+
427
+ ```js
428
+ import '@justeattakeaway/pie-icons-webc/dist/IconCheck.js';
429
+ ```
430
+
431
+ ```html
432
+ <pie-list aria-label="Account navigation">
433
+ <pie-list-item interactionType="link" primaryText="Overview">
434
+ <a slot="link" href="/account"></a>
435
+ </pie-list-item>
436
+ <pie-list-item interactionType="link" isBold primaryText="Orders">
437
+ <a slot="link" href="/account/orders" aria-current="page"></a>
438
+ <icon-check slot="trailing"></icon-check>
439
+ </pie-list-item>
440
+ <pie-list-item interactionType="link" primaryText="Payment methods">
441
+ <a slot="link" href="/account/payment"></a>
442
+ </pie-list-item>
443
+ <pie-list-item interactionType="link" primaryText="Addresses">
444
+ <a slot="link" href="/account/addresses"></a>
445
+ </pie-list-item>
446
+ </pie-list>
447
+ ```
448
+
449
+ In React, pass `isBold` as a prop and `aria-current="page"` directly to the `<Link>` component (or raw anchor):
450
+
451
+ ```jsx
452
+ import { IconCheck } from '@justeattakeaway/pie-icons-webc/dist/IconCheck.js';
453
+
454
+ <PieListItem interactionType="link" isBold primaryText="Orders">
455
+ <Link slot="link" href="/account/orders" aria-current="page" />
456
+ <IconCheck slot="trailing" />
457
+ </PieListItem>
458
+ ```
459
+
460
+ ### Button items
461
+
462
+ Set `interactionType="button"` to turn the whole row into one button. Use this for an **in-page action** (opening a dialog, running a command); use [`link`](#link-items) when the row navigates to another page. Unlike a link, **you do not slot anything** - the item renders the interactive element for you: an invisible, row-sized native `<button type="button">` stretched over the row. A click or tap anywhere on the row activates it, and keyboard focus lands on the row as a whole (the focus ring hugs the row). It is just an action trigger, **not a form control** - it is `type="button"`, so it never submits a form, and it carries no value.
463
+
464
+ The item names the button from its own text: `primaryText` becomes the button's `aria-label` and `secondaryText`/`metaText` its `aria-description`, and the visible text is hidden from assistive technology so nothing is announced twice (see [You never set ARIA on a list item or its slotted content](#you-never-set-aria-on-a-list-item-or-its-slotted-content)).
465
+
466
+ **Handle activation by listening for `click`** on the `pie-list-item` (or on the `pie-list`, since the event bubbles). Because it is a real `<button>`, activation is native: both pointer taps and keyboard (Enter, and Space) fire a `click`, so a single `click` listener covers every input. The row shows a pressed tint while it is pressed - by pointer, or while Space is held on the focused row - and the focus ring on keyboard focus. Give the `pie-list` its own accessible name (e.g. `aria-label`).
467
+
468
+ **When the button opens a popup (dialog, menu, etc.)**, pass the `aria` prop with `button.haspopup` set to the popup type. This is forwarded to the internal `<button>` so assistive technology announces it correctly:
469
+
470
+ ```html
471
+ <!-- A row that opens a dialog -->
472
+ <pie-list-item
473
+ interactionType="button"
474
+ primaryText="Delete account"
475
+ secondaryText="Permanently remove your account"
476
+ .aria=${{ button: { haspopup: 'dialog' } }}>
477
+ </pie-list-item>
478
+ ```
479
+
480
+ In React:
481
+
482
+ ```jsx
483
+ <PieListItem
484
+ interactionType="button"
485
+ primaryText="Delete account"
486
+ secondaryText="Permanently remove your account"
487
+ aria={{ button: { haspopup: 'dialog' } }}
488
+ onClick={openConfirmDialog}
489
+ />
490
+ ```
491
+
492
+ ```js
493
+ import '@justeattakeaway/pie-webc/components/list.js';
494
+ import '@justeattakeaway/pie-webc/components/list-item.js';
495
+ ```
496
+
497
+ ```html
498
+ <pie-list aria-label="Account actions">
499
+ <pie-list-item interactionType="button" primaryText="Edit profile" secondaryText="Update your name and photo"></pie-list-item>
500
+ <pie-list-item interactionType="button" primaryText="Sign out" secondaryText="End your session on this device"></pie-list-item>
501
+ </pie-list>
502
+
503
+ <script type="module">
504
+ document.querySelector('pie-list').addEventListener('click', (event) => {
505
+ const item = event.target.closest('pie-list-item');
506
+ if (item) runActionFor(item.primaryText);
507
+ });
508
+ </script>
509
+ ```
510
+
511
+ In React, attach an `onClick` to the item:
512
+
513
+ ```jsx
514
+ import { PieListItem } from '@justeattakeaway/pie-webc/react/list-item.js';
515
+
516
+ <PieListItem interactionType="button" primaryText="Edit profile" secondaryText="Update your name and photo" onClick={openProfileEditor} />
517
+ ```
518
+
418
519
  ### Framework variants
419
520
 
420
521
  **For Native JS Applications, Vue, Angular, Svelte etc.:**
@@ -70,9 +70,9 @@
70
70
  "kind": "variable",
71
71
  "name": "interactionTypes",
72
72
  "type": {
73
- "text": "['none', 'radio', 'checkbox', 'switch', 'link']"
73
+ "text": "['none', 'radio', 'checkbox', 'switch', 'link', 'button']"
74
74
  },
75
- "default": "['none', 'radio', 'checkbox', 'switch', 'link']"
75
+ "default": "['none', 'radio', 'checkbox', 'switch', 'link', 'button']"
76
76
  },
77
77
  {
78
78
  "kind": "variable",
@@ -174,6 +174,14 @@
174
174
  "name": "disabled",
175
175
  "attribute": "disabled"
176
176
  },
177
+ {
178
+ "kind": "field",
179
+ "name": "aria",
180
+ "type": {
181
+ "text": "ListItemProps['aria']"
182
+ },
183
+ "attribute": "aria"
184
+ },
177
185
  {
178
186
  "kind": "field",
179
187
  "name": "_parentDisabled",
@@ -250,6 +258,24 @@
250
258
  "privacy": "private",
251
259
  "readonly": true
252
260
  },
261
+ {
262
+ "kind": "field",
263
+ "name": "_isButtonRow",
264
+ "type": {
265
+ "text": "boolean"
266
+ },
267
+ "privacy": "private",
268
+ "readonly": true
269
+ },
270
+ {
271
+ "kind": "field",
272
+ "name": "_isOverlayRow",
273
+ "type": {
274
+ "text": "boolean"
275
+ },
276
+ "privacy": "private",
277
+ "readonly": true
278
+ },
253
279
  {
254
280
  "kind": "field",
255
281
  "name": "_providesAria",
@@ -295,7 +321,7 @@
295
321
  "text": "void"
296
322
  }
297
323
  },
298
- "description": "Sets the item's role from `interactionType`: `presentation` for radio/checkbox (so the group\nowns those controls directly), otherwise `listitem` (static items, switches and links). A role\nset explicitly by the consumer is left untouched, even if it is added or removed dynamically\nafter the element connects."
324
+ "description": "Sets the item's role from `interactionType`: `presentation` for radio/checkbox (so the group\nowns those controls directly), otherwise `listitem` (static items, switches, links and\nbuttons). A role set explicitly by the consumer is left untouched, even if it is added or\nremoved dynamically after the element connects."
299
325
  },
300
326
  {
301
327
  "kind": "field",
@@ -304,7 +330,7 @@
304
330
  "text": "ContextualAria | undefined"
305
331
  },
306
332
  "privacy": "private",
307
- "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.",
333
+ "description": "The accessible name and description this item provides to its interactive element, stitched\nfrom its text. Selection controls consume it via `ariaContext` (each applying it to the\nelement that carries its role); the action button of a `button` row takes it directly as\n`aria-label`/`aria-description` in the template; a link anchor takes it via `_applyLinkAria`.",
308
334
  "readonly": true
309
335
  },
310
336
  {
@@ -322,7 +348,7 @@
322
348
  "text": "void"
323
349
  }
324
350
  },
325
- "description": "When the item is a link, names the slotted (empty) anchor from the item's own text: the parent\nowns the child's aria. The anchor is plain light-DOM, so it cannot reference the shadow-DOM text\nvia `aria-labelledby`; and it is not a PIE control that consumes `ariaContext`, so we set the\nattributes imperatively. The visible text is `aria-hidden` in the template, so nothing is\nannounced twice.\n\nA consumer-provided name/description always wins: we capture (once) whether the anchor already\ncarries `aria-label`/`aria-labelledby` or `aria-description`/`aria-describedby` and, if so,\nleave that attribute untouched. Otherwise we keep our generated value in sync with the text."
351
+ "description": "On a link row, names the slotted (empty) anchor from the item's own text: the parent owns the\nchild's aria. The anchor is plain light-DOM, so it cannot reference the shadow-DOM text via\n`aria-labelledby`; and it is not a PIE control that consumes `ariaContext`, so we set the\nattributes imperatively. The visible text is `aria-hidden` in the template, so nothing is\nannounced twice. (A `button` row names its own rendered element in the template instead.)\n\nA consumer-provided name/description always wins: we capture (once) whether the anchor already\ncarries `aria-label`/`aria-labelledby` or `aria-description`/`aria-describedby` and, if so,\nleave that attribute untouched. Otherwise we keep our generated value in sync with the text."
326
352
  },
327
353
  {
328
354
  "kind": "method",
@@ -331,6 +357,11 @@
331
357
  {
332
358
  "kind": "method",
333
359
  "name": "_renderTrailingContent"
360
+ },
361
+ {
362
+ "kind": "method",
363
+ "name": "_renderActionButton",
364
+ "description": "The interactive target for a `button` row: an empty, native `<button>` stretched invisibly\nover the whole row (see the SCSS). The item renders and owns it - rather than the consumer\nslotting one - so it can name it from its own text and keep its attributes private (it is just\nan action box, not a configurable control). Being a real button, activation is native: pointer\nclicks and keyboard (Enter/Space) both fire a `click` that bubbles out, so consumers listen for\n`click` on the `pie-list-item`. It is `type=\"button\"`, so it never submits a form."
334
365
  }
335
366
  ],
336
367
  "attributes": [
@@ -374,6 +405,13 @@
374
405
  {
375
406
  "name": "disabled",
376
407
  "fieldName": "disabled"
408
+ },
409
+ {
410
+ "name": "aria",
411
+ "type": {
412
+ "text": "ListItemProps['aria']"
413
+ },
414
+ "fieldName": "aria"
377
415
  }
378
416
  ],
379
417
  "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.4.0";
7
+ t.v = "0.5.0";
8
8
  let s = t;
9
9
  export {
10
10
  s as P
@@ -0,0 +1,339 @@
1
+ import { unsafeCSS as x, isServer as y, html as d, nothing as p } from "lit";
2
+ import { property as c, state as k } from "lit/decorators.js";
3
+ import { classMap as _ } from "lit/directives/class-map.js";
4
+ import { ifDefined as m } from "lit/directives/if-defined.js";
5
+ import { validPropertyValues as I, safeCustomElement as T, ariaContext as w, parentDisabledContext as C } from "@justeattakeaway/pie-webc-core";
6
+ import { P as A } from "./PieElement-DQQMEr70.js";
7
+ /**
8
+ * @license
9
+ * Copyright 2021 Google LLC
10
+ * SPDX-License-Identifier: BSD-3-Clause
11
+ */
12
+ let f = class extends Event {
13
+ constructor(t, e, o, s) {
14
+ super("context-request", { bubbles: !0, composed: !0 }), this.context = t, this.contextTarget = e, this.callback = o, this.subscribe = s ?? !1;
15
+ }
16
+ };
17
+ /**
18
+ * @license
19
+ * Copyright 2021 Google LLC
20
+ * SPDX-License-Identifier: BSD-3-Clause
21
+ */
22
+ let g = class {
23
+ constructor(t, e, o, s) {
24
+ if (this.subscribe = !1, this.provided = !1, this.value = void 0, this.t = (r, n) => {
25
+ this.unsubscribe && (this.unsubscribe !== n && (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, n)), this.unsubscribe = n;
26
+ }, this.host = t, e.context !== void 0) {
27
+ const r = e;
28
+ this.context = r.context, this.callback = r.callback, this.subscribe = r.subscribe ?? !1;
29
+ } else this.context = e, this.callback = o, this.subscribe = s ?? !1;
30
+ this.host.addController(this);
31
+ }
32
+ hostConnected() {
33
+ this.dispatchRequest();
34
+ }
35
+ hostDisconnected() {
36
+ this.unsubscribe && (this.unsubscribe(), this.unsubscribe = void 0);
37
+ }
38
+ dispatchRequest() {
39
+ this.host.dispatchEvent(new f(this.context, this.host, this.t, this.subscribe));
40
+ }
41
+ };
42
+ /**
43
+ * @license
44
+ * Copyright 2021 Google LLC
45
+ * SPDX-License-Identifier: BSD-3-Clause
46
+ */
47
+ class R {
48
+ get value() {
49
+ return this.o;
50
+ }
51
+ set value(t) {
52
+ this.setValue(t);
53
+ }
54
+ setValue(t, e = !1) {
55
+ const o = e || !Object.is(t, this.o);
56
+ this.o = t, o && this.updateObservers();
57
+ }
58
+ constructor(t) {
59
+ this.subscriptions = /* @__PURE__ */ new Map(), this.updateObservers = () => {
60
+ for (const [e, { disposer: o }] of this.subscriptions) e(this.o, o);
61
+ }, t !== void 0 && (this.value = t);
62
+ }
63
+ addCallback(t, e, o) {
64
+ if (!o) return void t(this.value);
65
+ this.subscriptions.has(t) || this.subscriptions.set(t, { disposer: () => {
66
+ this.subscriptions.delete(t);
67
+ }, consumerHost: e });
68
+ const { disposer: s } = this.subscriptions.get(t);
69
+ t(this.value, s);
70
+ }
71
+ clearCallbacks() {
72
+ this.subscriptions.clear();
73
+ }
74
+ }
75
+ /**
76
+ * @license
77
+ * Copyright 2021 Google LLC
78
+ * SPDX-License-Identifier: BSD-3-Clause
79
+ */
80
+ class L extends Event {
81
+ constructor(t, e) {
82
+ super("context-provider", { bubbles: !0, composed: !0 }), this.context = t, this.contextTarget = e;
83
+ }
84
+ }
85
+ class P extends R {
86
+ constructor(t, e, o) {
87
+ var s, r;
88
+ super(e.context !== void 0 ? e.initialValue : o), this.onContextRequest = (n) => {
89
+ if (n.context !== this.context) return;
90
+ const b = n.contextTarget ?? n.composedPath()[0];
91
+ b !== this.host && (n.stopPropagation(), this.addCallback(n.callback, b, n.subscribe));
92
+ }, this.onProviderRequest = (n) => {
93
+ if (n.context !== this.context || (n.contextTarget ?? n.composedPath()[0]) === this.host) return;
94
+ const b = /* @__PURE__ */ new Set();
95
+ for (const [u, { consumerHost: v }] of this.subscriptions) b.has(u) || (b.add(u), v.dispatchEvent(new f(this.context, v, u, !0)));
96
+ n.stopPropagation();
97
+ }, 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);
98
+ }
99
+ attachListeners() {
100
+ this.host.addEventListener("context-request", this.onContextRequest), this.host.addEventListener("context-provider", this.onProviderRequest);
101
+ }
102
+ hostConnected() {
103
+ this.host.dispatchEvent(new L(this.context, this.host));
104
+ }
105
+ }
106
+ /**
107
+ * @license
108
+ * Copyright 2022 Google LLC
109
+ * SPDX-License-Identifier: BSD-3-Clause
110
+ */
111
+ function B({ context: i, subscribe: t }) {
112
+ return (e, o) => {
113
+ typeof o == "object" ? o.addInitializer((function() {
114
+ new g(this, { context: i, callback: (s) => {
115
+ e.set.call(this, s);
116
+ }, subscribe: t });
117
+ })) : e.constructor.addInitializer(((s) => {
118
+ new g(s, { context: i, callback: (r) => {
119
+ s[o] = r;
120
+ }, subscribe: t });
121
+ }));
122
+ };
123
+ }
124
+ const S = ["none", "radio", "checkbox", "switch", "link", "button"], h = {
125
+ isCompact: !1,
126
+ isBold: !1,
127
+ hasMedia: !1,
128
+ interactionType: "none",
129
+ disabled: !1
130
+ }, D = "*,*: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-link,.c-listItem-container.is-button{position:relative}.c-listItem-container.is-selectable:not(.is-disabled),.c-listItem-container.is-link,.c-listItem-container.is-button: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),.c-listItem-container.is-link,.c-listItem-container.is-button: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),.c-listItem-container.is-link,.c-listItem-container.is-button:not(.is-disabled){cursor:pointer}.c-listItem-container.is-selectable:not(.is-disabled):hover,.c-listItem-container.is-link:hover,.c-listItem-container.is-button:not(.is-disabled):hover{background-color:var(--list-item-hover-bg);--interactive-container-tint: var(--list-item-hover-bg)}.c-listItem-container.is-selectable:not(.is-disabled):active,.c-listItem-container.is-link:active,.c-listItem-container.is-button:not(.is-disabled):active{background-color:var(--list-item-active-bg);--interactive-container-tint: var(--list-item-active-bg)}.c-listItem-container.is-button:has(.c-listItem-action:active){background-color:var(--list-item-active-bg);--interactive-container-tint: var(--list-item-active-bg)}slot[name=link]::slotted(a),.c-listItem-action{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;border-radius:inherit}.c-listItem-action{padding:0;border:none;background:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none;font:inherit;cursor:pointer}.c-listItem-action:disabled{cursor:default}slot[name=link]::slotted(a:focus-visible),.c-listItem-action:focus-visible{box-shadow:0 0 0 2px var(--dt-color-focus-inner),0 0 0 4px var(--dt-color-focus-outer);outline:none}.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-container.is-button.is-disabled .c-listItem-primaryText,.c-listItem-container.is-button.is-disabled .c-listItem-secondaryText,.c-listItem-container.is-button.is-disabled .c-listItem-metaText{color:var(--dt-color-content-disabled)}.c-listItem-container.is-button.is-disabled ::slotted(.c-pieIcon){color:var(--dt-color-disabled-01)}.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)}";
131
+ var $ = Object.defineProperty, z = Object.getOwnPropertyDescriptor, l = (i, t, e, o) => {
132
+ for (var s = o > 1 ? void 0 : o ? z(t, e) : t, r = i.length - 1, n; r >= 0; r--)
133
+ (n = i[r]) && (s = (o ? n(t, e, s) : n(s)) || s);
134
+ return o && s && $(t, e, s), s;
135
+ };
136
+ const q = "pie-list-item";
137
+ let a = class extends A {
138
+ constructor() {
139
+ super(...arguments), this.isCompact = h.isCompact, this.isBold = h.isBold, this.hasMedia = h.hasMedia, this.interactionType = h.interactionType, this.disabled = h.disabled, this._parentDisabled = !1, this._ariaProvider = y ? void 0 : new P(this, { context: w }), this._managedRole = null, this._linkAriaCaptured = !1, this._consumerNamedLink = !1, this._consumerDescribedLink = !1, this._handleHostClick = (i) => {
140
+ if (!this._isSelectable || this._isDisabled) return;
141
+ const t = this._control;
142
+ !t || i.composedPath().includes(t) || (t.click(), t.focus());
143
+ };
144
+ }
145
+ // A selection row hosts a slotted control (radio/checkbox/switch) and makes the whole row a
146
+ // toggle target. `link`/`button` are interactive too, but they are single navigation/action
147
+ // targets rather than selection controls, so they are deliberately excluded here.
148
+ get _isSelectable() {
149
+ return this.interactionType === "radio" || this.interactionType === "checkbox" || this.interactionType === "switch";
150
+ }
151
+ get _isLinkRow() {
152
+ return this.interactionType === "link";
153
+ }
154
+ get _isButtonRow() {
155
+ return this.interactionType === "button";
156
+ }
157
+ // A link and a button row share the same visual mechanic: an empty element stretched over the
158
+ // whole row as the interactive target, named by the item from its own text. They differ in who
159
+ // owns that element - a link is a consumer-slotted anchor, a button is one the item renders.
160
+ get _isOverlayRow() {
161
+ return this._isLinkRow || this._isButtonRow;
162
+ }
163
+ // True when the item lends its text as an interactive element's accessible name/description:
164
+ // selectable rows (radio/checkbox/switch) and overlay rows (link anchor / action button).
165
+ get _providesAria() {
166
+ return this._isSelectable || this._isOverlayRow;
167
+ }
168
+ // radio/checkbox are owned by a selection group, which is why the item becomes `presentation`
169
+ // (so the group owns the controls directly) and the radio is named on its host. A switch has
170
+ // no group, so the item stays a `listitem`.
171
+ get _ownedByGroup() {
172
+ return this.interactionType === "radio" || this.interactionType === "checkbox";
173
+ }
174
+ // True when the row should be treated as disabled: either its own `disabled` prop is set, or the
175
+ // containing group is disabled (provided via context). Declarative and reactive, so the styling
176
+ // and row-click guard react without observing the slotted control.
177
+ get _isDisabled() {
178
+ return this.disabled || this._parentDisabled;
179
+ }
180
+ // The interactive control (radio/checkbox/switch) slotted into this item, if any. Used only by
181
+ // the client-side row-click handler.
182
+ get _control() {
183
+ return this.querySelector("pie-radio, pie-checkbox, pie-switch");
184
+ }
185
+ connectedCallback() {
186
+ super.connectedCallback(), this._abortController = new AbortController(), this.addEventListener("click", this._handleHostClick, { signal: this._abortController.signal });
187
+ }
188
+ disconnectedCallback() {
189
+ super.disconnectedCallback(), this._abortController.abort();
190
+ }
191
+ updated() {
192
+ var i;
193
+ this._applyRole(), (i = this._ariaProvider) == null || i.setValue(this._providedAria), this._applyLinkAria();
194
+ }
195
+ /**
196
+ * Sets the item's role from `interactionType`: `presentation` for radio/checkbox (so the group
197
+ * owns those controls directly), otherwise `listitem` (static items, switches, links and
198
+ * buttons). A role set explicitly by the consumer is left untouched, even if it is added or
199
+ * removed dynamically after the element connects.
200
+ */
201
+ _applyRole() {
202
+ const i = this.getAttribute("role");
203
+ if (i !== null && i !== this._managedRole) return;
204
+ const t = this._ownedByGroup ? "presentation" : "listitem";
205
+ this._managedRole = t, this.setAttribute("role", t);
206
+ }
207
+ /**
208
+ * The accessible name and description this item provides to its interactive element, stitched
209
+ * from its text. Selection controls consume it via `ariaContext` (each applying it to the
210
+ * element that carries its role); the action button of a `button` row takes it directly as
211
+ * `aria-label`/`aria-description` in the template; a link anchor takes it via `_applyLinkAria`.
212
+ */
213
+ get _providedAria() {
214
+ if (!this._providesAria) return;
215
+ const i = [this.secondaryText, this.metaText].filter(Boolean).join(". ");
216
+ return { label: this.primaryText, description: i || void 0 };
217
+ }
218
+ /**
219
+ * On a link row, names the slotted (empty) anchor from the item's own text: the parent owns the
220
+ * child's aria. The anchor is plain light-DOM, so it cannot reference the shadow-DOM text via
221
+ * `aria-labelledby`; and it is not a PIE control that consumes `ariaContext`, so we set the
222
+ * attributes imperatively. The visible text is `aria-hidden` in the template, so nothing is
223
+ * announced twice. (A `button` row names its own rendered element in the template instead.)
224
+ *
225
+ * A consumer-provided name/description always wins: we capture (once) whether the anchor already
226
+ * carries `aria-label`/`aria-labelledby` or `aria-description`/`aria-describedby` and, if so,
227
+ * leave that attribute untouched. Otherwise we keep our generated value in sync with the text.
228
+ */
229
+ _applyLinkAria() {
230
+ if (!this._isLinkRow) {
231
+ if (this._linkAriaCaptured) {
232
+ const e = this.querySelector('a[slot="link"]');
233
+ e && (this._consumerNamedLink || e.removeAttribute("aria-label"), this._consumerDescribedLink || e.removeAttribute("aria-description")), this._linkAriaCaptured = !1, this._consumerNamedLink = !1, this._consumerDescribedLink = !1;
234
+ }
235
+ return;
236
+ }
237
+ const i = this.querySelector('a[slot="link"]');
238
+ if (!i) return;
239
+ this._linkAriaCaptured || (this._consumerNamedLink = i.hasAttribute("aria-label") || i.hasAttribute("aria-labelledby"), this._consumerDescribedLink = i.hasAttribute("aria-description") || i.hasAttribute("aria-describedby"), this._linkAriaCaptured = !0);
240
+ const t = this._providedAria;
241
+ this._consumerNamedLink || (t != null && t.label ? i.setAttribute("aria-label", t.label) : i.removeAttribute("aria-label")), this._consumerDescribedLink || (t != null && t.description ? i.setAttribute("aria-description", t.description) : i.removeAttribute("aria-description"));
242
+ }
243
+ _renderSecondaryText() {
244
+ const { secondaryText: i } = this;
245
+ return i ? d`<span class="c-listItem-secondaryText">${i}</span>` : p;
246
+ }
247
+ // metaText is another form of trailing content. The component can only ever display either slotted trailing content or the metaText string
248
+ _renderTrailingContent() {
249
+ const { metaText: i } = this;
250
+ return i ? d`<span class="c-listItem-metaText c-listItem-trailing" aria-hidden=${this._providesAria ? "true" : p}>${i}</span>` : d`<div class="c-listItem-trailing"><slot name="trailing"></slot></div>`;
251
+ }
252
+ /**
253
+ * The interactive target for a `button` row: an empty, native `<button>` stretched invisibly
254
+ * over the whole row (see the SCSS). The item renders and owns it - rather than the consumer
255
+ * slotting one - so it can name it from its own text and keep its attributes private (it is just
256
+ * an action box, not a configurable control). Being a real button, activation is native: pointer
257
+ * clicks and keyboard (Enter/Space) both fire a `click` that bubbles out, so consumers listen for
258
+ * `click` on the `pie-list-item`. It is `type="button"`, so it never submits a form.
259
+ */
260
+ _renderActionButton() {
261
+ var t, e;
262
+ const i = this._providedAria;
263
+ return d`<button
264
+ class="c-listItem-action"
265
+ type="button"
266
+ ?disabled=${this._isDisabled}
267
+ aria-label=${m(i == null ? void 0 : i.label)}
268
+ aria-description=${m(i == null ? void 0 : i.description)}
269
+ aria-haspopup=${m((e = (t = this.aria) == null ? void 0 : t.button) == null ? void 0 : e.haspopup)}></button>`;
270
+ }
271
+ render() {
272
+ const { primaryText: i } = this, t = {
273
+ "c-listItem-container": !0,
274
+ "is-compact": this.isCompact,
275
+ "is-bold": this.isBold,
276
+ "has-media": this.hasMedia,
277
+ "is-selectable": this._isSelectable,
278
+ "is-link": this._isLinkRow,
279
+ "is-button": this._isButtonRow,
280
+ "is-disabled": this._isDisabled
281
+ };
282
+ return d`
283
+ <div class=${_(t)}>
284
+ ${this._isLinkRow ? d`<slot name="link"></slot>` : p}
285
+ ${this._isButtonRow ? this._renderActionButton() : p}
286
+ <div class="c-listItem-leading">
287
+ <slot name="leading"></slot>
288
+ </div>
289
+
290
+ <div class="c-listItem-text" aria-hidden=${this._providesAria ? "true" : p}>
291
+ <span class="c-listItem-primaryText">${i}</span>
292
+ ${this._renderSecondaryText()}
293
+ </div>
294
+
295
+ ${this._renderTrailingContent()}
296
+ </div>`;
297
+ }
298
+ };
299
+ a.styles = x(D);
300
+ l([
301
+ c({ type: String })
302
+ ], a.prototype, "primaryText", 2);
303
+ l([
304
+ c({ type: String })
305
+ ], a.prototype, "secondaryText", 2);
306
+ l([
307
+ c({ type: String })
308
+ ], a.prototype, "metaText", 2);
309
+ l([
310
+ c({ type: Boolean })
311
+ ], a.prototype, "isCompact", 2);
312
+ l([
313
+ c({ type: Boolean })
314
+ ], a.prototype, "isBold", 2);
315
+ l([
316
+ c({ type: Boolean })
317
+ ], a.prototype, "hasMedia", 2);
318
+ l([
319
+ c({ type: String }),
320
+ I(q, S, h.interactionType)
321
+ ], a.prototype, "interactionType", 2);
322
+ l([
323
+ c({ type: Boolean })
324
+ ], a.prototype, "disabled", 2);
325
+ l([
326
+ c({ type: Object })
327
+ ], a.prototype, "aria", 2);
328
+ l([
329
+ B({ context: C, subscribe: !0 }),
330
+ k()
331
+ ], a.prototype, "_parentDisabled", 2);
332
+ a = l([
333
+ T("pie-list-item")
334
+ ], a);
335
+ export {
336
+ a as P,
337
+ h as d,
338
+ S as i
339
+ };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { html as n, unsafeCSS as m } from "lit";
2
- import { P as f } from "./PieElement-CGtUo9tK.js";
2
+ import { P as f } from "./PieElement-DQQMEr70.js";
3
3
  import { RtlMixin as d, safeCustomElement as p } from "@justeattakeaway/pie-webc-core";
4
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
5
  var b = Object.getOwnPropertyDescriptor, u = (s, r, a, l) => {