Skip to content

Commit 94ca2e5

Browse files
feat(toggle): add helperText and errorText properties (#30161)
Issue number: N/A --------- ## What is the current behavior? Toggle does not support helper and error text. ## What is the new behavior? Adds support for helper and error text, similar to input and textarea. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information - [Bottom Content: Preview](https://ionic-framework-git-rou-11552-ionic1.vercel.app/src/components/toggle/test/bottom-content) - [Item: Preview](https://ionic-framework-git-rou-11552-ionic1.vercel.app/src/components/toggle/test/item) --------- Co-authored-by: Brandy Smith <[email protected]> Co-authored-by: Maria Hutt <[email protected]>
1 parent 99d2f1c commit 94ca2e5

File tree

68 files changed

+567
-29
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+567
-29
lines changed

core/api.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,6 +1948,8 @@ ion-toggle,prop,checked,boolean,false,false,false
19481948
ion-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
19491949
ion-toggle,prop,disabled,boolean,false,false,false
19501950
ion-toggle,prop,enableOnOffLabels,boolean | undefined,config.get('toggleOnOffLabels'),false,false
1951+
ion-toggle,prop,errorText,string | undefined,undefined,false,false
1952+
ion-toggle,prop,helperText,string | undefined,undefined,false,false
19511953
ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
19521954
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
19531955
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
@@ -1981,8 +1983,11 @@ ion-toggle,css-prop,--track-background,ios
19811983
ion-toggle,css-prop,--track-background,md
19821984
ion-toggle,css-prop,--track-background-checked,ios
19831985
ion-toggle,css-prop,--track-background-checked,md
1986+
ion-toggle,part,error-text
19841987
ion-toggle,part,handle
1988+
ion-toggle,part,helper-text
19851989
ion-toggle,part,label
1990+
ion-toggle,part,supporting-text
19861991
ion-toggle,part,track
19871992

19881993
ion-toolbar,shadow

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,6 +3284,14 @@ export namespace Components {
32843284
* Enables the on/off accessibility switch labels within the toggle.
32853285
*/
32863286
"enableOnOffLabels": boolean | undefined;
3287+
/**
3288+
* Text that is placed under the toggle label and displayed when an error is detected.
3289+
*/
3290+
"errorText"?: string;
3291+
/**
3292+
* Text that is placed under the toggle label and displayed when no error is detected.
3293+
*/
3294+
"helperText"?: string;
32873295
/**
32883296
* 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`.
32893297
*/
@@ -8171,6 +8179,14 @@ declare namespace LocalJSX {
81718179
* Enables the on/off accessibility switch labels within the toggle.
81728180
*/
81738181
"enableOnOffLabels"?: boolean | undefined;
8182+
/**
8183+
* Text that is placed under the toggle label and displayed when an error is detected.
8184+
*/
8185+
"errorText"?: string;
8186+
/**
8187+
* Text that is placed under the toggle label and displayed when no error is detected.
8188+
*/
8189+
"helperText"?: string;
81748190
/**
81758191
* 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`.
81768192
*/
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Toggle - Bottom Content</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
<style>
16+
.grid {
17+
display: grid;
18+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
19+
grid-row-gap: 20px;
20+
grid-column-gap: 20px;
21+
}
22+
h2 {
23+
font-size: 12px;
24+
font-weight: normal;
25+
26+
color: #6f7378;
27+
28+
margin-top: 10px;
29+
}
30+
ion-toggle {
31+
width: 100%;
32+
}
33+
</style>
34+
</head>
35+
36+
<body>
37+
<ion-app>
38+
<ion-header>
39+
<ion-toolbar>
40+
<ion-title>Toggle - Bottom Content</ion-title>
41+
</ion-toolbar>
42+
</ion-header>
43+
44+
<ion-content id="content" class="ion-padding">
45+
<div class="grid">
46+
<div class="grid-item">
47+
<h2>No Hint</h2>
48+
<ion-toggle>Label</ion-toggle>
49+
</div>
50+
51+
<div class="grid-item">
52+
<h2>No Hint: Stacked</h2>
53+
<ion-toggle label-placement="stacked">Label</ion-toggle>
54+
</div>
55+
56+
<div class="grid-item">
57+
<h2>Helper Text: Label Start</h2>
58+
<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>
59+
</div>
60+
61+
<div class="grid-item">
62+
<h2>Helper Text: Label End</h2>
63+
<ion-toggle label-placement="end" helper-text="Helper text" error-text="Error text">Label</ion-toggle>
64+
</div>
65+
66+
<div class="grid-item">
67+
<h2>Helper Text: Label Stacked</h2>
68+
<ion-toggle label-placement="stacked" helper-text="Helper text" error-text="Error text">Label</ion-toggle>
69+
</div>
70+
71+
<div class="grid-item">
72+
<h2>Helper Text: Label Fixed</h2>
73+
<ion-toggle label-placement="fixed" helper-text="Helper text" error-text="Error text">Label</ion-toggle>
74+
</div>
75+
76+
<div class="grid-item">
77+
<h2>Error Text: Label Start</h2>
78+
<ion-toggle helper-text="Helper text" error-text="Error text" class="ion-invalid ion-touched"
79+
>Label</ion-toggle
80+
>
81+
</div>
82+
83+
<div class="grid-item">
84+
<h2>Error Text: Label End</h2>
85+
<ion-toggle
86+
label-placement="end"
87+
helper-text="Helper text"
88+
error-text="Error text"
89+
class="ion-invalid ion-touched"
90+
>Label</ion-toggle
91+
>
92+
</div>
93+
94+
<div class="grid-item">
95+
<h2>Error Text: Label Stacked</h2>
96+
<ion-toggle
97+
label-placement="stacked"
98+
helper-text="Helper text"
99+
error-text="Error text"
100+
class="ion-invalid ion-touched"
101+
>Label</ion-toggle
102+
>
103+
</div>
104+
105+
<div class="grid-item">
106+
<h2>Error Text: Label Fixed</h2>
107+
<ion-toggle
108+
label-placement="fixed"
109+
helper-text="Helper text"
110+
error-text="Error text"
111+
class="ion-invalid ion-touched"
112+
>Label</ion-toggle
113+
>
114+
</div>
115+
</div>
116+
117+
<button onclick="toggleValid()" class="expand">Toggle error</button>
118+
119+
<script>
120+
const toggles = document.querySelectorAll('ion-toggle[helper-text]');
121+
122+
function toggleValid() {
123+
toggles.forEach((toggle) => {
124+
toggle.classList.toggle('ion-invalid');
125+
toggle.classList.toggle('ion-touched');
126+
});
127+
}
128+
</script>
129+
</ion-content>
130+
</ion-app>
131+
</body>
132+
</html>
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
/**
5+
* Functionality is the same across modes & directions
6+
*/
7+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
8+
test.describe(title('toggle: bottom content functionality'), () => {
9+
test('should not render bottom content if no hint is enabled', async ({ page }) => {
10+
await page.setContent(`<ion-toggle>Label</ion-toggle>`, config);
11+
12+
const bottomEl = page.locator('ion-toggle .toggle-bottom');
13+
await expect(bottomEl).toHaveCount(0);
14+
});
15+
test('helper text should be visible initially', async ({ page }) => {
16+
await page.setContent(`<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>`, config);
17+
18+
const helperText = page.locator('ion-toggle .helper-text');
19+
const errorText = page.locator('ion-toggle .error-text');
20+
await expect(helperText).toBeVisible();
21+
await expect(helperText).toHaveText('Helper text');
22+
await expect(errorText).toBeHidden();
23+
});
24+
test('toggle should have an aria-describedby attribute when helper text is present', async ({ page }) => {
25+
await page.setContent(`<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>`, config);
26+
27+
const toggle = page.locator('ion-toggle');
28+
const helperText = page.locator('ion-toggle .helper-text');
29+
const helperTextId = await helperText.getAttribute('id');
30+
const ariaDescribedBy = await toggle.getAttribute('aria-describedby');
31+
32+
expect(ariaDescribedBy).toBe(helperTextId);
33+
});
34+
test('error text should be visible when toggle is invalid', async ({ page }) => {
35+
await page.setContent(
36+
`<ion-toggle class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">Label</ion-toggle>`,
37+
config
38+
);
39+
40+
const helperText = page.locator('ion-toggle .helper-text');
41+
const errorText = page.locator('ion-toggle .error-text');
42+
await expect(helperText).toBeHidden();
43+
await expect(errorText).toBeVisible();
44+
await expect(errorText).toHaveText('Error text');
45+
});
46+
47+
test('toggle should have an aria-describedby attribute when error text is present', async ({ page }) => {
48+
await page.setContent(
49+
`<ion-toggle class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">Label</ion-toggle>`,
50+
config
51+
);
52+
53+
const toggle = page.locator('ion-toggle');
54+
const errorText = page.locator('ion-toggle .error-text');
55+
const errorTextId = await errorText.getAttribute('id');
56+
const ariaDescribedBy = await toggle.getAttribute('aria-describedby');
57+
58+
expect(ariaDescribedBy).toBe(errorTextId);
59+
});
60+
test('toggle should have aria-invalid attribute when toggle is invalid', async ({ page }) => {
61+
await page.setContent(
62+
`<ion-toggle class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">Label</ion-toggle>`,
63+
config
64+
);
65+
66+
const toggle = page.locator('ion-toggle');
67+
68+
await expect(toggle).toHaveAttribute('aria-invalid');
69+
});
70+
test('toggle should not have aria-invalid attribute when toggle is valid', async ({ page }) => {
71+
await page.setContent(`<ion-toggle helper-text="Helper text" error-text="Error text">Label</ion-toggle>`, config);
72+
73+
const toggle = page.locator('ion-toggle');
74+
75+
await expect(toggle).not.toHaveAttribute('aria-invalid');
76+
});
77+
test('toggle should not have aria-describedby attribute when no hint or error text is present', async ({
78+
page,
79+
}) => {
80+
await page.setContent(`<ion-toggle>Label</ion-toggle>`, config);
81+
82+
const toggle = page.locator('ion-toggle');
83+
84+
await expect(toggle).not.toHaveAttribute('aria-describedby');
85+
});
86+
});
87+
});
88+
89+
/**
90+
* Rendering is different across modes
91+
*/
92+
configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
93+
test.describe(title('toggle: helper text rendering'), () => {
94+
// Check the default label placement, end, and stacked
95+
[undefined, 'end', 'stacked'].forEach((labelPlacement) => {
96+
test(`${
97+
labelPlacement ? `${labelPlacement} label - ` : ''
98+
}should not have visual regressions when rendering helper text`, async ({ page }) => {
99+
await page.setContent(
100+
`<ion-toggle ${
101+
labelPlacement ? `label-placement="${labelPlacement}"` : ''
102+
} helper-text="Helper text">Label</ion-toggle>`,
103+
config
104+
);
105+
106+
const bottomEl = page.locator('ion-toggle');
107+
await expect(bottomEl).toHaveScreenshot(
108+
screenshot(`toggle-helper-text${labelPlacement ? `-${labelPlacement}` : ''}`)
109+
);
110+
});
111+
112+
test(`${
113+
labelPlacement ? `${labelPlacement} label - ` : ''
114+
}should not have visual regressions when rendering helper text with wrapping text`, async ({ page }) => {
115+
await page.setContent(
116+
`<ion-toggle ${
117+
labelPlacement ? `label-placement="${labelPlacement}"` : ''
118+
} helper-text="Helper text helper text helper text helper text helper text helper text helper text helper text helper text">Label</ion-toggle>`,
119+
config
120+
);
121+
122+
const bottomEl = page.locator('ion-toggle');
123+
await expect(bottomEl).toHaveScreenshot(
124+
screenshot(`toggle-helper-text${labelPlacement ? `-${labelPlacement}` : ''}-wrapping`)
125+
);
126+
});
127+
});
128+
});
129+
130+
test.describe(title('toggle: error text rendering'), () => {
131+
test('should not have visual regressions when rendering error text', async ({ page }) => {
132+
await page.setContent(
133+
`<ion-toggle class="ion-invalid ion-touched" error-text="Error text">Label</ion-toggle>`,
134+
config
135+
);
136+
137+
const bottomEl = page.locator('ion-toggle');
138+
await expect(bottomEl).toHaveScreenshot(screenshot(`toggle-error-text`));
139+
});
140+
test('should not have visual regressions when rendering error text with a stacked label', async ({ page }) => {
141+
await page.setContent(
142+
`<ion-toggle class="ion-invalid ion-touched" error-text="Error text" label-placement="stacked">Label</ion-toggle>`,
143+
config
144+
);
145+
146+
const bottomEl = page.locator('ion-toggle');
147+
await expect(bottomEl).toHaveScreenshot(screenshot(`toggle-error-text-stacked-label`));
148+
});
149+
});
150+
});
151+
152+
/**
153+
* Customizing supporting text is the same across modes and directions
154+
*/
155+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
156+
test.describe(title('toggle: supporting text customization'), () => {
157+
test('should not have visual regressions when rendering helper text with custom css', async ({ page }) => {
158+
await page.setContent(
159+
`
160+
<style>
161+
ion-toggle::part(supporting-text) {
162+
font-size: 20px;
163+
}
164+
165+
ion-toggle::part(helper-text) {
166+
color: green;
167+
}
168+
</style>
169+
<ion-toggle helper-text="Helper text">Label</ion-toggle>
170+
`,
171+
config
172+
);
173+
174+
const helperText = page.locator('ion-toggle');
175+
await expect(helperText).toHaveScreenshot(screenshot(`toggle-helper-text-custom-css`));
176+
});
177+
test('should not have visual regressions when rendering error text with custom css', async ({ page }) => {
178+
await page.setContent(
179+
`
180+
<style>
181+
ion-toggle::part(supporting-text) {
182+
font-size: 20px;
183+
}
184+
185+
ion-toggle::part(error-text) {
186+
color: purple;
187+
}
188+
</style>
189+
<ion-toggle class="ion-invalid ion-touched" error-text="Error text">Label</ion-toggle>
190+
`,
191+
config
192+
);
193+
194+
const errorText = page.locator('ion-toggle');
195+
await expect(errorText).toHaveScreenshot(screenshot(`toggle-error-text-custom-css`));
196+
});
197+
});
198+
});
2.84 KB
3.23 KB
2.73 KB
2.52 KB
2.87 KB
2.56 KB

0 commit comments

Comments
 (0)