@justeattakeaway/pie-radio-group 1.1.4 → 1.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 +41 -3
- package/custom-elements.json +21 -5
- package/dist/index.d.ts +9 -1
- package/dist/index.js +211 -129
- package/dist/react.d.ts +9 -1
- package/package.json +7 -6
- package/src/index.ts +38 -5
- package/src/radio-group.scss +13 -1
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` | — |
|
|
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` |
|
|
47
|
-
| `label` | To provide a custom label for the radio group. Please use [`pie-form-label`](/components
|
|
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).
|
package/custom-elements.json
CHANGED
|
@@ -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": "
|
|
165
|
+
"name": "_abortController",
|
|
161
166
|
"type": {
|
|
162
|
-
"text": "
|
|
163
|
-
}
|
|
167
|
+
"text": "AbortController"
|
|
168
|
+
},
|
|
169
|
+
"privacy": "private"
|
|
164
170
|
},
|
|
165
171
|
{
|
|
166
172
|
"kind": "field",
|
|
167
|
-
"name": "
|
|
173
|
+
"name": "_mutationObserver",
|
|
168
174
|
"type": {
|
|
169
|
-
"text": "
|
|
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
|
|
2
|
-
import { state as
|
|
3
|
-
import { FormControlMixin as
|
|
4
|
-
import { ifDefined as
|
|
5
|
-
import { classMap as
|
|
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
|
|
7
|
+
const b = class b extends x {
|
|
8
8
|
willUpdate() {
|
|
9
|
-
this.getAttribute("v") || this.setAttribute("v",
|
|
9
|
+
this.getAttribute("v") || this.setAttribute("v", b.v);
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
let
|
|
14
|
-
|
|
12
|
+
b.v = "1.2.0";
|
|
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)}:host(:not([isinline])) ::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
|
|
21
|
-
for (var o =
|
|
22
|
-
(
|
|
23
|
-
return
|
|
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
|
|
26
|
-
let
|
|
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((
|
|
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((
|
|
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((
|
|
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(
|
|
56
|
-
this.value =
|
|
57
|
-
|
|
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(
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
this._handleRadioSelection(
|
|
69
|
-
const
|
|
70
|
-
this.dispatchEvent(
|
|
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(
|
|
78
|
-
const
|
|
79
|
-
this._hasLabel =
|
|
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((
|
|
92
|
-
|
|
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 ?
|
|
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((
|
|
105
|
-
this.name &&
|
|
187
|
+
this._slottedChildren.forEach((t) => {
|
|
188
|
+
this.name && t.setAttribute("name", this.name);
|
|
106
189
|
});
|
|
107
190
|
}
|
|
108
|
-
updated(
|
|
109
|
-
|
|
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
|
|
196
|
+
var e;
|
|
113
197
|
super.connectedCallback(), this._abortController = new AbortController();
|
|
114
|
-
const { signal:
|
|
115
|
-
this.addEventListener("focus", this._handleFocus, { signal:
|
|
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(
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
this.value === "" || this.value === void 0 ? this._focusNearestAvailableRadio(
|
|
136
|
-
|
|
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(
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
235
|
+
_handleFocusOut(t) {
|
|
236
|
+
const e = t.relatedTarget;
|
|
237
|
+
if (!e) {
|
|
154
238
|
this._resetButtonsTabIndex(), this._fieldSetTabIndex = 0;
|
|
155
239
|
return;
|
|
156
240
|
}
|
|
157
|
-
(
|
|
241
|
+
(e.compareDocumentPosition(this) & Node.DOCUMENT_POSITION_CONTAINS) === 0 ? (this._resetButtonsTabIndex(), this._fieldSetTabIndex = 0) : this._fieldSetTabIndex = -1;
|
|
158
242
|
}
|
|
159
|
-
_moveFocus(
|
|
160
|
-
var
|
|
161
|
-
if (!this._slottedChildren.filter((
|
|
162
|
-
const
|
|
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
|
-
|
|
248
|
+
e === 0 ? this._moveFocus(i, 1) : this._moveFocus(i, e);
|
|
165
249
|
return;
|
|
166
250
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
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(
|
|
183
|
-
return
|
|
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(
|
|
198
|
-
return
|
|
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(
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
291
|
+
_handleKeyDown(t) {
|
|
292
|
+
const e = this._slottedChildren.find((i) => i === document.activeElement);
|
|
293
|
+
if (!e)
|
|
210
294
|
return;
|
|
211
|
-
const
|
|
212
|
-
|
|
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
|
-
|
|
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:
|
|
220
|
-
isInline:
|
|
221
|
-
disabled:
|
|
222
|
-
status:
|
|
304
|
+
name: t,
|
|
305
|
+
isInline: e,
|
|
306
|
+
disabled: s,
|
|
307
|
+
status: i,
|
|
223
308
|
assistiveText: o,
|
|
224
|
-
_fieldSetTabIndex:
|
|
225
|
-
_allRadiosDisabled:
|
|
226
|
-
} = this,
|
|
309
|
+
_fieldSetTabIndex: r,
|
|
310
|
+
_allRadiosDisabled: a
|
|
311
|
+
} = this, n = !!(o != null && o.length), h = {
|
|
227
312
|
"c-radioGroup": !0,
|
|
228
|
-
"c-radioGroup--inline":
|
|
229
|
-
"c-radioGroup--hasAssistiveText":
|
|
313
|
+
"c-radioGroup--inline": e,
|
|
314
|
+
"c-radioGroup--hasAssistiveText": n
|
|
230
315
|
};
|
|
231
|
-
return
|
|
316
|
+
return p`
|
|
232
317
|
<fieldset
|
|
233
|
-
tabindex=${
|
|
318
|
+
tabindex=${a ? -1 : r}
|
|
234
319
|
role="radiogroup"
|
|
235
|
-
name=${
|
|
236
|
-
?disabled=${
|
|
320
|
+
name=${L(t)}
|
|
321
|
+
?disabled=${s}
|
|
237
322
|
data-test-id="pie-radio-group-fieldset"
|
|
238
|
-
aria-describedby=${
|
|
239
|
-
class="${
|
|
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
|
-
${
|
|
328
|
+
${n ? p`
|
|
244
329
|
<pie-assistive-text
|
|
245
|
-
id=${
|
|
246
|
-
variant=${
|
|
330
|
+
id=${g}
|
|
331
|
+
variant=${i}
|
|
247
332
|
message=${o}
|
|
248
333
|
data-test-id="pie-radio-group-assistive-text">
|
|
249
|
-
</pie-assistive-text>` :
|
|
334
|
+
</pie-assistive-text>` : m}
|
|
250
335
|
`;
|
|
251
336
|
}
|
|
252
337
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
],
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
],
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
],
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
],
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
],
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
],
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
],
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
],
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
],
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
],
|
|
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
|
-
|
|
290
|
-
|
|
371
|
+
$ as ON_RADIO_GROUP_DISABLED,
|
|
372
|
+
l as PieRadioGroup,
|
|
291
373
|
c as defaultProps,
|
|
292
|
-
|
|
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.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/justeattakeaway/pie",
|
|
@@ -44,13 +44,14 @@
|
|
|
44
44
|
"license": "Apache-2.0",
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@justeattakeaway/pie-components-config": "0.21.3",
|
|
47
|
-
"@justeattakeaway/pie-css": "1.3.
|
|
48
|
-
"@justeattakeaway/pie-monorepo-utils": "0.9.
|
|
49
|
-
"@justeattakeaway/pie-radio": "1.
|
|
47
|
+
"@justeattakeaway/pie-css": "1.3.1",
|
|
48
|
+
"@justeattakeaway/pie-monorepo-utils": "0.9.7",
|
|
49
|
+
"@justeattakeaway/pie-radio": "1.2.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@justeattakeaway/pie-assistive-text": "1.0.
|
|
53
|
-
"@justeattakeaway/pie-webc-core": "16.
|
|
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,
|
|
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
|
|
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 () {
|
package/src/radio-group.scss
CHANGED
|
@@ -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
|
+
:host(:not([isinline])) {
|
|
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
|
+
}
|