Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/small-mugs-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@justeattakeaway/pie-switch": minor
---

- [Added] - Wrapped the checked value with the [lit live directive ](https://lit.dev/docs/api/directives/#live) to ensure the DOM and component state stay in sync.
- [Added] - Missing `defaultChecked` prop and form rest logic
- [Changed] - Refactored the toggle between leading and trailing labels to be handled via CSS.
- [Changed] - Updated the component to use a `<div>` as the parent wrapper when the label prop isn’t provided. This avoids nested `<label>` elements when consumers wrap the component with [an external label](https://webc.pie.design/?path=/docs/components-switch--overview#external-labels), which is an [invalid](https://forum.freecodecamp.org/t/nesting-label/462466) HTML pattern
11 changes: 10 additions & 1 deletion apps/pie-storybook/stories/pie-switch.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ type SwitchStoryMeta = Meta<SwitchProps>;

const defaultArgs: SwitchProps = {
...defaultProps,
label: 'Label',
aria: {
label: 'switch label',
describedBy: 'switch description',
},
label: 'Label',
name: 'switch',
value: 'switchValue',
};
Expand All @@ -33,6 +33,13 @@ const switchStoryMeta: SwitchStoryMeta = {
summary: defaultProps.checked,
},
},
defaultChecked: {
description: 'The default checked state of the switch (not necessarily the same as the current checked state). Used when the switch is part of a form that is reset.',
control: 'boolean',
defaultValue: {
summary: defaultProps.defaultChecked,
},
},
disabled: {
description: 'Same as the HTML disabled attribute - indicates whether the switch is disabled or not',
control: 'boolean',
Expand Down Expand Up @@ -107,6 +114,7 @@ const Template : TemplateFunction<SwitchProps> = (props) => {
const {
aria,
checked,
defaultChecked,
disabled,
label,
labelPlacement,
Expand All @@ -125,6 +133,7 @@ const Template : TemplateFunction<SwitchProps> = (props) => {
.aria="${aria}"
?required="${required}"
?checked="${checked}"
?defaultChecked="${defaultChecked}"
?disabled="${disabled}"
@change="${changeAction}">
</pie-switch>`;
Expand Down
95 changes: 54 additions & 41 deletions apps/pie-storybook/stories/testing/pie-switch.test.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { html, nothing } from 'lit';
import { type Meta } from '@storybook/web-components';

import '@justeattakeaway/pie-webc/components/switch';
import '@justeattakeaway/pie-webc/components/button';
import { type SwitchProps, labelPlacements, defaultProps } from '@justeattakeaway/pie-webc/components/switch';
import '@justeattakeaway/pie-icons-webc/dist/IconCheck.js';

Expand All @@ -13,11 +14,11 @@ type SwitchStoryMeta = Meta<SwitchProps>;

const defaultArgs: SwitchProps = {
...defaultProps,
label: 'Label',
aria: {
label: 'switch label',
describedBy: 'switch description',
},
label: 'Label',
name: 'switch',
value: 'switchValue',
};
Expand All @@ -32,6 +33,12 @@ const switchStoryMeta: SwitchStoryMeta = {
summary: defaultProps.checked,
},
},
defaultChecked: {
control: 'boolean',
defaultValue: {
summary: defaultProps.defaultChecked,
},
},
disabled: {
control: 'boolean',
defaultValue: {
Expand Down Expand Up @@ -89,43 +96,57 @@ const changeAction = () => console.info(EXPECTED_EVENT_MESSAGE);
const submitAction = (event: Event) => {
event.preventDefault(); // Prevent the actual submission

const formLog = document.querySelector('#formLog') as HTMLElement;
const form = event.currentTarget as HTMLFormElement;
const formLog = form.parentElement?.querySelector('#formLog') as HTMLElement | null;
const output = form.parentElement?.querySelector('#formDataOutput') as HTMLDivElement | null;

// Display a success message to the user when they submit the form
formLog.innerHTML = 'Form submitted!';
formLog.style.display = 'block';
if (formLog) {
formLog.innerHTML = 'Form submitted!';
formLog.style.display = 'block';
}

// Serialize form data into an object so tests can assert submitted payload.
const formData = new FormData(form);
const formDataObj: Record<string, unknown> = {};

formData.forEach((value, key) => {
formDataObj[key] = value;
});

if (output) {
output.innerText = JSON.stringify(formDataObj);
}

// Reset the success message after roughly 8 seconds
setTimeout(() => {
formLog.innerHTML = '';
formLog.style.display = 'none';
if (formLog) {
formLog.innerHTML = '';
formLog.style.display = 'none';
}
}, 8000);
};

const submitActionTestForm = (event: Event) => {
event.preventDefault(); // Prevent the actual submission

const form = document.querySelector('#testForm') as HTMLFormElement;
const resetAction = (event: Event) => {
const form = event.currentTarget as HTMLFormElement;
const formLog = form.parentElement?.querySelector('#formLog') as HTMLElement | null;
const output = form.parentElement?.querySelector('#formDataOutput') as HTMLDivElement | null;

// Serialize form data
const formData = new FormData(form);
const formDataObj: Record<string, unknown> = {};

// Serialize form data into an object
formData.forEach((value, key) => { formDataObj[key] = value; });
if (formLog) {
formLog.innerHTML = '';
formLog.style.display = 'none';
}

// Append serialized form data as JSON to a hidden element
const dataHolder = document.createElement('div');
dataHolder.id = 'formDataJson';
dataHolder.textContent = JSON.stringify(formDataObj);
dataHolder.style.display = 'none';
document.body.appendChild(dataHolder);
if (output) {
output.innerText = '';
}
};

const Template : TemplateFunction<SwitchProps> = (props) => {
const {
aria,
checked,
defaultChecked,
disabled,
label,
labelPlacement,
Expand All @@ -144,35 +165,27 @@ const Template : TemplateFunction<SwitchProps> = (props) => {
.aria="${aria}"
?required="${required}"
?checked="${checked}"
?defaultChecked="${defaultChecked}"
?disabled="${disabled}"
@change="${changeAction}">
</pie-switch>`;
};

const FormTemplate: TemplateFunction<SwitchProps> = (props: SwitchProps) => html`
<p id="formLog" style="display: none; font-size: 2rem; color: var(--dt-color-support-positive);"></p>
<form id="testForm" @submit="${submitAction}">

<section>
${Template({
...props,
})}
</section>
<button id="submitButton" type="submit"">Submit</button>
</form>
`;

const TestFormTemplate: TemplateFunction<SwitchProps> = (props: SwitchProps) => html`
<p id="formLog" style="display: none; font-size: 2rem; color: var(--dt-color-support-positive);"></p>
<form id="testForm" @submit="${submitActionTestForm}" action="/default-endpoint" method="POST">
<form id="testForm" @submit="${submitAction}" @reset="${resetAction}">

<section>
${Template({
...props,
})}
</section>
<button id="submitButton" type="submit">Submit</button>
<div style="display: flex; gap: var(--dt-spacing-b); align-items: center; margin-top: var(--dt-spacing-c);">
<pie-button id="resetButton" type="reset" variant="secondary">Reset</pie-button>
<pie-button id="submitButton" type="submit">Submit</pie-button>
</div>
</form>
<div id="formDataOutput"></div>
`;

const ExternalLabelsTemplate: TemplateFunction<SwitchProps> = (props: SwitchProps) => html`
Expand All @@ -183,6 +196,7 @@ const ExternalLabelsTemplate: TemplateFunction<SwitchProps> = (props: SwitchProp
name="${props.name || nothing}"
value="${props.value || nothing}"
?checked="${props.checked}"
?defaultChecked="${props.defaultChecked}"
@change="${changeAction}">
</pie-switch>

Expand All @@ -195,6 +209,7 @@ const ExternalLabelsTemplate: TemplateFunction<SwitchProps> = (props: SwitchProp
data-test-id="external-switch-wrapping"
name="${props.name || nothing}"
value="${props.value || nothing}"
?defaultChecked="${props.defaultChecked}"
@change="${changeAction}">
</pie-switch>
</label>
Expand All @@ -208,16 +223,16 @@ const ExternalLabelsTemplate: TemplateFunction<SwitchProps> = (props: SwitchProp
data-test-id="external-switch-multi"
name="${props.name || nothing}"
value="${props.value || nothing}"
?defaultChecked="${props.defaultChecked}"
@change="${changeAction}">
</pie-switch>

`;

const createSwitchStory = createStory(Template, defaultArgs);

const createSwitchStoryWithForm = createStory<SwitchProps>(FormTemplate, defaultArgs);

const createSwitchTestStoryWithForm = createStory<SwitchProps>(TestFormTemplate, defaultArgs);

const createSwitchStoryWithExternalLabels = createStory<SwitchProps>(ExternalLabelsTemplate, defaultArgs);

const formIntegrationOnly = {
Expand All @@ -236,8 +251,6 @@ export const Default = createSwitchStory({}, {

export const FormIntegration = createSwitchStoryWithForm();

export const TestFormIntegration = createSwitchTestStoryWithForm();

export const ExternalLabels = createSwitchStoryWithExternalLabels();

const baseSharedPropsMatrix: Partial<Record<keyof SwitchProps, unknown[]>> = {
Expand Down
1 change: 1 addition & 0 deletions packages/components/pie-switch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Ideally, you should install the component using the **`@justeattakeaway/pie-webc
| Prop | Options | Description | Default |
|------------------|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------|
| `checked` | `true`, `false` | Same as the HTML `checked` attribute; indicates whether the switch is on or off. | `false` |
| `defaultChecked` | `true`, `false` | Sets the default checked state for the switch. This does not directly set the initial checked state when the page loads, use `checked` for that. If the switch is inside a form which is reset, the `checked` state will be updated to match `defaultChecked`. | `false` |
| `disabled` | `true`, `false` | Same as the HTML `disabled` attribute; indicates whether the switch is disabled or not. | `false` |
| `required` | `true`, `false` | Same as the HTML `required` attribute; indicates whether the switch must be turned on when submitted as part of a form. | `false` |
| `label` | — | The label text for the switch. | — |
Expand Down
6 changes: 6 additions & 0 deletions packages/components/pie-switch/src/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export interface SwitchProps {
* Same as the HTML checked attribute - indicates whether the switch is on or off
*/
checked?: boolean;
/**
* The default checked state of the switch (not necessarily the same as the current checked state).
* Used when the switch is part of a form that is reset.
*/
defaultChecked?: boolean;
/**
* Same as the HTML required attribute - indicates whether the switch must be turned or not
*/
Expand Down Expand Up @@ -53,6 +58,7 @@ export type DefaultProps = ComponentDefaultProps<SwitchProps, keyof Omit<SwitchP

export const defaultProps: DefaultProps = {
checked: false,
defaultChecked: false,
disabled: false,
labelPlacement: 'leading',
required: false,
Expand Down
Loading
Loading