@justeattakeaway/pie-radio-group 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,15 +8,6 @@
8
8
  </a>
9
9
  </p>
10
10
 
11
- # Table of Contents
12
-
13
- 1. [Introduction](#pie-radio-group)
14
- 2. [Installation](#installation)
15
- 3. [Importing the component](#importing-the-component)
16
- 4. [Peer Dependencies](#peer-dependencies)
17
- 5. [Props](#props)
18
- 6. [Contributing](#contributing)
19
-
20
11
  ## pie-radio-group
21
12
 
22
13
  `pie-radio-group` is a Web Component built using the Lit library.
@@ -29,63 +20,22 @@ This component can be easily integrated into various frontend frameworks and cus
29
20
  To install `pie-radio-group` in your application, run the following on your command line:
30
21
 
31
22
  ```bash
32
- # npm
33
- $ npm i @justeattakeaway/pie-radio-group
34
-
35
- # yarn
36
- $ yarn add @justeattakeaway/pie-radio-group
37
- ```
38
-
39
- For full information on using PIE components as part of an application, check out the [Getting Started Guide](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components).
40
-
41
-
42
- ### Importing the component
43
-
44
- #### JavaScript
45
- ```js
46
- // Default – for Native JS Applications, Vue, Angular, Svelte, etc.
47
- import { PieRadioGroup } from '@justeattakeaway/pie-radio-group';
48
-
49
- // If you don't need to reference the imported object, you can simply
50
- // import the module which registers the component as a custom element.
51
- import '@justeattakeaway/pie-radio-group';
23
+ npm i @justeattakeaway/pie-radio-group
52
24
  ```
53
-
54
- #### React
55
- ```js
56
- // React
57
- // For React, you will need to import our React-specific component build
58
- // which wraps the web component using ​@lit/react
59
- import { PieRadioGroup } from '@justeattakeaway/pie-radio-group/dist/react';
25
+ ```bash
26
+ yarn add @justeattakeaway/pie-radio-group
60
27
  ```
61
28
 
62
- > [!NOTE]
63
- > When using the React version of the component, please make sure to also
64
- > include React as a [peer dependency](#peer-dependencies) in your project.
65
-
66
-
67
- ## Peer Dependencies
68
-
69
- > [!IMPORTANT]
70
- > When using `pie-radio-group`, you will also need to include a couple of dependencies to ensure the component renders as expected. See [the PIE Wiki](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components#expected-dependencies) for more information and how to include these in your application.
71
-
72
-
73
- ## Props
29
+ For full information on using PIE components as part of an application, check out the [Getting Started Guide](https://github.com/justeattakeaway/pie/wiki/Getting-started-with-PIE-Web-Components).
74
30
 
75
- | Property | Type | Default | Description |
76
- |---|---|---|---|
77
- | - | - | - | - |
31
+ ## Documentation
78
32
 
79
- In your markup or JSX, you can then use these to set the properties for the `pie-radio-group` component:
33
+ Visit [Radio Group | PIE Design System](https://pie.design/components/radio-group/code) to view more information on this component.
80
34
 
81
- ```html
82
- <!-- Native HTML -->
83
- <pie-radio-group></pie-radio-group>
35
+ ## Questions
84
36
 
85
- <!-- JSX -->
86
- <PieRadioGroup></PieRadioGroup>
87
- ```
37
+ Please head to [FAQs | PIE Design System](https://pie.design/support/contact-us/) to see our FAQs and get in touch.
88
38
 
89
39
  ## Contributing
90
40
 
91
- Check out our [contributing guide](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide) for more information on [local development](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#local-development) and how to run specific [component tests](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#testing).
41
+ Check out our [contributing guide](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide) for more information on [local development](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#local-development) and how to run specific [component tests](https://github.com/justeattakeaway/pie/wiki/Contributing-Guide#testing).
@@ -143,6 +143,14 @@
143
143
  "text": "Array<HTMLInputElement>"
144
144
  }
145
145
  },
146
+ {
147
+ "kind": "field",
148
+ "name": "_fieldset",
149
+ "type": {
150
+ "text": "HTMLInputElement"
151
+ },
152
+ "privacy": "private"
153
+ },
146
154
  {
147
155
  "kind": "field",
148
156
  "name": "_abortController",
@@ -151,6 +159,17 @@
151
159
  },
152
160
  "privacy": "private"
153
161
  },
162
+ {
163
+ "kind": "field",
164
+ "name": "_wasShiftTabPressed",
165
+ "type": {
166
+ "text": "boolean"
167
+ },
168
+ "privacy": "private",
169
+ "static": true,
170
+ "default": "false",
171
+ "description": "Tracks whether the `Shift` key was held during the last `Tab` key press.\n\nThe property is static because it needs to be shared across all instances of the\n`PieRadioGroup` component on the same page, ensuring consistent behavior."
172
+ },
154
173
  {
155
174
  "kind": "method",
156
175
  "name": "_handleDisabled",
@@ -243,6 +262,172 @@
243
262
  }
244
263
  },
245
264
  "description": "Renders the label element inside a legend, wrapping the slot content."
265
+ },
266
+ {
267
+ "kind": "method",
268
+ "name": "_updateShiftTabState",
269
+ "privacy": "private",
270
+ "return": {
271
+ "type": {
272
+ "text": "void"
273
+ }
274
+ },
275
+ "parameters": [
276
+ {
277
+ "name": "event",
278
+ "type": {
279
+ "text": "KeyboardEvent"
280
+ }
281
+ }
282
+ ],
283
+ "description": "Updates the state of `_wasShiftTabPressed` based on the last `Tab` key press."
284
+ },
285
+ {
286
+ "kind": "method",
287
+ "name": "_handleFocusIn",
288
+ "privacy": "private",
289
+ "return": {
290
+ "type": {
291
+ "text": "void"
292
+ }
293
+ },
294
+ "parameters": [
295
+ {
296
+ "name": "event",
297
+ "type": {
298
+ "text": "FocusEvent"
299
+ }
300
+ }
301
+ ],
302
+ "description": "Handles the `focusin` event to manage focus within the radio group.\n\nThis method determines the appropriate element to focus when the radio group\ngains focus. It considers the last navigation action (whether `Shift+Tab` was used)\nand focuses the checked option, the first option, or the last option as needed."
303
+ },
304
+ {
305
+ "kind": "method",
306
+ "name": "_handleFocusOut",
307
+ "privacy": "private",
308
+ "return": {
309
+ "type": {
310
+ "text": "void"
311
+ }
312
+ },
313
+ "description": "Handles the `focusout` event to restore the `tabindex` on the radio group's `fieldset`.\n\nWhen focus leaves the radio group, this method enables the `tabindex` attribute\non the `fieldset` element. This ensures the radio group remains accessible for\nkeyboard navigation and can be re-focused when tabbing back into the group."
314
+ },
315
+ {
316
+ "kind": "method",
317
+ "name": "_toggleFieldsetTabindex",
318
+ "privacy": "private",
319
+ "return": {
320
+ "type": {
321
+ "text": "void"
322
+ }
323
+ },
324
+ "parameters": [
325
+ {
326
+ "name": "enable",
327
+ "type": {
328
+ "text": "boolean"
329
+ }
330
+ }
331
+ ]
332
+ },
333
+ {
334
+ "kind": "method",
335
+ "name": "_moveFocus",
336
+ "privacy": "private",
337
+ "return": {
338
+ "type": {
339
+ "text": "void"
340
+ }
341
+ },
342
+ "parameters": [
343
+ {
344
+ "name": "currentIndex",
345
+ "type": {
346
+ "text": "number"
347
+ }
348
+ },
349
+ {
350
+ "name": "step",
351
+ "type": {
352
+ "text": "number"
353
+ }
354
+ }
355
+ ]
356
+ },
357
+ {
358
+ "kind": "method",
359
+ "name": "_isForwardKey",
360
+ "privacy": "private",
361
+ "return": {
362
+ "type": {
363
+ "text": "boolean"
364
+ }
365
+ },
366
+ "parameters": [
367
+ {
368
+ "name": "event",
369
+ "type": {
370
+ "text": "KeyboardEvent"
371
+ }
372
+ }
373
+ ],
374
+ "description": "Determines if a key press indicates forward navigation within the radio group.\n\nThis method evaluates a keyboard event to check if the pressed key corresponds\nto forward navigation based on the current text direction (LTR or RTL).\n\n**Behaviour:**\n- For LTR (Left-to-Right) layouts:\n - `ArrowRight` and `ArrowDown` indicate forward navigation.\n- For RTL (Right-to-Left) layouts:\n - `ArrowLeft` and `ArrowDown` indicate forward navigation."
375
+ },
376
+ {
377
+ "kind": "method",
378
+ "name": "_isBackwardKey",
379
+ "privacy": "private",
380
+ "return": {
381
+ "type": {
382
+ "text": "boolean"
383
+ }
384
+ },
385
+ "parameters": [
386
+ {
387
+ "name": "event",
388
+ "type": {
389
+ "text": "KeyboardEvent"
390
+ }
391
+ }
392
+ ],
393
+ "description": "Determines if a key press indicates backward navigation within the radio group.\n\nThis method evaluates a keyboard event to check if the pressed key corresponds\nto backward navigation based on the current text direction (LTR or RTL).\n\n**Behaviour:**\n- For LTR (Left-to-Right) layouts:\n - `ArrowLeft` and `ArrowUp` indicate backward navigation.\n- For RTL (Right-to-Left) layouts:\n - `ArrowRight` and `ArrowUp` indicate backward navigation."
394
+ },
395
+ {
396
+ "kind": "method",
397
+ "name": "_handleKeyDown",
398
+ "privacy": "private",
399
+ "return": {
400
+ "type": {
401
+ "text": "void"
402
+ }
403
+ },
404
+ "parameters": [
405
+ {
406
+ "name": "event",
407
+ "type": {
408
+ "text": "KeyboardEvent"
409
+ }
410
+ }
411
+ ],
412
+ "description": "Handles keyboard navigation within the radio group using arrow keys.\n\nThis method responds to `keydown` events and determines the appropriate navigation\naction (forward or backward) based on the pressed key and the current focus. It prevents\nthe default browser behaviour (e.g., scrolling) when arrow keys are used for navigation."
413
+ },
414
+ {
415
+ "kind": "method",
416
+ "name": "_focusAndClickOption",
417
+ "privacy": "private",
418
+ "return": {
419
+ "type": {
420
+ "text": "void"
421
+ }
422
+ },
423
+ "parameters": [
424
+ {
425
+ "name": "option",
426
+ "type": {
427
+ "text": "HTMLInputElement"
428
+ }
429
+ }
430
+ ]
246
431
  }
247
432
  ],
248
433
  "events": [
package/dist/index.d.ts CHANGED
@@ -33,7 +33,15 @@ export declare class PieRadioGroup extends PieRadioGroup_base implements RadioGr
33
33
  assistiveText?: RadioGroupProps['assistiveText'];
34
34
  status: "default" | "error" | "success";
35
35
  _slottedChildren: Array<HTMLInputElement>;
36
+ private _fieldset;
36
37
  private _abortController;
38
+ /**
39
+ * Tracks whether the `Shift` key was held during the last `Tab` key press.
40
+ *
41
+ * The property is static because it needs to be shared across all instances of the
42
+ * `PieRadioGroup` component on the same page, ensuring consistent behavior.
43
+ */
44
+ private static _wasShiftTabPressed;
37
45
  /**
38
46
  * Dispatches a custom event to notify each slotted child radio element
39
47
  * when the radio group is disabled.
@@ -71,7 +79,65 @@ export declare class PieRadioGroup extends PieRadioGroup_base implements RadioGr
71
79
  */
72
80
  private _renderWrappedLabel;
73
81
  protected updated(_changedProperties: PropertyValues<this>): void;
82
+ protected firstUpdated(): void;
74
83
  connectedCallback(): void;
84
+ /**
85
+ * Updates the state of `_wasShiftTabPressed` based on the last `Tab` key press.
86
+ */
87
+ private _updateShiftTabState;
88
+ /**
89
+ * Handles the `focusin` event to manage focus within the radio group.
90
+ *
91
+ * This method determines the appropriate element to focus when the radio group
92
+ * gains focus. It considers the last navigation action (whether `Shift+Tab` was used)
93
+ * and focuses the checked option, the first option, or the last option as needed.
94
+ */
95
+ private _handleFocusIn;
96
+ /**
97
+ * Handles the `focusout` event to restore the `tabindex` on the radio group's `fieldset`.
98
+ *
99
+ * When focus leaves the radio group, this method enables the `tabindex` attribute
100
+ * on the `fieldset` element. This ensures the radio group remains accessible for
101
+ * keyboard navigation and can be re-focused when tabbing back into the group.
102
+ */
103
+ private _handleFocusOut;
104
+ private _toggleFieldsetTabindex;
105
+ private _moveFocus;
106
+ /**
107
+ * Determines if a key press indicates forward navigation within the radio group.
108
+ *
109
+ * This method evaluates a keyboard event to check if the pressed key corresponds
110
+ * to forward navigation based on the current text direction (LTR or RTL).
111
+ *
112
+ * **Behaviour:**
113
+ * - For LTR (Left-to-Right) layouts:
114
+ * - `ArrowRight` and `ArrowDown` indicate forward navigation.
115
+ * - For RTL (Right-to-Left) layouts:
116
+ * - `ArrowLeft` and `ArrowDown` indicate forward navigation.
117
+ */
118
+ private _isForwardKey;
119
+ /**
120
+ * Determines if a key press indicates backward navigation within the radio group.
121
+ *
122
+ * This method evaluates a keyboard event to check if the pressed key corresponds
123
+ * to backward navigation based on the current text direction (LTR or RTL).
124
+ *
125
+ * **Behaviour:**
126
+ * - For LTR (Left-to-Right) layouts:
127
+ * - `ArrowLeft` and `ArrowUp` indicate backward navigation.
128
+ * - For RTL (Right-to-Left) layouts:
129
+ * - `ArrowRight` and `ArrowUp` indicate backward navigation.
130
+ */
131
+ private _isBackwardKey;
132
+ /**
133
+ * Handles keyboard navigation within the radio group using arrow keys.
134
+ *
135
+ * This method responds to `keydown` events and determines the appropriate navigation
136
+ * action (forward or backward) based on the pressed key and the current focus. It prevents
137
+ * the default browser behaviour (e.g., scrolling) when arrow keys are used for navigation.
138
+ */
139
+ private _handleKeyDown;
140
+ private _focusAndClickOption;
75
141
  disconnectedCallback(): void;
76
142
  render(): TemplateResult<1>;
77
143
  static styles: CSSResult;
package/dist/index.js CHANGED
@@ -1,23 +1,25 @@
1
- import { LitElement as v, html as p, nothing as u, unsafeCSS as m } from "lit";
2
- import { state as _, property as l, queryAssignedElements as y } from "lit/decorators.js";
3
- import { FormControlMixin as C, RtlMixin as x, wrapNativeEvent as S, validPropertyValues as E, defineCustomElement as $ } from "@justeattakeaway/pie-webc-core";
4
- import { ifDefined as w } from "lit/directives/if-defined.js";
5
- import { classMap as L } from "lit/directives/class-map.js";
1
+ import { LitElement as g, html as c, nothing as p, unsafeCSS as m } from "lit";
2
+ import { state as v, property as l, queryAssignedElements as y, query as w } from "lit/decorators.js";
3
+ import { FormControlMixin as C, RtlMixin as x, wrapNativeEvent as S, validPropertyValues as A, defineCustomElement as T } from "@justeattakeaway/pie-webc-core";
4
+ import { ifDefined as L } from "lit/directives/if-defined.js";
5
+ import { classMap as E } from "lit/directives/class-map.js";
6
6
  import "@justeattakeaway/pie-assistive-text";
7
- const A = "*,*:after,*:before{box-sizing:inherit}.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.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)}", I = ["default", "success", "error"], G = "pie-radio-group-disabled", n = {
7
+ const R = "*,*:after,*:before{box-sizing:inherit}.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.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)}", k = ["default", "success", "error"], F = "pie-radio-group-disabled", h = {
8
8
  status: "default",
9
9
  disabled: !1,
10
10
  isInline: !1,
11
11
  value: ""
12
12
  };
13
- var R = Object.defineProperty, o = (c, e, t, d) => {
14
- for (var a = void 0, i = c.length - 1, r; i >= 0; i--)
15
- (r = c[i]) && (a = r(e, t, a) || a);
16
- return a && R(e, t, a), a;
13
+ var I = Object.defineProperty, d = (u, t, e, s) => {
14
+ for (var i = void 0, a = u.length - 1, n; a >= 0; a--)
15
+ (n = u[a]) && (i = n(t, e, i) || i);
16
+ return i && I(t, e, i), i;
17
17
  };
18
- const b = "pie-radio-group", g = "assistive-text", h = class h extends C(x(v)) {
18
+ const _ = "pie-radio-group", f = "assistive-text";
19
+ var r;
20
+ const o = (r = class extends C(x(g)) {
19
21
  constructor() {
20
- super(...arguments), this._hasLabel = !1, this.value = n.value, this.isInline = n.isInline, this.disabled = n.disabled, this.status = n.status;
22
+ super(...arguments), this._hasLabel = !1, this.value = h.value, this.isInline = h.isInline, this.disabled = h.disabled, this.status = h.status;
21
23
  }
22
24
  /**
23
25
  * Dispatches a custom event to notify each slotted child radio element
@@ -25,7 +27,7 @@ const b = "pie-radio-group", g = "assistive-text", h = class h extends C(x(v)) {
25
27
  * @private
26
28
  */
27
29
  _handleDisabled() {
28
- this._slottedChildren.forEach((e) => e.dispatchEvent(new CustomEvent(G, {
30
+ this._slottedChildren.forEach((t) => t.dispatchEvent(new CustomEvent(F, {
29
31
  bubbles: !1,
30
32
  composed: !1,
31
33
  detail: { disabled: this.disabled }
@@ -37,16 +39,16 @@ const b = "pie-radio-group", g = "assistive-text", h = class h extends C(x(v)) {
37
39
  * @returns {void}
38
40
  */
39
41
  _handleStatus() {
40
- this._slottedChildren.forEach((e) => e.setAttribute("status", this.status === "error" ? "error" : "default"));
42
+ this._slottedChildren.forEach((t) => t.setAttribute("status", this.status === "error" ? "error" : "default"));
41
43
  }
42
44
  /**
43
45
  * Unselects all radios that are not the selected value.
44
46
  * @param {string} selectedValue - The value of the currently selected radio.
45
47
  * @private
46
48
  */
47
- _handleRadioSelection(e) {
48
- this.value = e, this._slottedChildren.forEach((t) => {
49
- t.disabled || (t.checked = t.value === e);
49
+ _handleRadioSelection(t) {
50
+ this.value = t, this._slottedChildren.forEach((e) => {
51
+ e.disabled || (e.checked = e.value === t);
50
52
  });
51
53
  }
52
54
  /**
@@ -54,21 +56,21 @@ const b = "pie-radio-group", g = "assistive-text", h = class h extends C(x(v)) {
54
56
  * @param {Event} event - The change event from a radio element.
55
57
  * @private
56
58
  */
57
- _handleRadioChange(e) {
58
- e.stopPropagation();
59
- const t = e.target;
60
- this._handleRadioSelection(t.value);
61
- const d = S(e);
62
- this.dispatchEvent(d);
59
+ _handleRadioChange(t) {
60
+ t.stopPropagation();
61
+ const e = t.target;
62
+ this._handleRadioSelection(e.value);
63
+ const s = S(t);
64
+ this.dispatchEvent(s);
63
65
  }
64
66
  /**
65
67
  * Updates the `_hasLabel` state when content is added to the label slot.
66
68
  * @param {Event} e - The slotchange event.
67
69
  * @private
68
70
  */
69
- _handleSlotChange(e) {
70
- const t = e.target.assignedNodes({ flatten: !0 });
71
- this._hasLabel = t.length > 0;
71
+ _handleSlotChange(t) {
72
+ const e = t.target.assignedNodes({ flatten: !0 });
73
+ this._hasLabel = e.length > 0;
72
74
  }
73
75
  /**
74
76
  * Renders the label element inside a legend, wrapping the slot content.
@@ -76,83 +78,173 @@ const b = "pie-radio-group", g = "assistive-text", h = class h extends C(x(v)) {
76
78
  * @private
77
79
  */
78
80
  _renderWrappedLabel() {
79
- return this._hasLabel ? p`<legend><slot name='label' @slotchange=${this._handleSlotChange}></slot></legend>` : p`<slot name='label' @slotchange=${this._handleSlotChange}></slot>`;
81
+ return this._hasLabel ? c`<legend><slot name='label' @slotchange=${this._handleSlotChange}></slot></legend>` : c`<slot name='label' @slotchange=${this._handleSlotChange}></slot>`;
80
82
  }
81
- updated(e) {
82
- e.has("disabled") && this._handleDisabled(), e.has("value") && this._handleRadioSelection(this.value), e.has("status") && this._handleStatus();
83
+ updated(t) {
84
+ t.has("disabled") && this._handleDisabled(), t.has("value") && this._handleRadioSelection(this.value), t.has("status") && this._handleStatus();
85
+ }
86
+ firstUpdated() {
87
+ this._slottedChildren.forEach((t) => t.setAttribute("tabindex", "-1"));
83
88
  }
84
89
  connectedCallback() {
85
- var t;
90
+ var e;
86
91
  super.connectedCallback(), this._abortController = new AbortController();
87
- const { signal: e } = this._abortController;
88
- (t = this.shadowRoot) == null || t.addEventListener("change", this._handleRadioChange.bind(this), { signal: e });
92
+ const { signal: t } = this._abortController;
93
+ (e = this.shadowRoot) == null || e.addEventListener("change", this._handleRadioChange.bind(this), { signal: t }), this.addEventListener("focusin", this._handleFocusIn, { signal: t }), this.addEventListener("focusout", this._handleFocusOut, { signal: t }), this.addEventListener("keydown", this._handleKeyDown, { signal: t }), document.addEventListener("keydown", this._updateShiftTabState.bind(this), { signal: t });
94
+ }
95
+ /**
96
+ * Updates the state of `_wasShiftTabPressed` based on the last `Tab` key press.
97
+ */
98
+ _updateShiftTabState(t) {
99
+ t.key === "Tab" && (r._wasShiftTabPressed = t.shiftKey);
100
+ }
101
+ /**
102
+ * Handles the `focusin` event to manage focus within the radio group.
103
+ *
104
+ * This method determines the appropriate element to focus when the radio group
105
+ * gains focus. It considers the last navigation action (whether `Shift+Tab` was used)
106
+ * and focuses the checked option, the first option, or the last option as needed.
107
+ */
108
+ _handleFocusIn(t) {
109
+ var i;
110
+ if (this !== t.target) return;
111
+ const e = r._wasShiftTabPressed, s = ((i = this._slottedChildren) == null ? void 0 : i.find((a) => a.checked)) || (e ? this._slottedChildren.at(-1) : this._slottedChildren[0]);
112
+ s && (s.focus(), this._toggleFieldsetTabindex(!1));
113
+ }
114
+ /**
115
+ * Handles the `focusout` event to restore the `tabindex` on the radio group's `fieldset`.
116
+ *
117
+ * When focus leaves the radio group, this method enables the `tabindex` attribute
118
+ * on the `fieldset` element. This ensures the radio group remains accessible for
119
+ * keyboard navigation and can be re-focused when tabbing back into the group.
120
+ */
121
+ _handleFocusOut() {
122
+ this._toggleFieldsetTabindex(!0);
123
+ }
124
+ _toggleFieldsetTabindex(t) {
125
+ t ? this._fieldset.setAttribute("tabindex", "0") : this._fieldset.removeAttribute("tabindex");
126
+ }
127
+ _moveFocus(t, e) {
128
+ const s = (t + e + this._slottedChildren.length) % this._slottedChildren.length;
129
+ this._focusAndClickOption(this._slottedChildren[s]);
130
+ }
131
+ /**
132
+ * Determines if a key press indicates forward navigation within the radio group.
133
+ *
134
+ * This method evaluates a keyboard event to check if the pressed key corresponds
135
+ * to forward navigation based on the current text direction (LTR or RTL).
136
+ *
137
+ * **Behaviour:**
138
+ * - For LTR (Left-to-Right) layouts:
139
+ * - `ArrowRight` and `ArrowDown` indicate forward navigation.
140
+ * - For RTL (Right-to-Left) layouts:
141
+ * - `ArrowLeft` and `ArrowDown` indicate forward navigation.
142
+ */
143
+ _isForwardKey(t) {
144
+ return t.code === "ArrowRight" && !this.isRTL || t.code === "ArrowLeft" && this.isRTL || t.code === "ArrowDown";
145
+ }
146
+ /**
147
+ * Determines if a key press indicates backward navigation within the radio group.
148
+ *
149
+ * This method evaluates a keyboard event to check if the pressed key corresponds
150
+ * to backward navigation based on the current text direction (LTR or RTL).
151
+ *
152
+ * **Behaviour:**
153
+ * - For LTR (Left-to-Right) layouts:
154
+ * - `ArrowLeft` and `ArrowUp` indicate backward navigation.
155
+ * - For RTL (Right-to-Left) layouts:
156
+ * - `ArrowRight` and `ArrowUp` indicate backward navigation.
157
+ */
158
+ _isBackwardKey(t) {
159
+ return t.code === "ArrowLeft" && !this.isRTL || t.code === "ArrowRight" && this.isRTL || t.code === "ArrowUp";
160
+ }
161
+ /**
162
+ * Handles keyboard navigation within the radio group using arrow keys.
163
+ *
164
+ * This method responds to `keydown` events and determines the appropriate navigation
165
+ * action (forward or backward) based on the pressed key and the current focus. It prevents
166
+ * the default browser behaviour (e.g., scrolling) when arrow keys are used for navigation.
167
+ */
168
+ _handleKeyDown(t) {
169
+ const e = this._slottedChildren.find((i) => i === document.activeElement);
170
+ if (!e)
171
+ return;
172
+ const s = this._slottedChildren.indexOf(e);
173
+ 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));
174
+ }
175
+ _focusAndClickOption(t) {
176
+ var e, s;
177
+ t.focus(), (s = (e = t.shadowRoot) == null ? void 0 : e.querySelector("input")) == null || s.click(), this._toggleFieldsetTabindex(!1);
89
178
  }
90
179
  disconnectedCallback() {
91
180
  super.disconnectedCallback(), this._abortController.abort();
92
181
  }
93
182
  render() {
94
183
  const {
95
- name: e,
96
- isInline: t,
97
- disabled: d,
98
- status: a,
99
- assistiveText: i
100
- } = this, r = !!(i != null && i.length), f = {
184
+ name: t,
185
+ isInline: e,
186
+ disabled: s,
187
+ status: i,
188
+ assistiveText: a
189
+ } = this, n = !!(a != null && a.length), b = {
101
190
  "c-radioGroup": !0,
102
- "c-radioGroup--inline": t,
103
- "c-radioGroup--hasAssistiveText": r
191
+ "c-radioGroup--inline": e,
192
+ "c-radioGroup--hasAssistiveText": n
104
193
  };
105
- return p`
194
+ return c`
106
195
  <fieldset
107
- name=${w(e)}
108
- ?disabled=${d}
196
+ tabindex="0"
197
+ name=${L(t)}
198
+ ?disabled=${s}
109
199
  data-test-id="pie-radio-group"
110
- aria-describedby=${r ? g : u}
111
- class="${L(f)}">
200
+ aria-describedby=${n ? f : p}
201
+ class="${E(b)}">
112
202
  ${this._renderWrappedLabel()}
113
203
  <slot></slot>
114
204
  </fieldset>
115
- ${r ? p`
205
+ ${n ? c`
116
206
  <pie-assistive-text
117
- id=${g}
118
- variant=${a}
207
+ id=${f}
208
+ variant=${i}
119
209
  data-test-id="pie-radio-group-assistive-text">
120
- ${i}
121
- </pie-assistive-text>` : u}
210
+ ${a}
211
+ </pie-assistive-text>` : p}
122
212
  `;
123
213
  }
124
- };
125
- h.styles = m(A);
126
- let s = h;
127
- o([
128
- _()
129
- ], s.prototype, "_hasLabel");
130
- o([
214
+ }, r._wasShiftTabPressed = !1, r.styles = m(R), r);
215
+ d([
216
+ v()
217
+ ], o.prototype, "_hasLabel");
218
+ d([
131
219
  l({ type: String })
132
- ], s.prototype, "name");
133
- o([
220
+ ], o.prototype, "name");
221
+ d([
134
222
  l({ type: String })
135
- ], s.prototype, "value");
136
- o([
223
+ ], o.prototype, "value");
224
+ d([
137
225
  l({ type: Boolean })
138
- ], s.prototype, "isInline");
139
- o([
226
+ ], o.prototype, "isInline");
227
+ d([
140
228
  l({ type: Boolean, reflect: !0 })
141
- ], s.prototype, "disabled");
142
- o([
229
+ ], o.prototype, "disabled");
230
+ d([
143
231
  l({ type: String })
144
- ], s.prototype, "assistiveText");
145
- o([
232
+ ], o.prototype, "assistiveText");
233
+ d([
146
234
  l({ type: String }),
147
- E(b, I, n.status)
148
- ], s.prototype, "status");
149
- o([
235
+ A(_, k, h.status)
236
+ ], o.prototype, "status");
237
+ d([
150
238
  y({ selector: "pie-radio" })
151
- ], s.prototype, "_slottedChildren");
152
- $(b, s);
239
+ ], o.prototype, "_slottedChildren");
240
+ d([
241
+ w("fieldset")
242
+ ], o.prototype, "_fieldset");
243
+ let D = o;
244
+ T(_, D);
153
245
  export {
154
- G as ON_RADIO_GROUP_DISABLED,
155
- s as PieRadioGroup,
156
- n as defaultProps,
157
- I as statusTypes
246
+ F as ON_RADIO_GROUP_DISABLED,
247
+ D as PieRadioGroup,
248
+ h as defaultProps,
249
+ k as statusTypes
158
250
  };
package/dist/react.d.ts CHANGED
@@ -36,7 +36,15 @@ declare class PieRadioGroup_2 extends PieRadioGroup_base implements RadioGroupPr
36
36
  assistiveText?: RadioGroupProps['assistiveText'];
37
37
  status: "default" | "error" | "success";
38
38
  _slottedChildren: Array<HTMLInputElement>;
39
+ private _fieldset;
39
40
  private _abortController;
41
+ /**
42
+ * Tracks whether the `Shift` key was held during the last `Tab` key press.
43
+ *
44
+ * The property is static because it needs to be shared across all instances of the
45
+ * `PieRadioGroup` component on the same page, ensuring consistent behavior.
46
+ */
47
+ private static _wasShiftTabPressed;
40
48
  /**
41
49
  * Dispatches a custom event to notify each slotted child radio element
42
50
  * when the radio group is disabled.
@@ -74,7 +82,65 @@ declare class PieRadioGroup_2 extends PieRadioGroup_base implements RadioGroupPr
74
82
  */
75
83
  private _renderWrappedLabel;
76
84
  protected updated(_changedProperties: PropertyValues<this>): void;
85
+ protected firstUpdated(): void;
77
86
  connectedCallback(): void;
87
+ /**
88
+ * Updates the state of `_wasShiftTabPressed` based on the last `Tab` key press.
89
+ */
90
+ private _updateShiftTabState;
91
+ /**
92
+ * Handles the `focusin` event to manage focus within the radio group.
93
+ *
94
+ * This method determines the appropriate element to focus when the radio group
95
+ * gains focus. It considers the last navigation action (whether `Shift+Tab` was used)
96
+ * and focuses the checked option, the first option, or the last option as needed.
97
+ */
98
+ private _handleFocusIn;
99
+ /**
100
+ * Handles the `focusout` event to restore the `tabindex` on the radio group's `fieldset`.
101
+ *
102
+ * When focus leaves the radio group, this method enables the `tabindex` attribute
103
+ * on the `fieldset` element. This ensures the radio group remains accessible for
104
+ * keyboard navigation and can be re-focused when tabbing back into the group.
105
+ */
106
+ private _handleFocusOut;
107
+ private _toggleFieldsetTabindex;
108
+ private _moveFocus;
109
+ /**
110
+ * Determines if a key press indicates forward navigation within the radio group.
111
+ *
112
+ * This method evaluates a keyboard event to check if the pressed key corresponds
113
+ * to forward navigation based on the current text direction (LTR or RTL).
114
+ *
115
+ * **Behaviour:**
116
+ * - For LTR (Left-to-Right) layouts:
117
+ * - `ArrowRight` and `ArrowDown` indicate forward navigation.
118
+ * - For RTL (Right-to-Left) layouts:
119
+ * - `ArrowLeft` and `ArrowDown` indicate forward navigation.
120
+ */
121
+ private _isForwardKey;
122
+ /**
123
+ * Determines if a key press indicates backward navigation within the radio group.
124
+ *
125
+ * This method evaluates a keyboard event to check if the pressed key corresponds
126
+ * to backward navigation based on the current text direction (LTR or RTL).
127
+ *
128
+ * **Behaviour:**
129
+ * - For LTR (Left-to-Right) layouts:
130
+ * - `ArrowLeft` and `ArrowUp` indicate backward navigation.
131
+ * - For RTL (Right-to-Left) layouts:
132
+ * - `ArrowRight` and `ArrowUp` indicate backward navigation.
133
+ */
134
+ private _isBackwardKey;
135
+ /**
136
+ * Handles keyboard navigation within the radio group using arrow keys.
137
+ *
138
+ * This method responds to `keydown` events and determines the appropriate navigation
139
+ * action (forward or backward) based on the pressed key and the current focus. It prevents
140
+ * the default browser behaviour (e.g., scrolling) when arrow keys are used for navigation.
141
+ */
142
+ private _handleKeyDown;
143
+ private _focusAndClickOption;
78
144
  disconnectedCallback(): void;
79
145
  render(): TemplateResult<1>;
80
146
  static styles: CSSResult;
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": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -38,11 +38,11 @@
38
38
  "@custom-elements-manifest/analyzer": "0.9.0",
39
39
  "@justeattakeaway/pie-components-config": "0.18.0",
40
40
  "@justeattakeaway/pie-css": "0.13.1",
41
- "@justeattakeaway/pie-radio": "0.5.0",
41
+ "@justeattakeaway/pie-radio": "0.6.0",
42
42
  "cem-plugin-module-file-extensions": "0.0.5"
43
43
  },
44
44
  "dependencies": {
45
- "@justeattakeaway/pie-assistive-text": "0.8.0",
45
+ "@justeattakeaway/pie-assistive-text": "0.8.1",
46
46
  "@justeattakeaway/pie-webc-core": "0.24.2"
47
47
  },
48
48
  "volta": {
package/src/index.ts CHANGED
@@ -6,7 +6,9 @@ import {
6
6
  type PropertyValues,
7
7
  type TemplateResult,
8
8
  } from 'lit';
9
- import { property, queryAssignedElements, state } from 'lit/decorators.js';
9
+ import {
10
+ property, query, queryAssignedElements, state,
11
+ } from 'lit/decorators.js';
10
12
  import {
11
13
  RtlMixin,
12
14
  defineCustomElement,
@@ -63,8 +65,19 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
63
65
  @queryAssignedElements({ selector: 'pie-radio' })
64
66
  _slottedChildren!: Array<HTMLInputElement>;
65
67
 
68
+ @query('fieldset')
69
+ private _fieldset!: HTMLInputElement;
70
+
66
71
  private _abortController!: AbortController;
67
72
 
73
+ /**
74
+ * Tracks whether the `Shift` key was held during the last `Tab` key press.
75
+ *
76
+ * The property is static because it needs to be shared across all instances of the
77
+ * `PieRadioGroup` component on the same page, ensuring consistent behavior.
78
+ */
79
+ private static _wasShiftTabPressed = false;
80
+
68
81
  /**
69
82
  * Dispatches a custom event to notify each slotted child radio element
70
83
  * when the radio group is disabled.
@@ -147,12 +160,154 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
147
160
  }
148
161
  }
149
162
 
163
+ protected firstUpdated (): void {
164
+ // Make all radios impossible to tab to
165
+ // This is because by default, we are able to tab to each individual radio button.
166
+ // This is not the behaviour we want, so applying -1 tabindex prevents it.
167
+ this._slottedChildren.forEach((radio) => radio.setAttribute('tabindex', '-1'));
168
+ }
169
+
150
170
  connectedCallback (): void {
151
171
  super.connectedCallback();
152
172
  this._abortController = new AbortController();
153
173
  const { signal } = this._abortController;
154
174
 
155
175
  this.shadowRoot?.addEventListener('change', this._handleRadioChange.bind(this), { signal });
176
+
177
+ this.addEventListener('focusin', this._handleFocusIn, { signal });
178
+ this.addEventListener('focusout', this._handleFocusOut, { signal });
179
+
180
+ this.addEventListener('keydown', this._handleKeyDown, { signal });
181
+ document.addEventListener('keydown', this._updateShiftTabState.bind(this), { signal });
182
+ }
183
+
184
+ /**
185
+ * Updates the state of `_wasShiftTabPressed` based on the last `Tab` key press.
186
+ */
187
+ private _updateShiftTabState (event: KeyboardEvent): void {
188
+ if (event.key === 'Tab') {
189
+ PieRadioGroup._wasShiftTabPressed = event.shiftKey;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Handles the `focusin` event to manage focus within the radio group.
195
+ *
196
+ * This method determines the appropriate element to focus when the radio group
197
+ * gains focus. It considers the last navigation action (whether `Shift+Tab` was used)
198
+ * and focuses the checked option, the first option, or the last option as needed.
199
+ */
200
+ private _handleFocusIn (event: FocusEvent): void {
201
+ if (this !== event.target) return;
202
+
203
+ const isShiftTab = PieRadioGroup._wasShiftTabPressed;
204
+ const focusTarget = this._slottedChildren?.find((child) => child.checked) ||
205
+ (isShiftTab ? this._slottedChildren.at(-1) : this._slottedChildren[0]);
206
+
207
+ if (!focusTarget) return;
208
+
209
+ focusTarget.focus();
210
+ this._toggleFieldsetTabindex(false);
211
+ }
212
+
213
+ /**
214
+ * Handles the `focusout` event to restore the `tabindex` on the radio group's `fieldset`.
215
+ *
216
+ * When focus leaves the radio group, this method enables the `tabindex` attribute
217
+ * on the `fieldset` element. This ensures the radio group remains accessible for
218
+ * keyboard navigation and can be re-focused when tabbing back into the group.
219
+ */
220
+ private _handleFocusOut (): void {
221
+ this._toggleFieldsetTabindex(true);
222
+ }
223
+
224
+ private _toggleFieldsetTabindex (enable: boolean): void {
225
+ if (enable) {
226
+ this._fieldset.setAttribute('tabindex', '0');
227
+ } else {
228
+ this._fieldset.removeAttribute('tabindex');
229
+ }
230
+ }
231
+
232
+ private _moveFocus (currentIndex: number, step: number): void {
233
+ const newIndex = (currentIndex + step + this._slottedChildren.length) % this._slottedChildren.length;
234
+ this._focusAndClickOption(this._slottedChildren[newIndex]);
235
+ }
236
+
237
+ /**
238
+ * Determines if a key press indicates forward navigation within the radio group.
239
+ *
240
+ * This method evaluates a keyboard event to check if the pressed key corresponds
241
+ * to forward navigation based on the current text direction (LTR or RTL).
242
+ *
243
+ * **Behaviour:**
244
+ * - For LTR (Left-to-Right) layouts:
245
+ * - `ArrowRight` and `ArrowDown` indicate forward navigation.
246
+ * - For RTL (Right-to-Left) layouts:
247
+ * - `ArrowLeft` and `ArrowDown` indicate forward navigation.
248
+ */
249
+ private _isForwardKey (event: KeyboardEvent): boolean {
250
+ return (event.code === 'ArrowRight' && !this.isRTL) ||
251
+ (event.code === 'ArrowLeft' && this.isRTL) ||
252
+ event.code === 'ArrowDown';
253
+ }
254
+
255
+ /**
256
+ * Determines if a key press indicates backward navigation within the radio group.
257
+ *
258
+ * This method evaluates a keyboard event to check if the pressed key corresponds
259
+ * to backward navigation based on the current text direction (LTR or RTL).
260
+ *
261
+ * **Behaviour:**
262
+ * - For LTR (Left-to-Right) layouts:
263
+ * - `ArrowLeft` and `ArrowUp` indicate backward navigation.
264
+ * - For RTL (Right-to-Left) layouts:
265
+ * - `ArrowRight` and `ArrowUp` indicate backward navigation.
266
+ */
267
+ private _isBackwardKey (event: KeyboardEvent): boolean {
268
+ return (event.code === 'ArrowLeft' && !this.isRTL) ||
269
+ (event.code === 'ArrowRight' && this.isRTL) ||
270
+ event.code === 'ArrowUp';
271
+ }
272
+
273
+ /**
274
+ * Handles keyboard navigation within the radio group using arrow keys.
275
+ *
276
+ * This method responds to `keydown` events and determines the appropriate navigation
277
+ * action (forward or backward) based on the pressed key and the current focus. It prevents
278
+ * the default browser behaviour (e.g., scrolling) when arrow keys are used for navigation.
279
+ */
280
+ private _handleKeyDown (event: KeyboardEvent): void {
281
+ const currentlyFocusedChild = this._slottedChildren.find((child) => child === document.activeElement);
282
+
283
+ if (!currentlyFocusedChild) {
284
+ return;
285
+ }
286
+
287
+ const currentIndex = this._slottedChildren.indexOf(currentlyFocusedChild);
288
+ if (currentIndex === -1) {
289
+ return;
290
+ }
291
+
292
+ // Prevent default scrolling behavior when using Arrow keys for Radio Group navigation
293
+ if (['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp'].includes(event.code)) {
294
+ event.preventDefault();
295
+ }
296
+
297
+ if (this._isForwardKey(event)) {
298
+ this._moveFocus(currentIndex, 1);
299
+ } else if (this._isBackwardKey(event)) {
300
+ this._moveFocus(currentIndex, -1);
301
+ }
302
+ }
303
+
304
+ private _focusAndClickOption (option: HTMLInputElement): void {
305
+ option.focus();
306
+ // This is quite hacky, but it ensures the radio elements correct emit a real change event.
307
+ // Simply setting option.checked as true would require re-architecture of both this component and the radio button
308
+ // to ensure that property changes are observed and correctly propagated up.
309
+ option.shadowRoot?.querySelector('input')?.click();
310
+ this._toggleFieldsetTabindex(false);
156
311
  }
157
312
 
158
313
  disconnectedCallback (): void {
@@ -179,6 +334,7 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
179
334
 
180
335
  return html`
181
336
  <fieldset
337
+ tabindex="0"
182
338
  name=${ifDefined(name)}
183
339
  ?disabled=${disabled}
184
340
  data-test-id="pie-radio-group"