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 <6577830+brandyscarney@users.noreply.github.com> Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
@@ -1948,6 +1948,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
|
||||
@@ -1981,8 +1983,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
|
||||
|
||||
16
core/src/components.d.ts
vendored
@@ -3284,6 +3284,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`.
|
||||
*/
|
||||
@@ -8171,6 +8179,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`.
|
||||
*/
|
||||
|
||||
132
core/src/components/toggle/test/bottom-content/index.html
Normal file
@@ -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>
|
||||
198
core/src/components/toggle/test/bottom-content/toggle.e2e.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
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('toggle 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 toggle = page.locator('ion-toggle');
|
||||
const helperText = page.locator('ion-toggle .helper-text');
|
||||
const helperTextId = await helperText.getAttribute('id');
|
||||
const ariaDescribedBy = await toggle.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('toggle 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 toggle = page.locator('ion-toggle');
|
||||
const errorText = page.locator('ion-toggle .error-text');
|
||||
const errorTextId = await errorText.getAttribute('id');
|
||||
const ariaDescribedBy = await toggle.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaDescribedBy).toBe(errorTextId);
|
||||
});
|
||||
test('toggle 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 toggle = page.locator('ion-toggle');
|
||||
|
||||
await expect(toggle).toHaveAttribute('aria-invalid');
|
||||
});
|
||||
test('toggle 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 toggle = page.locator('ion-toggle');
|
||||
|
||||
await expect(toggle).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
test('toggle 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 toggle = page.locator('ion-toggle');
|
||||
|
||||
await expect(toggle).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) => {
|
||||
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'), () => {
|
||||
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`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -47,6 +47,15 @@
|
||||
<ion-content id="content" class="ion-padding">
|
||||
<h1>Placement Start</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default Justify</h2>
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-toggle label-placement="start">Enable Notifications</ion-toggle>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Justify Start</h2>
|
||||
<ion-list>
|
||||
@@ -77,6 +86,15 @@
|
||||
|
||||
<h1>Placement End</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default Justify</h2>
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-toggle label-placement="end">Enable Notifications</ion-toggle>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Justify Start</h2>
|
||||
<ion-list>
|
||||
@@ -107,6 +125,15 @@
|
||||
|
||||
<h1>Placement Fixed</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default Justify</h2>
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-toggle label-placement="fixed">Enable Notifications</ion-toggle>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Justify Start</h2>
|
||||
<ion-list>
|
||||
@@ -167,6 +194,33 @@
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<ion-item>
|
||||
<ion-toggle justify="start" alignment="start">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Enable Notifications Enable Notifications Enable Notifications
|
||||
</ion-label>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<ion-item>
|
||||
<ion-toggle label-placement="end">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Enable Notifications Enable Notifications Enable Notifications
|
||||
</ion-label>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<ion-item>
|
||||
<ion-toggle label-placement="end" alignment="start">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Enable Notifications Enable Notifications Enable Notifications
|
||||
</ion-label>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
@@ -53,7 +53,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
|
||||
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('toggle: long label in item'), () => {
|
||||
test('should render margins correctly when using long label in item', async ({ page }) => {
|
||||
test('should not have visual regressions when using long label in item', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-list>
|
||||
@@ -71,8 +71,27 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('toggle: end label in item'), () => {
|
||||
test('should not have visual regressions when using end label in item', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-list>
|
||||
<ion-radio-group>
|
||||
<ion-item>
|
||||
<ion-toggle label-placement="end">Enable Notifications</ion-toggle>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
`,
|
||||
config
|
||||
);
|
||||
const list = page.locator('ion-list');
|
||||
await expect(list).toHaveScreenshot(screenshot(`toggle-end-label-in-item`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('toggle: stacked label in item'), () => {
|
||||
test('should render margins correctly when using stacked label in item', async ({ page }) => {
|
||||
test('should not have visual regressions when using stacked label in item', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-list>
|
||||
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -134,45 +134,51 @@ input {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Toggle Justify
|
||||
// --------------------------------------------------
|
||||
// Toggle Bottom Content
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.toggle-bottom {
|
||||
@include padding(4px, null, null, null);
|
||||
|
||||
display: flex;
|
||||
|
||||
:host(.toggle-justify-space-between) .toggle-wrapper {
|
||||
justify-content: space-between;
|
||||
|
||||
font-size: dynamic-font(12px);
|
||||
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
:host(.toggle-justify-start) .toggle-wrapper {
|
||||
justify-content: start;
|
||||
:host(.toggle-label-placement-stacked) .toggle-bottom {
|
||||
font-size: dynamic-font(16px);
|
||||
}
|
||||
|
||||
:host(.toggle-justify-end) .toggle-wrapper {
|
||||
justify-content: end;
|
||||
// Toggle Hint Text
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Error text should only be shown when .ion-invalid is
|
||||
* present on the checkbox. Otherwise the helper text should
|
||||
* be shown.
|
||||
*/
|
||||
.toggle-bottom .error-text {
|
||||
display: none;
|
||||
|
||||
color: ion-color(danger, base);
|
||||
}
|
||||
|
||||
// Toggle Align
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
:host(.toggle-alignment-start) .toggle-wrapper {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
:host(.toggle-alignment-center) .toggle-wrapper {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Justify Content & Align Items
|
||||
// ---------------------------------------------
|
||||
|
||||
// The toggle should be displayed as block when either justify
|
||||
// or alignment is set; otherwise, these properties will have no
|
||||
// visible effect.
|
||||
:host(.toggle-justify-space-between),
|
||||
:host(.toggle-justify-start),
|
||||
:host(.toggle-justify-end),
|
||||
:host(.toggle-alignment-start),
|
||||
:host(.toggle-alignment-center) {
|
||||
.toggle-bottom .helper-text {
|
||||
display: block;
|
||||
|
||||
color: $text-color-step-300;
|
||||
}
|
||||
|
||||
:host(.ion-touched.ion-invalid) .toggle-bottom .error-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host(.ion-touched.ion-invalid) .toggle-bottom .helper-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Toggle Label Placement - Start
|
||||
@@ -204,6 +210,8 @@ input {
|
||||
*/
|
||||
:host(.toggle-label-placement-end) .toggle-wrapper {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,6 +255,8 @@ input {
|
||||
*/
|
||||
:host(.toggle-label-placement-stacked) .toggle-wrapper {
|
||||
flex-direction: column;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:host(.toggle-label-placement-stacked) .label-text-wrapper {
|
||||
@@ -274,6 +284,46 @@ input {
|
||||
@include transform-origin(center, top);
|
||||
}
|
||||
|
||||
// Toggle Justify
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.toggle-justify-space-between) .toggle-wrapper {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:host(.toggle-justify-start) .toggle-wrapper {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
:host(.toggle-justify-end) .toggle-wrapper {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
// Toggle Align
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.toggle-alignment-start) .toggle-wrapper {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
:host(.toggle-alignment-center) .toggle-wrapper {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Justify Content & Align Items
|
||||
// ---------------------------------------------
|
||||
|
||||
// The toggle should be displayed as block when either justify
|
||||
// or alignment is set; otherwise, these properties will have no
|
||||
// visible effect.
|
||||
:host(.toggle-justify-space-between),
|
||||
:host(.toggle-justify-start),
|
||||
:host(.toggle-justify-end),
|
||||
:host(.toggle-alignment-start),
|
||||
:host(.toggle-alignment-center) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Toggle Background Track: Unchecked
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ import type { ToggleChangeEventDetail } from './toggle-interface';
|
||||
* @part track - The background track of the toggle.
|
||||
* @part handle - The toggle handle, or knob, used to change the checked state.
|
||||
* @part label - The label text describing the toggle.
|
||||
* @part supporting-text - Supporting text displayed beneath the toggle label.
|
||||
* @part helper-text - Supporting text displayed beneath the toggle label when the toggle is valid.
|
||||
* @part error-text - Supporting text displayed beneath the toggle label when the toggle is invalid and touched.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-toggle',
|
||||
@@ -32,6 +35,8 @@ import type { ToggleChangeEventDetail } from './toggle-interface';
|
||||
})
|
||||
export class Toggle implements ComponentInterface {
|
||||
private inputId = `ion-tg-${toggleIds++}`;
|
||||
private helperTextId = `${this.inputId}-helper-text`;
|
||||
private errorTextId = `${this.inputId}-error-text`;
|
||||
private gesture?: Gesture;
|
||||
private focusEl?: HTMLElement;
|
||||
private lastDrag = 0;
|
||||
@@ -65,6 +70,16 @@ export class Toggle implements ComponentInterface {
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* Text that is placed under the toggle label and displayed when an error is detected.
|
||||
*/
|
||||
@Prop() errorText?: string;
|
||||
|
||||
/**
|
||||
* Text that is placed under the toggle label and displayed when no error is detected.
|
||||
*/
|
||||
@Prop() helperText?: string;
|
||||
|
||||
/**
|
||||
* The value of the toggle does not mean if it's checked or not, use the `checked`
|
||||
* property for that.
|
||||
@@ -297,6 +312,48 @@ export class Toggle implements ComponentInterface {
|
||||
return this.el.textContent !== '';
|
||||
}
|
||||
|
||||
private getHintTextID(): string | undefined {
|
||||
const { el, helperText, errorText, helperTextId, errorTextId } = this;
|
||||
|
||||
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
|
||||
return errorTextId;
|
||||
}
|
||||
|
||||
if (helperText) {
|
||||
return helperTextId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for rendering helper text and error text.
|
||||
* This element should only be rendered if hint text is set.
|
||||
*/
|
||||
private renderHintText() {
|
||||
const { helperText, errorText, helperTextId, errorTextId } = this;
|
||||
|
||||
/**
|
||||
* undefined and empty string values should
|
||||
* be treated as not having helper/error text.
|
||||
*/
|
||||
const hasHintText = !!helperText || !!errorText;
|
||||
if (!hasHintText) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="toggle-bottom">
|
||||
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
|
||||
{helperText}
|
||||
</div>
|
||||
<div id={errorTextId} class="error-text" part="supporting-text error-text">
|
||||
{errorText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment, required } =
|
||||
this;
|
||||
@@ -308,6 +365,8 @@ export class Toggle implements ComponentInterface {
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-describedby={this.getHintTextID()}
|
||||
aria-invalid={this.getHintTextID() === this.errorTextId}
|
||||
onClick={this.onClick}
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
@@ -347,6 +406,7 @@ export class Toggle implements ComponentInterface {
|
||||
part="label"
|
||||
>
|
||||
<slot></slot>
|
||||
{this.renderHintText()}
|
||||
</div>
|
||||
<div class="native-wrapper">{this.renderToggleControl()}</div>
|
||||
</label>
|
||||
|
||||
@@ -2473,14 +2473,14 @@ Shorthand for ionToastDidDismiss.
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value']
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-toggle',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value'],
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value'],
|
||||
})
|
||||
export class IonToggle {
|
||||
protected el: HTMLIonToggleElement;
|
||||
|
||||
@@ -21,6 +21,8 @@ const TOGGLE_INPUTS = [
|
||||
'color',
|
||||
'disabled',
|
||||
'enableOnOffLabels',
|
||||
'errorText',
|
||||
'helperText',
|
||||
'justify',
|
||||
'labelPlacement',
|
||||
'mode',
|
||||
|
||||
@@ -1015,6 +1015,8 @@ export const IonToggle = /*@__PURE__*/ defineContainer<JSX.IonToggle, JSX.IonTog
|
||||
'name',
|
||||
'checked',
|
||||
'disabled',
|
||||
'errorText',
|
||||
'helperText',
|
||||
'value',
|
||||
'enableOnOffLabels',
|
||||
'labelPlacement',
|
||||
|
||||