Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(toggle): add helperText and errorText properties #30161

Open
wants to merge 8 commits into
base: feature-8.5
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1940,6 +1940,8 @@ ion-toggle,prop,checked,boolean,false,false,false
ion-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-toggle,prop,disabled,boolean,false,false,false
ion-toggle,prop,enableOnOffLabels,boolean | undefined,config.get('toggleOnOffLabels'),false,false
ion-toggle,prop,errorText,string | undefined,undefined,false,false
ion-toggle,prop,helperText,string | undefined,undefined,false,false
ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
Expand Down Expand Up @@ -1972,8 +1974,11 @@ ion-toggle,css-prop,--track-background,ios
ion-toggle,css-prop,--track-background,md
ion-toggle,css-prop,--track-background-checked,ios
ion-toggle,css-prop,--track-background-checked,md
ion-toggle,part,error-text
ion-toggle,part,handle
ion-toggle,part,helper-text
ion-toggle,part,label
ion-toggle,part,supporting-text
ion-toggle,part,track

ion-toolbar,shadow
Expand Down
16 changes: 16 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,14 @@ export namespace Components {
* Enables the on/off accessibility switch labels within the toggle.
*/
"enableOnOffLabels": boolean | undefined;
/**
* Text that is placed under the toggle label and displayed when an error is detected.
*/
"errorText"?: string;
/**
* Text that is placed under the toggle label and displayed when no error is detected.
*/
"helperText"?: string;
/**
* How to pack the label and toggle within a line. `"start"`: The label and toggle will appear on the left in LTR and on the right in RTL. `"end"`: The label and toggle will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and toggle will appear on opposite ends of the line with space between the two elements. Setting this property will change the toggle `display` to `block`.
*/
Expand Down Expand Up @@ -8127,6 +8135,14 @@ declare namespace LocalJSX {
* Enables the on/off accessibility switch labels within the toggle.
*/
"enableOnOffLabels"?: boolean | undefined;
/**
* Text that is placed under the toggle label and displayed when an error is detected.
*/
"errorText"?: string;
/**
* Text that is placed under the toggle label and displayed when no error is detected.
*/
"helperText"?: string;
/**
* How to pack the label and toggle within a line. `"start"`: The label and toggle will appear on the left in LTR and on the right in RTL. `"end"`: The label and toggle will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and toggle will appear on opposite ends of the line with space between the two elements. Setting this property will change the toggle `display` to `block`.
*/
Expand Down
132 changes: 132 additions & 0 deletions core/src/components/toggle/test/bottom-content/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Toggle - Bottom Content</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;

color: #6f7378;

margin-top: 10px;
}
ion-toggle {
width: 100%;
}
</style>
</head>

<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Toggle - Bottom Content</ion-title>
</ion-toolbar>
</ion-header>

<ion-content id="content" class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>No Hint</h2>
<ion-toggle>Label</ion-toggle>
</div>

<div class="grid-item">
<h2>No Hint: Stacked</h2>
<ion-toggle label-placement="stacked">Label</ion-toggle>
</div>

<div class="grid-item">
<h2>Helper Text: Label Start</h2>
<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>
</div>

<div class="grid-item">
<h2>Helper Text: Label End</h2>
<ion-toggle label-placement="end" helper-text="Helper text" error-text="Error text">Label</ion-toggle>
</div>

<div class="grid-item">
<h2>Helper Text: Label Stacked</h2>
<ion-toggle label-placement="stacked" helper-text="Helper text" error-text="Error text">Label</ion-toggle>
</div>

<div class="grid-item">
<h2>Helper Text: Label Fixed</h2>
<ion-toggle label-placement="fixed" helper-text="Helper text" error-text="Error text">Label</ion-toggle>
</div>

<div class="grid-item">
<h2>Error Text: Label Start</h2>
<ion-toggle helper-text="Helper text" error-text="Error text" class="ion-invalid ion-touched"
>Label</ion-toggle
>
</div>

<div class="grid-item">
<h2>Error Text: Label End</h2>
<ion-toggle
label-placement="end"
helper-text="Helper text"
error-text="Error text"
class="ion-invalid ion-touched"
>Label</ion-toggle
>
</div>

<div class="grid-item">
<h2>Error Text: Label Stacked</h2>
<ion-toggle
label-placement="stacked"
helper-text="Helper text"
error-text="Error text"
class="ion-invalid ion-touched"
>Label</ion-toggle
>
</div>

<div class="grid-item">
<h2>Error Text: Label Fixed</h2>
<ion-toggle
label-placement="fixed"
helper-text="Helper text"
error-text="Error text"
class="ion-invalid ion-touched"
>Label</ion-toggle
>
</div>
</div>

<button onclick="toggleValid()" class="expand">Toggle error</button>

<script>
const toggles = document.querySelectorAll('ion-toggle[helper-text]');

function toggleValid() {
toggles.forEach((toggle) => {
toggle.classList.toggle('ion-invalid');
toggle.classList.toggle('ion-touched');
});
}
</script>
</ion-content>
</ion-app>
</body>
</html>
196 changes: 196 additions & 0 deletions core/src/components/toggle/test/bottom-content/toggle.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

/**
* Functionality is the same across modes & directions
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('toggle: bottom content functionality'), () => {
test('should not render bottom content if no hint is enabled', async ({ page }) => {
await page.setContent(`<ion-toggle>Label</ion-toggle>`, config);

const bottomEl = page.locator('ion-toggle .toggle-bottom');
await expect(bottomEl).toHaveCount(0);
});
test('helper text should be visible initially', async ({ page }) => {
await page.setContent(`<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>`, config);

const helperText = page.locator('ion-toggle .helper-text');
const errorText = page.locator('ion-toggle .error-text');
await expect(helperText).toBeVisible();
await expect(helperText).toHaveText('Helper text');
await expect(errorText).toBeHidden();
});
test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => {
await page.setContent(`<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>`, config);

const input = page.locator('ion-toggle input[type=checkbox]');
const helperText = page.locator('ion-toggle .helper-text');
const helperTextId = await helperText.getAttribute('id');
const ariaDescribedBy = await input.getAttribute('aria-describedby');

expect(ariaDescribedBy).toBe(helperTextId);
});
test('error text should be visible when toggle is invalid', async ({ page }) => {
await page.setContent(
`<ion-toggle class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">Label</ion-toggle>`,
config
);

const helperText = page.locator('ion-toggle .helper-text');
const errorText = page.locator('ion-toggle .error-text');
await expect(helperText).toBeHidden();
await expect(errorText).toBeVisible();
await expect(errorText).toHaveText('Error text');
});

test('input should have an aria-describedby attribute when error text is present', async ({ page }) => {
await page.setContent(
`<ion-toggle class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">Label</ion-toggle>`,
config
);

const input = page.locator('ion-toggle input[type=checkbox]');
const errorText = page.locator('ion-toggle .error-text');
const errorTextId = await errorText.getAttribute('id');
const ariaDescribedBy = await input.getAttribute('aria-describedby');

expect(ariaDescribedBy).toBe(errorTextId);
});
test('input should have aria-invalid attribute when toggle is invalid', async ({ page }) => {
await page.setContent(
`<ion-toggle class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">Label</ion-toggle>`,
config
);

const input = page.locator('ion-toggle input[type=checkbox]');

await expect(input).toHaveAttribute('aria-invalid');
});
test('input should not have aria-invalid attribute when toggle is valid', async ({ page }) => {
await page.setContent(`<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>`, config);

const input = page.locator('ion-toggle input[type=checkbox]');

await expect(input).not.toHaveAttribute('aria-invalid');
});
test('input should not have aria-describedby attribute when no hint or error text is present', async ({ page }) => {
await page.setContent(`<ion-toggle>Label</ion-toggle>`, config);

const input = page.locator('ion-toggle input[type=checkbox]');

await expect(input).not.toHaveAttribute('aria-describedby');
});
});
});

/**
* Rendering is different across modes
*/
configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('toggle: helper text rendering'), () => {
// Check the default label placement, end, and stacked
[undefined, 'end', 'stacked'].forEach((labelPlacement) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reasoning for not checking fixed?

test(`${
labelPlacement ? `${labelPlacement} label - ` : ''
}should not have visual regressions when rendering helper text`, async ({ page }) => {
await page.setContent(
`<ion-toggle ${
labelPlacement ? `label-placement="${labelPlacement}"` : ''
} helper-text="Helper text">Label</ion-toggle>`,
config
);

const bottomEl = page.locator('ion-toggle');
await expect(bottomEl).toHaveScreenshot(
screenshot(`toggle-helper-text${labelPlacement ? `-${labelPlacement}` : ''}`)
);
});

test(`${
labelPlacement ? `${labelPlacement} label - ` : ''
}should not have visual regressions when rendering helper text with wrapping text`, async ({ page }) => {
await page.setContent(
`<ion-toggle ${
labelPlacement ? `label-placement="${labelPlacement}"` : ''
} helper-text="Helper text helper text helper text helper text helper text helper text helper text helper text helper text">Label</ion-toggle>`,
config
);

const bottomEl = page.locator('ion-toggle');
await expect(bottomEl).toHaveScreenshot(
screenshot(`toggle-helper-text${labelPlacement ? `-${labelPlacement}` : ''}-wrapping`)
);
});
});
});

test.describe(title('toggle: error text rendering'), () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does error text not get a check for the end or fixed placement?

test('should not have visual regressions when rendering error text', async ({ page }) => {
await page.setContent(
`<ion-toggle class="ion-invalid ion-touched" error-text="Error text">Label</ion-toggle>`,
config
);

const bottomEl = page.locator('ion-toggle');
await expect(bottomEl).toHaveScreenshot(screenshot(`toggle-error-text`));
});
test('should not have visual regressions when rendering error text with a stacked label', async ({ page }) => {
await page.setContent(
`<ion-toggle class="ion-invalid ion-touched" error-text="Error text" label-placement="stacked">Label</ion-toggle>`,
config
);

const bottomEl = page.locator('ion-toggle');
await expect(bottomEl).toHaveScreenshot(screenshot(`toggle-error-text-stacked-label`));
});
});
});

/**
* Customizing supporting text is the same across modes and directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('toggle: supporting text customization'), () => {
test('should not have visual regressions when rendering helper text with custom css', async ({ page }) => {
await page.setContent(
`
<style>
ion-toggle::part(supporting-text) {
font-size: 20px;
}

ion-toggle::part(helper-text) {
color: green;
}
</style>
<ion-toggle helper-text="Helper text">Label</ion-toggle>
`,
config
);

const helperText = page.locator('ion-toggle');
await expect(helperText).toHaveScreenshot(screenshot(`toggle-helper-text-custom-css`));
});
test('should not have visual regressions when rendering error text with custom css', async ({ page }) => {
await page.setContent(
`
<style>
ion-toggle::part(supporting-text) {
font-size: 20px;
}

ion-toggle::part(error-text) {
color: purple;
}
</style>
<ion-toggle class="ion-invalid ion-touched" error-text="Error text">Label</ion-toggle>
`,
config
);

const errorText = page.locator('ion-toggle');
await expect(errorText).toHaveScreenshot(screenshot(`toggle-error-text-custom-css`));
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading