feat(toggle): on/off icons for toggle (#25459)

Resolves #20524
This commit is contained in:
Sean Perkins
2022-07-06 12:39:34 -04:00
committed by GitHub
parent 7cdc388b78
commit bc0bdc438b
33 changed files with 493 additions and 21 deletions

View File

@ -2983,6 +2983,10 @@ export namespace Components {
* If `true`, the user cannot interact with the toggle.
*/
"disabled": boolean;
/**
* Enables the on/off accessibility switch labels within the toggle.
*/
"enableOnOffLabels": boolean | undefined;
/**
* The mode determines which platform styles to use.
*/
@ -6935,6 +6939,10 @@ declare namespace LocalJSX {
* If `true`, the user cannot interact with the toggle.
*/
"disabled"?: boolean;
/**
* Enables the on/off accessibility switch labels within the toggle.
*/
"enableOnOffLabels"?: boolean | undefined;
/**
* The mode determines which platform styles to use.
*/

View File

@ -0,0 +1,284 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Toggle - enableOnOffLabels</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>
/*
* Dark Colors
* ------------------
*/
body.dark {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66, 140, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80, 200, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106, 100, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47, 223, 117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0, 0, 0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255, 213, 52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255, 73, 97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244, 245, 248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0, 0, 0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0, 0, 0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34, 36, 40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255, 255, 255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------
*/
.ios body.dark {
--ion-background-color: #000000;
--ion-background-color-rgb: 0, 0, 0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-toolbar-background: #0d0d0d;
--ion-item-background: #1c1c1c;
--ion-item-background-activated: #313131;
}
/*
* Material Design Dark Theme
* ------------------------------
*/
.md body.dark {
--ion-background-color: #121212;
--ion-background-color-rgb: 18, 18, 18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1a1b1e;
}
/* Optional CSS, this is added for the flashing that happens when toggling between themes */
ion-item {
--transition: none;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Toggle - enableOnOffLabels</ion-title>
<ion-buttons slot="end">
<ion-button id="popover-trigger">Options</ion-button>
</ion-buttons>
<ion-popover class="options-popover" trigger="popover-trigger">
<ion-list lines="none">
<ion-item id="dark-mode">
<ion-label>Dark Mode</ion-label>
<ion-checkbox slot="end"></ion-checkbox>
</ion-item>
</ion-list>
</ion-popover>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-label>Unchecked</ion-label>
<ion-toggle slot="end" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Checked</ion-label>
<ion-toggle slot="end" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Secondary Unchecked</ion-label>
<ion-toggle slot="end" color="secondary" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Secondary Checked</ion-label>
<ion-toggle slot="end" color="secondary" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Success Unchecked</ion-label>
<ion-toggle slot="end" color="success" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Success Checked</ion-label>
<ion-toggle slot="end" color="success" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Danger Unchecked</ion-label>
<ion-toggle slot="end" color="danger" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Danger Checked</ion-label>
<ion-toggle slot="end" color="danger" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Tertiary Unchecked</ion-label>
<ion-toggle slot="end" color="tertiary" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Tertiary Checked</ion-label>
<ion-toggle slot="end" color="tertiary" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Light Unchecked</ion-label>
<ion-toggle slot="end" color="light" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Light Checked</ion-label>
<ion-toggle slot="end" color="light" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Medium Unchecked</ion-label>
<ion-toggle slot="end" color="medium" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Medium Checked</ion-label>
<ion-toggle slot="end" color="medium" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Dark Unchecked</ion-label>
<ion-toggle slot="end" color="dark" enable-on-off-labels="true"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Dark Checked</ion-label>
<ion-toggle slot="end" color="dark" enable-on-off-labels="true" checked></ion-toggle>
</ion-item>
</ion-list>
</ion-content>
</ion-app>
<script>
const darkModeCheckbox = document.querySelector('ion-checkbox');
darkModeCheckbox.addEventListener('ionChange', (ev) => {
if (ev.detail.checked) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,40 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('toggle: enableOnOffLabels', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/toggle/test/enable-on-off-labels`);
});
test('should not have visual regressions', async ({ page }) => {
await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(`toggle-on-off-labels-diff-${page.getSnapshotSettings()}.png`);
});
test.describe('dark mode', () => {
test('should not have visual regressions', async ({ page }) => {
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
const ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss');
await page.click('#popover-trigger');
await ionPopoverDidPresent.next();
await page.click('#dark-mode');
await page.evaluate(() => {
const popover = document.querySelector('ion-popover');
return popover?.dismiss();
});
await ionPopoverDidDismiss.next();
await page.waitForChanges();
await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(
`toggle-on-off-labels-dark-mode-diff-${page.getSnapshotSettings()}.png`
);
});
});
});

View File

@ -30,6 +30,9 @@
background: current-color(base);
}
:host(.toggle-activated) .toggle-switch-icon {
opacity: 0;
}
// iOS Toggle Background Track: Unchecked
// ----------------------------------------------------------
@ -42,7 +45,6 @@
transition: background-color $toggle-ios-transition-duration;
}
// iOS Toggle Inner Knob: Unchecked
// ----------------------------------------------------------
@ -50,6 +52,70 @@
will-change: transform;
}
// iOS Toggle On/Off Labels
// ----------------------------------------------------------
.toggle-switch-icon {
position: absolute;
top: 50%;
width: 11px;
height: 11px;
transform: translateY(-50%);
transition: opacity $toggle-ios-transition-duration, color $toggle-ios-transition-duration;
}
.toggle-switch-icon {
@include ltr() {
/* stylelint-disable-next-line property-disallowed-list */
right: 6px;
}
@include rtl() {
/* stylelint-disable property-disallowed-list */
right: initial;
left: 6px;
/* stylelint-enable property-disallowed-list */
}
position: absolute;
color: var(--ion-color-dark);
}
:host(.toggle-checked) .toggle-switch-icon.toggle-switch-icon-checked {
// The color contrast of iOS default on/off labels fails to meet WCAG 2.0.
// We use Ionic's color contrast variables to meet the WCAG 2.0 standard (AAA).
color: var(--ion-color-contrast, $toggle-ios-on-off-label-checked-color);
}
:host(.toggle-checked) .toggle-switch-icon:not(.toggle-switch-icon-checked) {
opacity: 0;
}
.toggle-switch-icon-checked {
@include ltr() {
/* stylelint-disable property-disallowed-list */
right: initial;
left: 4px;
/* stylelint-enable property-disallowed-list */
}
@include rtl() {
/* stylelint-disable-next-line property-disallowed-list */
right: 4px;
}
position: absolute;
width: 15px;
height: 15px;
transform: translateY(-50%) rotate(90deg);
}
// iOS Toggle Background Oval: Activated or Checked
// ----------------------------------------------------------
@ -59,7 +125,6 @@
transform: scale3d(0, 0, 0);
}
// iOS Toggle Background Oval: Activated and Checked
// ----------------------------------------------------------
@ -67,7 +132,6 @@
transform: scale3d(0, 0, 0);
}
// iOS Toggle Inner Knob: Activated and Unchecked
// ----------------------------------------------------------
@ -75,7 +139,6 @@
width: calc(var(--handle-width) + 6px);
}
// iOS Toggle Inner Knob: Activated and Checked
// ----------------------------------------------------------
@ -93,25 +156,31 @@
}
}
// iOS Toggle: Disabled
// ----------------------------------------------------------
// .item-ios.item-toggle-disabled ion-label
:host(.toggle-disabled) {
opacity: $toggle-ios-disabled-opacity;
}
// iOS Toggle Within An Item
// ----------------------------------------------------------
:host(.in-item[slot]) {
@include margin($toggle-ios-media-margin);
@include padding($toggle-ios-item-end-padding-top, $toggle-ios-item-end-padding-end, $toggle-ios-item-end-padding-bottom, $toggle-ios-item-end-padding-start);
@include padding(
$toggle-ios-item-end-padding-top,
$toggle-ios-item-end-padding-end,
$toggle-ios-item-end-padding-bottom,
$toggle-ios-item-end-padding-start
);
}
:host(.in-item[slot="start"]) {
@include padding($toggle-ios-item-start-padding-top, $toggle-ios-item-start-padding-end, $toggle-ios-item-start-padding-bottom, $toggle-ios-item-start-padding-start);
@include padding(
$toggle-ios-item-start-padding-top,
$toggle-ios-item-start-padding-end,
$toggle-ios-item-start-padding-bottom,
$toggle-ios-item-start-padding-start
);
}

View File

@ -75,3 +75,6 @@ $toggle-ios-item-end-padding-bottom: 5px !default;
/// @prop - Padding start of the toggle positioned on the end in an item
$toggle-ios-item-end-padding-start: $item-ios-padding-start !default;
/// @prop - The text color of the on/off labels when the toggle is checked
$toggle-ios-on-off-label-checked-color: #fff !default;

View File

@ -34,6 +34,10 @@
background: current-color(base);
}
:host(.toggle-checked) .toggle-inner {
color: var(--ion-color-contrast, $toggle-md-on-off-label-checked-color);
}
// Material Design Toggle Background Track: Unchecked
// ----------------------------------------------------------
@ -41,14 +45,26 @@
transition: background-color $toggle-md-transition-duration;
}
// Material Design Toggle Inner Knob: Unchecked
// ----------------------------------------------------------
.toggle-inner {
will-change: background-color, transform;
display: flex;
align-items: center;
justify-content: center;
color: $toggle-md-on-off-label-color;
}
.toggle-inner .toggle-switch-icon {
@include padding(1px);
width: 100%;
height: 100%;
}
// Material Design Toggle: Disabled
// ----------------------------------------------------------
@ -68,17 +84,31 @@
// opacity: $toggle-md-disabled-opacity;
// }
// Material Design Toggle Within An Item
// ----------------------------------------------------------
:host(.in-item[slot]) {
@include margin($toggle-md-media-margin-top, $toggle-md-media-margin-end, $toggle-md-media-margin-bottom, $toggle-md-media-margin-start);
@include padding($toggle-md-item-end-padding-top, $toggle-md-item-end-padding-end, $toggle-md-item-end-padding-bottom, $toggle-md-item-end-padding-start);
@include margin(
$toggle-md-media-margin-top,
$toggle-md-media-margin-end,
$toggle-md-media-margin-bottom,
$toggle-md-media-margin-start
);
@include padding(
$toggle-md-item-end-padding-top,
$toggle-md-item-end-padding-end,
$toggle-md-item-end-padding-bottom,
$toggle-md-item-end-padding-start
);
cursor: pointer;
}
:host(.in-item[slot="start"]) {
@include padding($toggle-md-item-start-padding-top, $toggle-md-item-start-padding-end, $toggle-md-item-start-padding-bottom, $toggle-md-item-start-padding-start);
@include padding(
$toggle-md-item-start-padding-top,
$toggle-md-item-start-padding-end,
$toggle-md-item-start-padding-bottom,
$toggle-md-item-start-padding-start
);
}

View File

@ -99,3 +99,9 @@ $toggle-md-item-end-padding-bottom: 12px !default;
/// @prop - Padding start of the toggle positioned on the end in an item
$toggle-md-item-end-padding-start: $item-md-padding-start !default;
/// @prop - The text color of the on/off labels
$toggle-md-on-off-label-color: #000 !default;
/// @prop - The text color of the on/off labels when the toggle is checked
$toggle-md-on-off-label-checked-color: #fff !default;

View File

@ -83,6 +83,7 @@ input {
@include border-radius(var(--border-radius));
display: block;
position: relative;
width: 100%;
@ -95,7 +96,6 @@ input {
overflow: inherit;
}
// Toggle Background Track: Checked
// ----------------------------------------------------------
@ -103,7 +103,6 @@ input {
background: var(--background-checked);
}
// Toggle Inner Knob: Unchecked
// --------------------------------------------------
@ -127,7 +126,6 @@ input {
contain: strict;
}
// Toggle Inner Knob: Checked
// ----------------------------------------------------------

View File

@ -1,8 +1,9 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
import { checkmarkOutline, removeOutline, ellipseOutline } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import type { Color, Gesture, GestureDetail, StyleEventDetail, ToggleChangeEventDetail } from '../../interface';
import type { Color, Gesture, GestureDetail, Mode, StyleEventDetail, ToggleChangeEventDetail } from '../../interface';
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import { hapticSelection } from '../../utils/native/haptic';
import { isRTL } from '../../utils/rtl';
@ -63,6 +64,11 @@ export class Toggle implements ComponentInterface {
*/
@Prop() value?: string | null = 'on';
/**
* Enables the on/off accessibility switch labels within the toggle.
*/
@Prop() enableOnOffLabels: boolean | undefined = undefined;
/**
* Emitted when the value property has changed.
*/
@ -178,8 +184,29 @@ export class Toggle implements ComponentInterface {
this.ionBlur.emit();
};
private getSwitchLabelIcon = (mode: Mode, checked: boolean) => {
if (mode === 'md') {
return checked ? checkmarkOutline : removeOutline;
}
return checked ? removeOutline : ellipseOutline;
};
private renderOnOffSwitchLabels(mode: Mode, checked: boolean) {
const icon = this.getSwitchLabelIcon(mode, checked);
return (
<ion-icon
class={{
'toggle-switch-icon': true,
'toggle-switch-icon-checked': checked,
}}
icon={icon}
></ion-icon>
);
}
render() {
const { activated, color, checked, disabled, el, inputId, name } = this;
const { activated, color, checked, disabled, el, inputId, name, enableOnOffLabels } = this;
const mode = getIonMode(this);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
const value = this.getValue();
@ -203,8 +230,15 @@ export class Toggle implements ComponentInterface {
})}
>
<div class="toggle-icon" part="track">
{/* The iOS on/off labels are rendered outside of .toggle-icon-wrapper,
since the wrapper is translated when the handle is interacted with and
this would move the on/off labels outside of the view box */}
{enableOnOffLabels &&
mode === 'ios' && [this.renderOnOffSwitchLabels(mode, true), this.renderOnOffSwitchLabels(mode, false)]}
<div class="toggle-icon-wrapper">
<div class="toggle-inner" part="handle" />
<div class="toggle-inner" part="handle">
{enableOnOffLabels && mode === 'md' && this.renderOnOffSwitchLabels(mode, checked)}
</div>
</div>
</div>
<label htmlFor={inputId}>{labelText}</label>