diff --git a/.changeset/wet-gorillas-attack.md b/.changeset/wet-gorillas-attack.md
new file mode 100644
index 0000000000..2225885e78
--- /dev/null
+++ b/.changeset/wet-gorillas-attack.md
@@ -0,0 +1,5 @@
+---
+"@justeattakeaway/pie-radio-group": minor
+---
+
+[Added] - Pass name prop down to radio child elements in slot
diff --git a/apps/pie-storybook/stories/testing/pie-radio-group.test.stories.ts b/apps/pie-storybook/stories/testing/pie-radio-group.test.stories.ts
index a5a1742f3d..6c90f3a7f8 100644
--- a/apps/pie-storybook/stories/testing/pie-radio-group.test.stories.ts
+++ b/apps/pie-storybook/stories/testing/pie-radio-group.test.stories.ts
@@ -10,6 +10,7 @@ import '@justeattakeaway/pie-link';
import '@justeattakeaway/pie-radio';
import '@justeattakeaway/pie-form-label';
import '@justeattakeaway/pie-button';
+import '@justeattakeaway/pie-icons-webc/dist/IconPlusCircle';
import { createStory } from '../../utilities';
@@ -48,7 +49,7 @@ export default radioGroupStoryMeta;
const KeyboardNavigationTemplate = () => html`
Radio group 1
Button 1
-
+
Chinese
Shawarma
Pizza
@@ -69,4 +70,38 @@ const KeyboardNavigationTemplate = () => html`
Button 4
`;
+const DynamicSlotsTemplate = () => {
+ let counter = 2;
+
+ // Function to dynamically add radio buttons to the radio group
+ const onAddButtonClick = () => {
+ const radioGroup = document.querySelector('pie-radio-group[data-test-id="radio-group-1"]');
+
+ if (radioGroup) {
+ const newRadio = document.createElement('pie-radio');
+ newRadio.setAttribute('value', counter.toString());
+ newRadio.textContent = `Option ${counter}`;
+
+ radioGroup.appendChild(newRadio);
+
+ counter++;
+ }
+ };
+
+ return html`
+
+ Option 1
+
+
+
+
+
+ Add another option
+
+
+ `;
+};
+
export const KeyboardNavigation = createStory(KeyboardNavigationTemplate, defaultArgs)();
+export const DynamicSlots = createStory(DynamicSlotsTemplate, defaultArgs)();
+
diff --git a/packages/components/pie-radio-group/src/index.ts b/packages/components/pie-radio-group/src/index.ts
index 47dbe72979..b02e234538 100644
--- a/packages/components/pie-radio-group/src/index.ts
+++ b/packages/components/pie-radio-group/src/index.ts
@@ -130,11 +130,22 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
* @param {Event} e - The slotchange event.
* @private
*/
- private _handleSlotChange (e: { target: HTMLSlotElement }): void {
+ private _handleLabelSlotChange (e: { target: HTMLSlotElement }): void {
const childNodes = e.target.assignedNodes({ flatten: true });
this._hasLabel = childNodes.length > 0;
}
+ /**
+ * Ensures all newly added radio buttons are not tabbable and inherit the name property
+ */
+ private _handleRadioSlotChange (): void {
+ // Make all (including any newly added) radio buttons impossible to tab to
+ // This is because by default, we are able to tab to each individual radio button.
+ // This is not the behaviour we want, so applying -1 tabindex prevents it.
+ this._slottedChildren.forEach((radio) => radio.setAttribute('tabindex', '-1'));
+ this._applyNameToChildren();
+ }
+
/**
* Renders the label element inside a legend, wrapping the slot content.
* @returns {TemplateResult } The template for the label slot.
@@ -142,8 +153,16 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
*/
private _renderWrappedLabel (): TemplateResult {
return this._hasLabel
- ? html``
- : html``;
+ ? html``
+ : html``;
+ }
+
+ private _applyNameToChildren () : void {
+ this._slottedChildren.forEach((radio) => {
+ if (this.name) {
+ radio.setAttribute('name', this.name);
+ }
+ });
}
protected updated (_changedProperties: PropertyValues): void {
@@ -158,13 +177,10 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
if (_changedProperties.has('status')) {
this._handleStatus();
}
- }
- protected firstUpdated (): void {
- // Make all radios impossible to tab to
- // This is because by default, we are able to tab to each individual radio button.
- // This is not the behaviour we want, so applying -1 tabindex prevents it.
- this._slottedChildren.forEach((radio) => radio.setAttribute('tabindex', '-1'));
+ if (_changedProperties.has('name')) {
+ this._applyNameToChildren();
+ }
}
connectedCallback (): void {
@@ -178,7 +194,13 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
this.addEventListener('focusout', this._handleFocusOut, { signal });
this.addEventListener('keydown', this._handleKeyDown, { signal });
+
+ // Warning! One edge case bug we've noticed is if the radio group is the first focusable element in the document,
+ // and you shift + tab out of it, when tabbing back in, it will focus the last radio instead of the first.
+ // Given it is quite an edge case, we will leave it for now.
document.addEventListener('keydown', this._updateShiftTabState.bind(this), { signal });
+
+ this._applyNameToChildren();
}
/**
@@ -342,7 +364,7 @@ export class PieRadioGroup extends FormControlMixin(RtlMixin(LitElement)) implem
aria-describedby=${hasAssistiveText ? assistiveTextId : nothing}
class="${classMap(classes)}">
${this._renderWrappedLabel()}
-
+
${hasAssistiveText ? html`
=> {
@@ -59,19 +66,65 @@ const expectFocusedRadioToBeChecked = async (page: Page, expect: Expect): Promis
test.describe('PieRadioGroup - Component tests new', () => {
let pageObject;
- test.describe('Keyboard navigation', () => {
- test.describe('Tab', () => {
- test.beforeEach(async ({ page }) => {
+ test.describe('props', () => {
+ test.describe('Name', () => {
+ test('Name prop is passed down to all slotted radio buttons', async ({ page }) => {
+ const expectedName = 'radio-group-1';
pageObject = new BasePage(page, 'radio-group');
await pageObject.load();
+
+ const radioSelectors = Object.keys(keyboardNavigationStorySelectors.radioGroup1.radios).map(Number) as Array;
+
+ await Promise.all(radioSelectors.map(async (radioSelectorKey) => {
+ const selector = keyboardNavigationStorySelectors.radioGroup1.radios[radioSelectorKey];
+ const radio = page.getByTestId(selector);
+
+ await expect(radio).toHaveAttribute('name', expectedName);
+ }));
});
+ test('Name prop is passed down to any dynamically added radio buttons', async ({ page }) => {
+ const expectedName = 'radio-group-1';
+
+ pageObject = new BasePage(page, 'radio-group--dynamic-slots');
+ await pageObject.load();
+
+ const addOptionsBtn = page.getByTestId(dynamicSlotsStorySelectors.addOptionBtn);
+
+ // Create 3 new options
+ await addOptionsBtn.click();
+ await addOptionsBtn.click();
+ await addOptionsBtn.click();
+
+ // Ensure correct number of radios
+ const radioButtons = page.getByTestId(dynamicSlotsStorySelectors.radioGroup1.self).locator('pie-radio');
+ await expect(radioButtons).toHaveCount(4);
+
+ const radioButtonsCount = await radioButtons.count();
+
+ const validateRadioButtonName = async (index: number) => {
+ const radio = radioButtons.nth(index);
+ await expect(radio).toHaveAttribute('name', expectedName);
+ };
+
+ await Promise.all(Array.from({ length: radioButtonsCount }, (_, i) => validateRadioButtonName(i)));
+ });
+ });
+ });
+
+ test.describe('Keyboard navigation', () => {
+ test.beforeEach(async ({ page }) => {
+ pageObject = new BasePage(page, 'radio-group');
+ await pageObject.load();
+ });
+
+ test.describe('Tab', () => {
test('Tab and no option selected focuses the first radio', async ({ page }) => {
// Tab 2 times to go button 1 -> Radio group 1
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
- const radio = page.getByTestId(selectors.radioGroup1.radios[1]);
+ const radio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[1]);
await expect(radio).toBeFocused();
});
@@ -83,7 +136,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
- const radio = page.getByTestId(selectors.radioGroup2.radios[4]);
+ const radio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup2.radios[4]);
await expect(radio).toBeFocused();
});
@@ -94,7 +147,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
- const button = page.getByTestId(selectors.button2);
+ const button = page.getByTestId(keyboardNavigationStorySelectors.button2);
await expect(button).toBeFocused();
});
@@ -114,7 +167,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('Shift+Tab');
- const radio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const radio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(radio).toBeFocused();
});
@@ -130,7 +183,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('Shift+Tab');
- const radio = page.getByTestId(selectors.radioGroup2.radios[4]);
+ const radio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup2.radios[4]);
await expect(radio).toBeFocused();
});
@@ -144,7 +197,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('Shift+Tab');
await page.keyboard.press('Shift+Tab');
- const button = page.getByTestId(selectors.button1);
+ const button = page.getByTestId(keyboardNavigationStorySelectors.button1);
await expect(button).toBeFocused();
});
@@ -164,13 +217,13 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowLeft');
// Ensure it's gone backwards and selected the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Ensure it's gone backwards once more and selected the second to last radio
await page.keyboard.press('ArrowLeft');
- const secondToLastRadio = page.getByTestId(selectors.radioGroup1.radios[4]);
+ const secondToLastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[4]);
await expect(secondToLastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -183,13 +236,13 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowUp');
// Ensure it's gone backwards and selected the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Ensure it's gone backwards once more and selected the second to last radio
await page.keyboard.press('ArrowUp');
- const secondToLastRadio = page.getByTestId(selectors.radioGroup1.radios[4]);
+ const secondToLastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[4]);
await expect(secondToLastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -206,14 +259,14 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowRight');
// Ensure we are on the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Press Arrow Right 1 more time to get back to the first radio
await page.keyboard.press('ArrowRight');
- const firstRadio = page.getByTestId(selectors.radioGroup1.radios[1]);
+ const firstRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[1]);
await expect(firstRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -230,14 +283,14 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowDown');
// Ensure we are on the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Press Arrow Down 1 more time to get back to the first radio
await page.keyboard.press('ArrowDown');
- const firstRadio = page.getByTestId(selectors.radioGroup1.radios[1]);
+ const firstRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[1]);
await expect(firstRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -250,7 +303,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowRight');
// Ensure it's gone forwards and selected the second radio
- const secondRadioButtonGroup1 = page.getByTestId(selectors.radioGroup1.radios[2]);
+ const secondRadioButtonGroup1 = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[2]);
await expect(secondRadioButtonGroup1).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
@@ -261,7 +314,7 @@ test.describe('PieRadioGroup - Component tests new', () => {
// Press right to move from the selected radio to the next
await page.keyboard.press('ArrowRight');
- const lastRadioButtonGroup2 = page.getByTestId(selectors.radioGroup2.radios[5]);
+ const lastRadioButtonGroup2 = page.getByTestId(keyboardNavigationStorySelectors.radioGroup2.radios[5]);
await expect(lastRadioButtonGroup2).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
@@ -295,14 +348,14 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowLeft');
// Ensure we are on the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Press Arrow Left 1 more time to get back to the first radio
await page.keyboard.press('ArrowLeft');
- const firstRadio = page.getByTestId(selectors.radioGroup1.radios[1]);
+ const firstRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[1]);
await expect(firstRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -315,13 +368,13 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowUp');
// Ensure it's gone backwards and selected the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Ensure it's gone backwards once more and selected the second to last radio
await page.keyboard.press('ArrowUp');
- const secondToLastRadio = page.getByTestId(selectors.radioGroup1.radios[4]);
+ const secondToLastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[4]);
await expect(secondToLastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -334,13 +387,13 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowRight');
// Ensure it's gone backwards and selected the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Ensure it's gone backwards once more and selected the second to last radio
await page.keyboard.press('ArrowRight');
- const secondToLastRadio = page.getByTestId(selectors.radioGroup1.radios[4]);
+ const secondToLastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[4]);
await expect(secondToLastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
@@ -357,14 +410,14 @@ test.describe('PieRadioGroup - Component tests new', () => {
await page.keyboard.press('ArrowDown');
// Ensure we are on the last radio
- const lastRadio = page.getByTestId(selectors.radioGroup1.radios[5]);
+ const lastRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[5]);
await expect(lastRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
// Press Arrow Down 1 more time to get back to the first radio
await page.keyboard.press('ArrowDown');
- const firstRadio = page.getByTestId(selectors.radioGroup1.radios[1]);
+ const firstRadio = page.getByTestId(keyboardNavigationStorySelectors.radioGroup1.radios[1]);
await expect(firstRadio).toBeFocused();
await expectFocusedRadioToBeChecked(page, expect);
});
diff --git a/packages/tools/pie-monorepo-utils/changeset-snapshot/test-aperture.js b/packages/tools/pie-monorepo-utils/changeset-snapshot/test-aperture.js
index 5f76b37877..e3847749f0 100644
--- a/packages/tools/pie-monorepo-utils/changeset-snapshot/test-aperture.js
+++ b/packages/tools/pie-monorepo-utils/changeset-snapshot/test-aperture.js
@@ -1,29 +1,40 @@
/* eslint-disable camelcase */
module.exports = async ({ github, context }, execa) => {
- await execa.command('yarn changeset:version --snapshot snapshot-release', { stdio: 'inherit' });
+ // Helper function to handle errors and post GitHub comments
+ const handleError = async (message, error) => {
+ console.error(`${message}:`, error);
+ await github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: `${message}:\n\`\`\`\n${error.message || error}\n\`\`\``,
+ });
+ };
- const releaseProcess = execa.command('yarn changeset:publish --no-git-tags --snapshot --tag snapshot-release');
- releaseProcess.stdout.pipe(process.stdout);
+ try {
+ await execa.command('yarn changeset:version --snapshot snapshot-release', { stdio: 'inherit' });
- const { stdout } = await releaseProcess;
+ const releaseProcess = execa.command('yarn changeset:publish --no-git-tags --snapshot --tag snapshot-release');
+ releaseProcess.stdout.pipe(process.stdout);
- const newTags = Array
- .from(stdout.matchAll(/New tag:\s+([^\s\n]+)/g))
- .map(([, tag]) => tag)
- .filter((tag) => !/^wc-.+$|pie-(monorepo|docs|storybook)/.test(tag));
+ const { stdout } = await releaseProcess;
- // Extract the snapshot version from one of the tags
- const [snapshotVersion] = newTags[0].match(/\d{14}$/);
+ const newTags = Array
+ .from(stdout.matchAll(/New tag:\s+([^\s\n]+)/g))
+ .map(([, tag]) => tag)
+ .filter((tag) => !/^wc-.+$|pie-(monorepo|docs|storybook)/.test(tag));
- // Extract package names by removing version and scope from the tags
- const packageNames = newTags.map((tag) => `@justeattakeaway/${tag.match(/pie-[\w-]+/)[0]}`);
+ // Extract the snapshot version from one of the tags
+ if (newTags.length === 0) {
+ throw new Error('No changed packages found! Please make sure you have added a changeset entry for the packages you would like to snapshot.');
+ }
- let body;
+ const [snapshotVersion] = newTags[0].match(/\d{14}$/);
+ const packageNames = newTags.map((tag) => `@justeattakeaway/${tag.match(/pie-[\w-]+/)[0]}`);
- if (newTags.length > 0) {
const multiple = newTags.length > 1;
- body = `@${context.actor} Your snapshot${multiple ? 's have' : ' has'} been published to npm!\n\nTest the snapshot${multiple ? 's' : ''} by updating your \`package.json\` with the newly-published version${multiple ? 's' : ''}:\n`;
+ let body = `@${context.actor} Your snapshot${multiple ? 's have' : ' has'} been published to npm!\n\nTest the snapshot${multiple ? 's' : ''} by updating your \`package.json\` with the newly-published version${multiple ? 's' : ''}:\n`;
if (multiple) {
body += `> [!NOTE]\n> If you have more than one of these packages installed, we suggest using the new snapshots for all of them to help avoid version conflicts.\n\n${newTags.map((tag) => `\`\`\`sh\nyarn up ${tag} --mode=update-lockfile\n\`\`\``).join('\n')}\nThen finally:\n\`\`\`sh\nyarn install\n\`\`\``;
@@ -45,23 +56,17 @@ module.exports = async ({ github, context }, execa) => {
},
});
} catch (error) {
- console.error(`Failed to dispatch workflow: ${error.message}`);
- await github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: `Failed to trigger PIE Aperture workflow: ${error.message}`,
- });
+ await handleError('Failed to dispatch workflow', error);
}
- } else {
- body = 'No changed packages found! Please make sure you have added a changeset entry for the packages you would like to snapshot.';
- }
- // Create a GitHub comment with the update instructions
- await github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body,
- });
+ // Create a GitHub comment with the update instructions
+ await github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body,
+ });
+ } catch (error) {
+ await handleError('An unexpected error occurred during the snapshot release process', error);
+ }
};