@justeattakeaway/pie-radio-group 1.1.5 → 1.2.1

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
@@ -34,7 +34,7 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc
34
34
  | Prop | Options | Description | Default |
35
35
  |----------------|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|-------------|
36
36
  | `name` | — | The name associated with the group. | `undefined` |
37
- | `value` | — | The value of the radio group (used as a key/value pair in HTML forms with `name`). | `""` |
37
+ | `value` | — | Selects the radio whose `value` matches (unselecting the others), and is the group's form value alongside `name`. Set this to pre-select an option. | `""` |
38
38
  | `isInline` | `true`, `false` | Inline (horizontal) positioning of radio items. | `false` |
39
39
  | `disabled` | `true`, `false` | Indicates whether or not the radio group is disabled. | `false` |
40
40
  | `assistiveText`| — | An optional assistive text to display below the checkbox group. | `undefined` |
@@ -43,8 +43,8 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc
43
43
  ### Slots
44
44
  | Slot | Description |
45
45
  |-----------|--------------------------------------------------------------------------------------------------|
46
- | `default` | This should be [`pie-radio-button`](/components/radio) components without any other HTML. |
47
- | `label` | To provide a custom label for the radio group. Please use [`pie-form-label`](/components/form-label). |
46
+ | `default` | Either [`pie-radio`](https://webc.pie.design/?path=/docs/components-radio--overview) components, or [`pie-list-item`](https://webc.pie.design/?path=/docs/components-list--overview) components that each slot a `pie-radio` for a list-style layout (see [List items in a radio group](#list-items-in-a-radio-group)). Do not add other HTML. |
47
+ | `label` | To provide a custom label for the radio group. Please use [`pie-form-label`](https://webc.pie.design/?path=/docs/components-form-label--overview). |
48
48
 
49
49
  ### CSS Variables
50
50
  This component does not expose any CSS variables for style overrides.
@@ -116,6 +116,44 @@ import { PieFormLabel } from '@justeattakeaway/pie-webc/react/form-label.js';
116
116
  </PieRadioGroup>
117
117
  ```
118
118
 
119
+ ### List items in a radio group
120
+
121
+ For a list-style layout (each option on its own row with primary and secondary text, and dividers between rows), wrap each `pie-radio` in a [`pie-list-item`](https://webc.pie.design/?path=/docs/components-list--overview) with `selection-type="radio"`. The radio goes in the item's `leading` (or `trailing`) slot, and you provide the label through the item's `primaryText` rather than as the radio's content. The group lays the rows out as a divided list automatically when its children are `pie-list-item`s.
122
+
123
+ `selection-type="radio"` on each **item** makes that row a selectable target:
124
+
125
+ - the item takes a `presentation` role, so the group still owns its `radio` children directly;
126
+ - the item's `primaryText` becomes the radio's accessible name (`aria-label`), and its `secondaryText` and `metaText` its `aria-description`, so you do not pass label text to the radio itself;
127
+ - the visible item text is hidden from assistive technology so it is not announced twice;
128
+ - clicking anywhere on a row selects that row's radio.
129
+
130
+ Keyboard navigation, focus management and selection are identical to a standard radio group.
131
+
132
+ ```js
133
+ import '@justeattakeaway/pie-webc/components/radio-group.js';
134
+ import '@justeattakeaway/pie-webc/components/radio.js';
135
+ import '@justeattakeaway/pie-webc/components/form-label.js';
136
+ import '@justeattakeaway/pie-webc/components/list-item.js';
137
+ ```
138
+
139
+ ```html
140
+ <pie-radio-group name="delivery">
141
+ <pie-form-label slot="label">Delivery method</pie-form-label>
142
+ <pie-list-item selection-type="radio" primaryText="Standard delivery" secondaryText="3 to 5 working days" metaText="Free">
143
+ <pie-radio slot="leading" value="standard"></pie-radio>
144
+ </pie-list-item>
145
+ <pie-list-item selection-type="radio" primaryText="Express delivery" secondaryText="Next working day" metaText="£4.99">
146
+ <pie-radio slot="leading" value="express"></pie-radio>
147
+ </pie-list-item>
148
+ <pie-list-item selection-type="radio" disabled primaryText="Collection" secondaryText="Collect from a nearby store">
149
+ <pie-radio slot="leading" value="collection" disabled></pie-radio>
150
+ </pie-list-item>
151
+ <pie-list-item selection-type="radio" primaryText="Locker" secondaryText="Pick up from a parcel locker" metaText="£1.99">
152
+ <pie-radio slot="leading" value="locker"></pie-radio>
153
+ </pie-list-item>
154
+ </pie-radio-group>
155
+ ```
156
+
119
157
  ## Questions and Support
120
158
 
121
159
  If you work at Just Eat Takeaway.com, please contact us on **#help-designsystem**. Otherwise, please raise an issue on [Github](https://github.com/justeattakeaway/pie/issues).
@@ -140,6 +140,11 @@
140
140
  "attribute": "disabled",
141
141
  "reflects": true
142
142
  },
143
+ {
144
+ "kind": "field",
145
+ "name": "_disabledProvider",
146
+ "privacy": "private"
147
+ },
143
148
  {
144
149
  "kind": "field",
145
150
  "name": "assistiveText",
@@ -157,19 +162,30 @@
157
162
  },
158
163
  {
159
164
  "kind": "field",
160
- "name": "_slottedChildren",
165
+ "name": "_abortController",
161
166
  "type": {
162
- "text": "Array<HTMLInputElement>"
163
- }
167
+ "text": "AbortController"
168
+ },
169
+ "privacy": "private"
164
170
  },
165
171
  {
166
172
  "kind": "field",
167
- "name": "_abortController",
173
+ "name": "_mutationObserver",
168
174
  "type": {
169
- "text": "AbortController"
175
+ "text": "MutationObserver"
170
176
  },
171
177
  "privacy": "private"
172
178
  },
179
+ {
180
+ "kind": "field",
181
+ "name": "_slottedChildren",
182
+ "type": {
183
+ "text": "HTMLInputElement[]"
184
+ },
185
+ "privacy": "private",
186
+ "description": "The radios in the group. This uses a subtree query rather than immediate slotted\nchildren so radios wrapped in `pie-list-item`s (at any depth) are discovered. It is\na superset of the previous immediate-child query, so radios passed as direct children\nkeep working unchanged.",
187
+ "readonly": true
188
+ },
173
189
  {
174
190
  "kind": "method",
175
191
  "name": "_handleDisabled",
package/dist/index.d.ts CHANGED
@@ -32,10 +32,18 @@ export declare class PieRadioGroup extends PieRadioGroup_base implements RadioGr
32
32
  value: string;
33
33
  isInline: boolean;
34
34
  disabled: boolean;
35
+ private _disabledProvider;
35
36
  assistiveText?: RadioGroupProps['assistiveText'];
36
37
  status: "default" | "success" | "error";
37
- _slottedChildren: Array<HTMLInputElement>;
38
38
  private _abortController;
39
+ private _mutationObserver;
40
+ /**
41
+ * The radios in the group. This uses a subtree query rather than immediate slotted
42
+ * children so radios wrapped in `pie-list-item`s (at any depth) are discovered. It is
43
+ * a superset of the previous immediate-child query, so radios passed as direct children
44
+ * keep working unchanged.
45
+ */
46
+ private get _slottedChildren();
39
47
  /**
40
48
  * Dispatches a custom event to notify each slotted child radio element
41
49
  * when the radio group is disabled.
package/dist/index.js CHANGED
@@ -1,31 +1,114 @@
1
- import { LitElement as v, html as u, nothing as g, unsafeCSS as C } from "lit";
2
- import { state as b, property as h, queryAssignedElements as y } from "lit/decorators.js";
3
- import { FormControlMixin as x, RtlMixin as w, wrapNativeEvent as T, validPropertyValues as I, safeCustomElement as S } from "@justeattakeaway/pie-webc-core";
4
- import { ifDefined as R } from "lit/directives/if-defined.js";
5
- import { classMap as E } from "lit/directives/class-map.js";
1
+ import { LitElement as x, isServer as C, html as p, nothing as m, unsafeCSS as y } from "lit";
2
+ import { state as _, property as u } from "lit/decorators.js";
3
+ import { FormControlMixin as w, RtlMixin as T, parentDisabledContext as R, wrapNativeEvent as S, validPropertyValues as E, safeCustomElement as I } from "@justeattakeaway/pie-webc-core";
4
+ import { ifDefined as L } from "lit/directives/if-defined.js";
5
+ import { classMap as O } from "lit/directives/class-map.js";
6
6
  import "@justeattakeaway/pie-assistive-text";
7
- const p = class p extends v {
7
+ const b = class b extends x {
8
8
  willUpdate() {
9
- this.getAttribute("v") || this.setAttribute("v", p.v);
9
+ this.getAttribute("v") || this.setAttribute("v", b.v);
10
10
  }
11
11
  };
12
- p.v = "1.1.5";
13
- let _ = p;
14
- const A = "*,*:after,*:before{box-sizing:inherit}:host{display:block}.c-radioGroup{--radio-group-gap: var(--dt-spacing-c);--radio-group-gap--inline: var(--dt-spacing-c) var(--dt-spacing-e);margin:0;padding:0;border:0;min-width:0;display:flex;flex-flow:column wrap;gap:var(--radio-group-gap)}.c-radioGroup>legend{margin-block-end:var(--dt-spacing-b)}.c-radioGroup.c-radioGroup--inline{flex-flow:row wrap;gap:var(--radio-group-gap--inline)}.c-radioGroup.c-radioGroup--hasAssistiveText{margin-block-end:var(--dt-spacing-a)}", L = ["default", "success", "error"], N = "pie-radio-group-disabled", c = {
12
+ b.v = "1.2.1";
13
+ let f = b;
14
+ /**
15
+ * @license
16
+ * Copyright 2021 Google LLC
17
+ * SPDX-License-Identifier: BSD-3-Clause
18
+ */
19
+ let A = class extends Event {
20
+ constructor(e, s, i, o) {
21
+ super("context-request", { bubbles: !0, composed: !0 }), this.context = e, this.contextTarget = s, this.callback = i, this.subscribe = o ?? !1;
22
+ }
23
+ };
24
+ /**
25
+ * @license
26
+ * Copyright 2021 Google LLC
27
+ * SPDX-License-Identifier: BSD-3-Clause
28
+ */
29
+ class D {
30
+ get value() {
31
+ return this.o;
32
+ }
33
+ set value(e) {
34
+ this.setValue(e);
35
+ }
36
+ setValue(e, s = !1) {
37
+ const i = s || !Object.is(e, this.o);
38
+ this.o = e, i && this.updateObservers();
39
+ }
40
+ constructor(e) {
41
+ this.subscriptions = /* @__PURE__ */ new Map(), this.updateObservers = () => {
42
+ for (const [s, { disposer: i }] of this.subscriptions) s(this.o, i);
43
+ }, e !== void 0 && (this.value = e);
44
+ }
45
+ addCallback(e, s, i) {
46
+ if (!i) return void e(this.value);
47
+ this.subscriptions.has(e) || this.subscriptions.set(e, { disposer: () => {
48
+ this.subscriptions.delete(e);
49
+ }, consumerHost: s });
50
+ const { disposer: o } = this.subscriptions.get(e);
51
+ e(this.value, o);
52
+ }
53
+ clearCallbacks() {
54
+ this.subscriptions.clear();
55
+ }
56
+ }
57
+ /**
58
+ * @license
59
+ * Copyright 2021 Google LLC
60
+ * SPDX-License-Identifier: BSD-3-Clause
61
+ */
62
+ class N extends Event {
63
+ constructor(e, s) {
64
+ super("context-provider", { bubbles: !0, composed: !0 }), this.context = e, this.contextTarget = s;
65
+ }
66
+ }
67
+ class F extends D {
68
+ constructor(e, s, i) {
69
+ var o, r;
70
+ super(s.context !== void 0 ? s.initialValue : i), this.onContextRequest = (a) => {
71
+ if (a.context !== this.context) return;
72
+ const n = a.contextTarget ?? a.composedPath()[0];
73
+ n !== this.host && (a.stopPropagation(), this.addCallback(a.callback, n, a.subscribe));
74
+ }, this.onProviderRequest = (a) => {
75
+ if (a.context !== this.context || (a.contextTarget ?? a.composedPath()[0]) === this.host) return;
76
+ const n = /* @__PURE__ */ new Set();
77
+ for (const [h, { consumerHost: v }] of this.subscriptions) n.has(h) || (n.add(h), v.dispatchEvent(new A(this.context, v, h, !0)));
78
+ a.stopPropagation();
79
+ }, this.host = e, s.context !== void 0 ? this.context = s.context : this.context = s, this.attachListeners(), (r = (o = this.host).addController) == null || r.call(o, this);
80
+ }
81
+ attachListeners() {
82
+ this.host.addEventListener("context-request", this.onContextRequest), this.host.addEventListener("context-provider", this.onProviderRequest);
83
+ }
84
+ hostConnected() {
85
+ this.host.dispatchEvent(new N(this.context, this.host));
86
+ }
87
+ }
88
+ const P = "*,*: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)}.c-radioGroup{--radio-group-gap: var(--dt-spacing-c);--radio-group-gap--inline: var(--dt-spacing-c) var(--dt-spacing-e);margin:0;padding:0;border:0;min-width:0;display:flex;flex-flow:column wrap}.c-radioGroup>legend{margin-block-end:var(--dt-spacing-b)}.c-radioGroup.c-radioGroup--inline{flex-flow:row wrap;gap:var(--radio-group-gap--inline)}.c-radioGroup.c-radioGroup--hasAssistiveText{margin-block-end:var(--dt-spacing-a)}.c-radioGroup:not(.c-radioGroup--inline) ::slotted(pie-radio:not(:first-of-type)){margin-block-start:var(--radio-group-gap)}", G = ["default", "success", "error"], $ = "pie-radio-group-disabled", c = {
15
89
  status: "default",
16
90
  disabled: !1,
17
91
  isInline: !1,
18
92
  value: ""
19
93
  };
20
- var D = Object.defineProperty, O = Object.getOwnPropertyDescriptor, r = (e, t, i, s) => {
21
- for (var o = s > 1 ? void 0 : s ? O(t, i) : t, l = e.length - 1, n; l >= 0; l--)
22
- (n = e[l]) && (o = (s ? n(t, i, o) : n(o)) || o);
23
- return s && o && D(t, i, o), o;
94
+ var k = Object.defineProperty, B = Object.getOwnPropertyDescriptor, d = (t, e, s, i) => {
95
+ for (var o = i > 1 ? void 0 : i ? B(e, s) : e, r = t.length - 1, a; r >= 0; r--)
96
+ (a = t[r]) && (o = (i ? a(e, s, o) : a(o)) || o);
97
+ return i && o && k(e, s, o), o;
24
98
  };
25
- const F = "pie-radio-group", m = "assistive-text";
26
- let a = class extends x(w(_)) {
99
+ const q = "pie-radio-group", g = "assistive-text";
100
+ let l = class extends w(T(f)) {
27
101
  constructor() {
28
- super(...arguments), this._hasLabel = !1, this._fieldSetTabIndex = 0, this._allRadiosDisabled = !1, this.value = c.value, this.isInline = c.isInline, this.disabled = c.disabled, this.status = c.status;
102
+ super(...arguments), this._hasLabel = !1, this._fieldSetTabIndex = 0, this._allRadiosDisabled = !1, this.value = c.value, this.isInline = c.isInline, this.disabled = c.disabled, this._disabledProvider = C ? void 0 : new F(this, { context: R, initialValue: c.disabled }), this.status = c.status;
103
+ }
104
+ /**
105
+ * The radios in the group. This uses a subtree query rather than immediate slotted
106
+ * children so radios wrapped in `pie-list-item`s (at any depth) are discovered. It is
107
+ * a superset of the previous immediate-child query, so radios passed as direct children
108
+ * keep working unchanged.
109
+ */
110
+ get _slottedChildren() {
111
+ return Array.from(this.querySelectorAll("pie-radio"));
29
112
  }
30
113
  /**
31
114
  * Dispatches a custom event to notify each slotted child radio element
@@ -33,11 +116,11 @@ let a = class extends x(w(_)) {
33
116
  * @private
34
117
  */
35
118
  _handleDisabled() {
36
- this._slottedChildren.forEach((e) => e.dispatchEvent(new CustomEvent(N, {
119
+ this._slottedChildren.forEach((t) => t.dispatchEvent(new CustomEvent($, {
37
120
  bubbles: !1,
38
121
  composed: !1,
39
122
  detail: { disabled: this.disabled }
40
- }))), this._allRadiosDisabled = this._slottedChildren.every((e) => e.disabled);
123
+ }))), this._allRadiosDisabled = this._slottedChildren.every((t) => t.disabled);
41
124
  }
42
125
  /**
43
126
  * Updates the `status` attribute of all slotted children.
@@ -45,16 +128,16 @@ let a = class extends x(w(_)) {
45
128
  * @returns {void}
46
129
  */
47
130
  _handleStatus() {
48
- this._slottedChildren.forEach((e) => e.setAttribute("status", this.status === "error" ? "error" : "default"));
131
+ this._slottedChildren.forEach((t) => t.setAttribute("status", this.status === "error" ? "error" : "default"));
49
132
  }
50
133
  /**
51
134
  * Unselects all radios that are not the selected value.
52
135
  * @param {string} selectedValue - The value of the currently selected radio.
53
136
  * @private
54
137
  */
55
- _handleRadioSelection(e) {
56
- this.value = e, this._slottedChildren.forEach((t) => {
57
- t.checked = t.value === this.value;
138
+ _handleRadioSelection(t) {
139
+ this.value = t, this._slottedChildren.forEach((e) => {
140
+ e.checked = e.value === this.value;
58
141
  });
59
142
  }
60
143
  /**
@@ -62,21 +145,21 @@ let a = class extends x(w(_)) {
62
145
  * @param {Event} event - The change event from a radio element.
63
146
  * @private
64
147
  */
65
- _handleRadioChange(e) {
66
- e.stopPropagation();
67
- const t = e.target;
68
- this._handleRadioSelection(t.value), this._resetButtonsTabIndex();
69
- const i = T(e);
70
- this.dispatchEvent(i);
148
+ _handleRadioChange(t) {
149
+ t.stopPropagation();
150
+ const e = t.target;
151
+ this._handleRadioSelection(e.value), this._resetButtonsTabIndex();
152
+ const s = S(t);
153
+ this.dispatchEvent(s);
71
154
  }
72
155
  /**
73
156
  * Updates the `_hasLabel` state when content is added to the label slot.
74
157
  * @param {Event} e - The slotchange event.
75
158
  * @private
76
159
  */
77
- _handleLabelSlotChange(e) {
78
- const t = e.target.assignedNodes({ flatten: !0 });
79
- this._hasLabel = t.length > 0;
160
+ _handleLabelSlotChange(t) {
161
+ const e = t.target.assignedNodes({ flatten: !0 });
162
+ this._hasLabel = e.length > 0;
80
163
  }
81
164
  /**
82
165
  * Ensures all newly added radio buttons are tabbable and inherit the name property
@@ -88,8 +171,8 @@ let a = class extends x(w(_)) {
88
171
  * Prevent radio buttons from receiving focus
89
172
  */
90
173
  _resetButtonsTabIndex() {
91
- this._slottedChildren.forEach((e) => {
92
- e.tabIndex = -1;
174
+ this._slottedChildren.forEach((t) => {
175
+ t.tabIndex = -1;
93
176
  });
94
177
  }
95
178
  /**
@@ -98,42 +181,43 @@ let a = class extends x(w(_)) {
98
181
  * @private
99
182
  */
100
183
  _renderWrappedLabel() {
101
- return this._hasLabel ? u`<legend><slot name='label' @slotchange=${this._handleLabelSlotChange}></slot></legend>` : u`<slot name='label' @slotchange=${this._handleLabelSlotChange}></slot>`;
184
+ return this._hasLabel ? p`<legend><slot name='label' @slotchange=${this._handleLabelSlotChange}></slot></legend>` : p`<slot name='label' @slotchange=${this._handleLabelSlotChange}></slot>`;
102
185
  }
103
186
  _applyNameToChildren() {
104
- this._slottedChildren.forEach((e) => {
105
- this.name && e.setAttribute("name", this.name);
187
+ this._slottedChildren.forEach((t) => {
188
+ this.name && t.setAttribute("name", this.name);
106
189
  });
107
190
  }
108
- updated(e) {
109
- e.has("disabled") && this._handleDisabled(), e.has("value") && this._handleRadioSelection(this.value), e.has("status") && this._handleStatus(), e.has("name") && this._applyNameToChildren();
191
+ updated(t) {
192
+ var e;
193
+ t.has("disabled") && ((e = this._disabledProvider) == null || e.setValue(this.disabled), this._handleDisabled()), t.has("value") && this._handleRadioSelection(this.value), t.has("status") && this._handleStatus(), t.has("name") && this._applyNameToChildren();
110
194
  }
111
195
  connectedCallback() {
112
- var t;
196
+ var e;
113
197
  super.connectedCallback(), this._abortController = new AbortController();
114
- const { signal: e } = this._abortController;
115
- this.addEventListener("focus", this._handleFocus, { signal: e }), this.addEventListener("focusout", this._handleFocusOut, { signal: e }), (t = this.shadowRoot) == null || t.addEventListener("change", this._handleRadioChange.bind(this), { signal: e }), this.addEventListener("keydown", this._handleKeyDown, { signal: e }), this._applyNameToChildren();
198
+ const { signal: t } = this._abortController;
199
+ this.addEventListener("focus", this._handleFocus, { signal: t }), this.addEventListener("focusout", this._handleFocusOut, { signal: t }), (e = this.shadowRoot) == null || e.addEventListener("change", this._handleRadioChange.bind(this), { signal: t }), this.addEventListener("keydown", this._handleKeyDown, { signal: t }), this._applyNameToChildren(), this._mutationObserver = new MutationObserver(() => this._handleRadioSlotChange()), this._mutationObserver.observe(this, { childList: !0, subtree: !0 });
116
200
  }
117
201
  /**
118
202
  * This method determines the appropriate element to focus when the radio group
119
203
  * gains focus. It considers where the focus is coming from, and focuses the
120
204
  * the first or the last option as needed.
121
205
  */
122
- _focusNearestAvailableRadio(e, t) {
123
- const i = t.relatedTarget || this.parentNode, l = this.compareDocumentPosition(i) & Node.DOCUMENT_POSITION_PRECEDING ? e.shift() : e.pop();
124
- l && (l.tabIndex = 0, l.focus()), e.forEach((n) => {
125
- n.tabIndex = -1;
206
+ _focusNearestAvailableRadio(t, e) {
207
+ const s = e.relatedTarget || this.parentNode, r = this.compareDocumentPosition(s) & Node.DOCUMENT_POSITION_PRECEDING ? t.shift() : t.pop();
208
+ r && (r.tabIndex = 0, r.focus()), t.forEach((a) => {
209
+ a.tabIndex = -1;
126
210
  });
127
211
  }
128
212
  /**
129
213
  * Handles the `focus` event to manage focus within the radio group.
130
214
  *
131
215
  */
132
- _handleFocus(e) {
133
- const t = this._slottedChildren.filter((s) => !s.disabled);
134
- if (!t || t.length === 0) return;
135
- this.value === "" || this.value === void 0 ? this._focusNearestAvailableRadio(t, e) : this._slottedChildren.forEach((s) => {
136
- s.value === this.value ? s.disabled ? this._focusNearestAvailableRadio(t, e) : (s.tabIndex = 0, s.focus()) : s.tabIndex = -1;
216
+ _handleFocus(t) {
217
+ const e = this._slottedChildren.filter((i) => !i.disabled);
218
+ if (!e || e.length === 0) return;
219
+ this.value === "" || this.value === void 0 ? this._focusNearestAvailableRadio(e, t) : this._slottedChildren.forEach((i) => {
220
+ i.value === this.value ? i.disabled ? this._focusNearestAvailableRadio(e, t) : (i.tabIndex = 0, i.focus()) : i.tabIndex = -1;
137
221
  });
138
222
  }
139
223
  /**
@@ -148,24 +232,24 @@ let a = class extends x(w(_)) {
148
232
  * to the correspondent radio, so ir can be re-focused when tabbing back into
149
233
  * the group.
150
234
  */
151
- _handleFocusOut(e) {
152
- const t = e.relatedTarget;
153
- if (!t) {
235
+ _handleFocusOut(t) {
236
+ const e = t.relatedTarget;
237
+ if (!e) {
154
238
  this._resetButtonsTabIndex(), this._fieldSetTabIndex = 0;
155
239
  return;
156
240
  }
157
- (t.compareDocumentPosition(this) & Node.DOCUMENT_POSITION_CONTAINS) === 0 ? (this._resetButtonsTabIndex(), this._fieldSetTabIndex = 0) : this._fieldSetTabIndex = -1;
241
+ (e.compareDocumentPosition(this) & Node.DOCUMENT_POSITION_CONTAINS) === 0 ? (this._resetButtonsTabIndex(), this._fieldSetTabIndex = 0) : this._fieldSetTabIndex = -1;
158
242
  }
159
- _moveFocus(e, t) {
160
- var n, d;
161
- if (!this._slottedChildren.filter((f) => !f.disabled)) return;
162
- const s = (e + t + this._slottedChildren.length) % this._slottedChildren.length, o = this._slottedChildren[s];
243
+ _moveFocus(t, e) {
244
+ var a, n;
245
+ if (!this._slottedChildren.filter((h) => !h.disabled)) return;
246
+ const i = (t + e + this._slottedChildren.length) % this._slottedChildren.length, o = this._slottedChildren[i];
163
247
  if (o.disabled) {
164
- t === 0 ? this._moveFocus(s, 1) : this._moveFocus(s, t);
248
+ e === 0 ? this._moveFocus(i, 1) : this._moveFocus(i, e);
165
249
  return;
166
250
  }
167
- const l = o;
168
- l.focus(), (d = (n = l.shadowRoot) == null ? void 0 : n.querySelector("input")) == null || d.click();
251
+ const r = o;
252
+ r.focus(), (n = (a = r.shadowRoot) == null ? void 0 : a.querySelector("input")) == null || n.click();
169
253
  }
170
254
  /**
171
255
  * Determines if a key press indicates forward navigation within the radio group.
@@ -179,8 +263,8 @@ let a = class extends x(w(_)) {
179
263
  * - For RTL (Right-to-Left) layouts:
180
264
  * - `ArrowLeft` and `ArrowDown` indicate forward navigation.
181
265
  */
182
- _isForwardKey(e) {
183
- return e.code === "ArrowRight" && !this.isRTL || e.code === "ArrowLeft" && this.isRTL || e.code === "ArrowDown";
266
+ _isForwardKey(t) {
267
+ return t.code === "ArrowRight" && !this.isRTL || t.code === "ArrowLeft" && this.isRTL || t.code === "ArrowDown";
184
268
  }
185
269
  /**
186
270
  * Determines if a key press indicates backward navigation within the radio group.
@@ -194,8 +278,8 @@ let a = class extends x(w(_)) {
194
278
  * - For RTL (Right-to-Left) layouts:
195
279
  * - `ArrowRight` and `ArrowUp` indicate backward navigation.
196
280
  */
197
- _isBackwardKey(e) {
198
- return e.code === "ArrowLeft" && !this.isRTL || e.code === "ArrowRight" && this.isRTL || e.code === "ArrowUp";
281
+ _isBackwardKey(t) {
282
+ return t.code === "ArrowLeft" && !this.isRTL || t.code === "ArrowRight" && this.isRTL || t.code === "ArrowUp";
199
283
  }
200
284
  /**
201
285
  * Handles keyboard navigation within the radio group using arrow keys.
@@ -204,90 +288,88 @@ let a = class extends x(w(_)) {
204
288
  * action (forward or backward) based on the pressed key and the current focus. It prevents
205
289
  * the default browser behaviour (e.g., scrolling) when arrow keys are used for navigation.
206
290
  */
207
- _handleKeyDown(e) {
208
- const t = this._slottedChildren.find((s) => s === document.activeElement);
209
- if (!t)
291
+ _handleKeyDown(t) {
292
+ const e = this._slottedChildren.find((i) => i === document.activeElement);
293
+ if (!e)
210
294
  return;
211
- const i = this._slottedChildren.indexOf(t);
212
- i !== -1 && (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(e.code) && e.preventDefault(), this._isForwardKey(e) ? this._moveFocus(i, 1) : this._isBackwardKey(e) ? this._moveFocus(i, -1) : e.code === "Space" && this._moveFocus(i, 0));
295
+ const s = this._slottedChildren.indexOf(e);
296
+ s !== -1 && (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(t.code) && t.preventDefault(), this._isForwardKey(t) ? this._moveFocus(s, 1) : this._isBackwardKey(t) ? this._moveFocus(s, -1) : t.code === "Space" && this._moveFocus(s, 0));
213
297
  }
214
298
  disconnectedCallback() {
215
- super.disconnectedCallback(), this._abortController.abort();
299
+ var t;
300
+ super.disconnectedCallback(), this._abortController.abort(), (t = this._mutationObserver) == null || t.disconnect();
216
301
  }
217
302
  render() {
218
303
  const {
219
- name: e,
220
- isInline: t,
221
- disabled: i,
222
- status: s,
304
+ name: t,
305
+ isInline: e,
306
+ disabled: s,
307
+ status: i,
223
308
  assistiveText: o,
224
- _fieldSetTabIndex: l,
225
- _allRadiosDisabled: n
226
- } = this, d = !!(o != null && o.length), f = {
309
+ _fieldSetTabIndex: r,
310
+ _allRadiosDisabled: a
311
+ } = this, n = !!(o != null && o.length), h = {
227
312
  "c-radioGroup": !0,
228
- "c-radioGroup--inline": t,
229
- "c-radioGroup--hasAssistiveText": d
313
+ "c-radioGroup--inline": e,
314
+ "c-radioGroup--hasAssistiveText": n
230
315
  };
231
- return u`
316
+ return p`
232
317
  <fieldset
233
- tabindex=${n ? -1 : l}
318
+ tabindex=${a ? -1 : r}
234
319
  role="radiogroup"
235
- name=${R(e)}
236
- ?disabled=${i}
320
+ name=${L(t)}
321
+ ?disabled=${s}
237
322
  data-test-id="pie-radio-group-fieldset"
238
- aria-describedby=${d ? m : g}
239
- class="${E(f)}">
323
+ aria-describedby=${n ? g : m}
324
+ class="${O(h)}">
240
325
  ${this._renderWrappedLabel()}
241
326
  <slot @slotchange=${this._handleRadioSlotChange}></slot>
242
327
  </fieldset>
243
- ${d ? u`
328
+ ${n ? p`
244
329
  <pie-assistive-text
245
- id=${m}
246
- variant=${s}
330
+ id=${g}
331
+ variant=${i}
247
332
  message=${o}
248
333
  data-test-id="pie-radio-group-assistive-text">
249
- </pie-assistive-text>` : g}
334
+ </pie-assistive-text>` : m}
250
335
  `;
251
336
  }
252
337
  };
253
- a.styles = C(A);
254
- r([
255
- b()
256
- ], a.prototype, "_hasLabel", 2);
257
- r([
258
- b()
259
- ], a.prototype, "_fieldSetTabIndex", 2);
260
- r([
261
- b()
262
- ], a.prototype, "_allRadiosDisabled", 2);
263
- r([
264
- h({ type: String, reflect: !0 })
265
- ], a.prototype, "name", 2);
266
- r([
267
- h({ type: String })
268
- ], a.prototype, "value", 2);
269
- r([
270
- h({ type: Boolean })
271
- ], a.prototype, "isInline", 2);
272
- r([
273
- h({ type: Boolean, reflect: !0 })
274
- ], a.prototype, "disabled", 2);
275
- r([
276
- h({ type: String })
277
- ], a.prototype, "assistiveText", 2);
278
- r([
279
- h({ type: String }),
280
- I(F, L, c.status)
281
- ], a.prototype, "status", 2);
282
- r([
283
- y({ selector: "pie-radio" })
284
- ], a.prototype, "_slottedChildren", 2);
285
- a = r([
286
- S("pie-radio-group")
287
- ], a);
338
+ l.styles = y(P);
339
+ d([
340
+ _()
341
+ ], l.prototype, "_hasLabel", 2);
342
+ d([
343
+ _()
344
+ ], l.prototype, "_fieldSetTabIndex", 2);
345
+ d([
346
+ _()
347
+ ], l.prototype, "_allRadiosDisabled", 2);
348
+ d([
349
+ u({ type: String, reflect: !0 })
350
+ ], l.prototype, "name", 2);
351
+ d([
352
+ u({ type: String })
353
+ ], l.prototype, "value", 2);
354
+ d([
355
+ u({ type: Boolean })
356
+ ], l.prototype, "isInline", 2);
357
+ d([
358
+ u({ type: Boolean, reflect: !0 })
359
+ ], l.prototype, "disabled", 2);
360
+ d([
361
+ u({ type: String })
362
+ ], l.prototype, "assistiveText", 2);
363
+ d([
364
+ u({ type: String }),
365
+ E(q, G, c.status)
366
+ ], l.prototype, "status", 2);
367
+ l = d([
368
+ I("pie-radio-group")
369
+ ], l);
288
370
  export {
289
- N as ON_RADIO_GROUP_DISABLED,
290
- a as PieRadioGroup,
371
+ $ as ON_RADIO_GROUP_DISABLED,
372
+ l as PieRadioGroup,
291
373
  c as defaultProps,
292
- L as statusTypes
374
+ G as statusTypes
293
375
  };
package/dist/react.d.ts CHANGED
@@ -35,10 +35,18 @@ declare class PieRadioGroup_2 extends PieRadioGroup_base implements RadioGroupPr
35
35
  value: string;
36
36
  isInline: boolean;
37
37
  disabled: boolean;
38
+ private _disabledProvider;
38
39
  assistiveText?: RadioGroupProps['assistiveText'];
39
40
  status: "default" | "success" | "error";
40
- _slottedChildren: Array<HTMLInputElement>;
41
41
  private _abortController;
42
+ private _mutationObserver;
43
+ /**
44
+ * The radios in the group. This uses a subtree query rather than immediate slotted
45
+ * children so radios wrapped in `pie-list-item`s (at any depth) are discovered. It is
46
+ * a superset of the previous immediate-child query, so radios passed as direct children
47
+ * keep working unchanged.
48
+ */
49
+ private get _slottedChildren();
42
50
  /**
43
51
  * Dispatches a custom event to notify each slotted child radio element
44
52
  * when the radio group is disabled.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-radio-group",
3
3
  "description": "PIE Design System Radio Group built using Web Components",
4
- "version": "1.1.5",
4
+ "version": "1.2.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/justeattakeaway/pie",
@@ -45,12 +45,13 @@
45
45
  "devDependencies": {
46
46
  "@justeattakeaway/pie-components-config": "0.21.3",
47
47
  "@justeattakeaway/pie-css": "1.3.1",
48
- "@justeattakeaway/pie-monorepo-utils": "0.9.7",
49
- "@justeattakeaway/pie-radio": "1.1.18"
48
+ "@justeattakeaway/pie-monorepo-utils": "0.9.8",
49
+ "@justeattakeaway/pie-radio": "1.2.0"
50
50
  },
51
51
  "dependencies": {
52
- "@justeattakeaway/pie-assistive-text": "1.0.5",
53
- "@justeattakeaway/pie-webc-core": "16.0.0"
52
+ "@justeattakeaway/pie-assistive-text": "1.0.6",
53
+ "@justeattakeaway/pie-webc-core": "16.1.0",
54
+ "@lit/context": "1.1.6"
54
55
  },
55
56
  "customElements": "custom-elements.json",
56
57
  "sideEffects": [
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  html,
3
+ isServer,
3
4
  unsafeCSS,
4
5
  nothing,
5
6
  type PropertyValues,
@@ -7,14 +8,16 @@ import {
7
8
  } from 'lit';
8
9
  import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement';
9
10
  import {
10
- property, queryAssignedElements, state,
11
+ property, state,
11
12
  } from 'lit/decorators.js';
13
+ import { ContextProvider } from '@lit/context';
12
14
  import {
13
15
  RtlMixin,
14
16
  FormControlMixin,
15
17
  wrapNativeEvent,
16
18
  validPropertyValues,
17
19
  safeCustomElement,
20
+ parentDisabledContext,
18
21
  } from '@justeattakeaway/pie-webc-core';
19
22
  import { ifDefined } from 'lit/directives/if-defined.js';
20
23
  import { classMap } from 'lit/directives/class-map.js';
@@ -62,6 +65,19 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(PieElement)) implem
62
65
  @property({ type: Boolean, reflect: true })
63
66
  public disabled = defaultProps.disabled;
64
67
 
68
+ // Provided to descendant `pie-list-item`s so a fully-disabled group also disables the list rows
69
+ // (suppressing their hover/active states), and kept in sync in `updated()`.
70
+ //
71
+ // Created manually (rather than via the `@provide` decorator) so it can be guarded with
72
+ // `isServer`: `@lit/context`'s ContextProvider attaches `context-request` listeners to the host
73
+ // in its constructor (via `host.addEventListener`). During SSR/prerender the element is
74
+ // constructed without a DOM host, so that call throws and breaks the build. The provider is
75
+ // client-only anyway (context is delivered after `connectedCallback`, which SSR never runs), so
76
+ // it is safe to skip on the server.
77
+ private _disabledProvider = isServer
78
+ ? undefined
79
+ : new ContextProvider(this, { context: parentDisabledContext, initialValue: defaultProps.disabled });
80
+
65
81
  @property({ type: String })
66
82
  public assistiveText?: RadioGroupProps['assistiveText'];
67
83
 
@@ -69,11 +85,20 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(PieElement)) implem
69
85
  @validPropertyValues(componentSelector, statusTypes, defaultProps.status)
70
86
  public status = defaultProps.status;
71
87
 
72
- @queryAssignedElements({ selector: 'pie-radio' })
73
- _slottedChildren!: Array<HTMLInputElement>;
74
-
75
88
  private _abortController!: AbortController;
76
89
 
90
+ private _mutationObserver!: MutationObserver;
91
+
92
+ /**
93
+ * The radios in the group. This uses a subtree query rather than immediate slotted
94
+ * children so radios wrapped in `pie-list-item`s (at any depth) are discovered. It is
95
+ * a superset of the previous immediate-child query, so radios passed as direct children
96
+ * keep working unchanged.
97
+ */
98
+ private get _slottedChildren (): HTMLInputElement[] {
99
+ return Array.from(this.querySelectorAll<HTMLInputElement>('pie-radio'));
100
+ }
101
+
77
102
  /**
78
103
  * Dispatches a custom event to notify each slotted child radio element
79
104
  * when the radio group is disabled.
@@ -171,6 +196,7 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(PieElement)) implem
171
196
 
172
197
  protected updated (_changedProperties: PropertyValues<this>): void {
173
198
  if (_changedProperties.has('disabled')) {
199
+ this._disabledProvider?.setValue(this.disabled);
174
200
  this._handleDisabled();
175
201
  }
176
202
 
@@ -200,6 +226,12 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(PieElement)) implem
200
226
  this.addEventListener('keydown', this._handleKeyDown, { signal });
201
227
 
202
228
  this._applyNameToChildren();
229
+
230
+ // The default slot's `slotchange` fires when list items are added or removed, but not
231
+ // when a radio is added deep inside an existing list item. Observe the light-DOM
232
+ // subtree so those radios still pick up the correct tabindex and name.
233
+ this._mutationObserver = new MutationObserver(() => this._handleRadioSlotChange());
234
+ this._mutationObserver.observe(this, { childList: true, subtree: true });
203
235
  }
204
236
 
205
237
  /**
@@ -383,7 +415,7 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(PieElement)) implem
383
415
  return;
384
416
  }
385
417
 
386
- // Prevent default scrolling behavior when using Arrow keys for Radio Group navigation
418
+ // Prevent default scrolling behaviour when using Arrow keys for Radio Group navigation
387
419
  if (['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp'].includes(event.code)) {
388
420
  event.preventDefault();
389
421
  }
@@ -400,6 +432,7 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(PieElement)) implem
400
432
  disconnectedCallback (): void {
401
433
  super.disconnectedCallback();
402
434
  this._abortController.abort();
435
+ this._mutationObserver?.disconnect();
403
436
  }
404
437
 
405
438
  render () {
@@ -2,6 +2,10 @@
2
2
 
3
3
  :host {
4
4
  display: block;
5
+
6
+ // Applies the divider between adjacent `pie-list-item`s when radios are wrapped in them.
7
+ // Direct-child radios are unaffected (the selector only matches `pie-list-item`).
8
+ @include p.list-item-divider;
5
9
  }
6
10
 
7
11
  .c-radioGroup {
@@ -14,7 +18,6 @@
14
18
  min-width: 0;
15
19
  display: flex;
16
20
  flex-flow: column wrap;
17
- gap: var(--radio-group-gap);
18
21
 
19
22
  > legend {
20
23
  margin-block-end: var(--dt-spacing-b);
@@ -22,6 +25,8 @@
22
25
 
23
26
  &.c-radioGroup--inline {
24
27
  flex-flow: row wrap;
28
+
29
+ // inline only supported for pie-radio in slots (rather than list-item) so gap can be used here
25
30
  gap: var(--radio-group-gap--inline);
26
31
  }
27
32
 
@@ -29,3 +34,10 @@
29
34
  margin-block-end: var(--dt-spacing-a);
30
35
  }
31
36
  }
37
+
38
+ .c-radioGroup:not(.c-radioGroup--inline) {
39
+ // used instead of gap because list-item slotted children do not need the gap. Only pie-radio slotted children do.
40
+ ::slotted(pie-radio:not(:first-of-type)) {
41
+ margin-block-start: var(--radio-group-gap);
42
+ }
43
+ }