feat(toggle): component can be used outside of ion-item (#26357)

resolves #25570, resolves #23213

BREAKING CHANGE:

The `--background` and `--background-checked` variables have been renamed to `--track-background` and `--track-background-checked`, respectively.
This commit is contained in:
Liam DeBeasi
2022-11-29 12:54:31 -05:00
committed by GitHub
parent 0ca6fee1d7
commit c74901c973
293 changed files with 1719 additions and 221 deletions

View File

@ -1,10 +1,15 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
// TODO(FW-2845) - Use @utils/forms and @utils/logging when https://github.com/ionic-team/stencil/issues/3826 is resolved
import { checkmarkOutline, removeOutline, ellipseOutline } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import type { Color, Gesture, GestureDetail, Mode, StyleEventDetail, ToggleChangeEventDetail } from '../../interface';
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import type { LegacyFormController } from '../../utils/forms';
import { createLegacyFormController } from '../../utils/forms';
import { getAriaLabel, renderHiddenInput, inheritAriaAttributes } from '../../utils/helpers';
import type { Attributes } from '../../utils/helpers';
import { printIonWarning } from '../../utils/logging';
import { hapticSelection } from '../../utils/native/haptic';
import { isRTL } from '../../utils/rtl';
import { createColorClasses, hostContext } from '../../utils/theme';
@ -12,6 +17,8 @@ import { createColorClasses, hostContext } from '../../utils/theme';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot - The label text to associate with the toggle. Use the "labelPlacement" property to control where the label is placed relative to the toggle.
*
* @part track - The background track of the toggle.
* @part handle - The toggle handle, or knob, used to change the checked state.
*/
@ -28,8 +35,13 @@ export class Toggle implements ComponentInterface {
private gesture?: Gesture;
private focusEl?: HTMLElement;
private lastDrag = 0;
private legacyFormController!: LegacyFormController;
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;
// This flag ensures we log the deprecation warning at most once.
private hasLoggedDeprecationWarning = false;
@Element() el!: HTMLIonToggleElement;
@State() activated = false;
@ -69,6 +81,25 @@ export class Toggle implements ComponentInterface {
*/
@Prop() enableOnOffLabels: boolean | undefined = undefined;
/**
* Where to place the label relative to the input.
* `'start'`: The label will appear to the left of the toggle in LTR and to the right in RTL.
* `'end'`: The label will appear to the right of the toggle in LTR and to the left in RTL.
* `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
@Prop() labelPlacement: 'start' | 'end' | 'fixed' = 'start';
/**
* 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.
*/
@Prop() justify: 'start' | 'end' | 'space-between' = 'space-between';
/**
* Emitted when the user switches the toggle on or off. Does not emit
* when programmatically changing the value of the `checked` property.
@ -112,8 +143,12 @@ export class Toggle implements ComponentInterface {
}
async connectedCallback() {
const { el } = this;
this.legacyFormController = createLegacyFormController(el);
this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.el,
el,
gestureName: 'toggle',
gesturePriority: 100,
threshold: 5,
@ -134,6 +169,12 @@ export class Toggle implements ComponentInterface {
componentWillLoad() {
this.emitStyle();
if (!this.legacyFormController.hasLegacyControl()) {
this.inheritedAttributes = {
...inheritAriaAttributes(this.el),
};
}
}
private emitStyle() {
@ -210,8 +251,99 @@ export class Toggle implements ComponentInterface {
);
}
private renderToggleControl() {
const mode = getIonMode(this);
const { enableOnOffLabels, checked } = this;
return (
<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">
{enableOnOffLabels && mode === 'md' && this.renderOnOffSwitchLabels(mode, checked)}
</div>
</div>
</div>
);
}
private get hasLabel() {
return this.el.textContent !== '';
}
render() {
const { activated, color, checked, disabled, el, inputId, name, enableOnOffLabels } = this;
const { legacyFormController } = this;
return legacyFormController.hasLegacyControl() ? this.renderLegacyToggle() : this.renderToggle();
}
private renderToggle() {
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name } = this;
const mode = getIonMode(this);
const value = this.getValue();
const rtl = isRTL(el) ? 'rtl' : 'ltr';
renderHiddenInput(true, el, name, checked ? value : '', disabled);
return (
<Host
onClick={this.onClick}
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
'toggle-activated': activated,
'toggle-checked': checked,
'toggle-disabled': disabled,
[`toggle-justify-${justify}`]: true,
[`toggle-label-placement-${labelPlacement}`]: true,
[`toggle-${rtl}`]: true,
})}
>
<label class="toggle-wrapper">
<div
class={{
'label-text-wrapper': true,
'label-text-wrapper-hidden': !this.hasLabel,
}}
>
<slot></slot>
</div>
<div class="native-wrapper">{this.renderToggleControl()}</div>
<input
type="checkbox"
role="switch"
aria-checked={`${checked}`}
checked={checked}
disabled={disabled}
id={inputId}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
{...this.inheritedAttributes}
/>
</label>
</Host>
);
}
private renderLegacyToggle() {
if (!this.hasLoggedDeprecationWarning) {
printIonWarning(
`Using ion-toggle with an ion-label has been deprecated. To migrate, remove the ion-label and pass your label directly into ion-toggle instead.
Example: <ion-toggle>Email:</ion-toggle>
For toggles that do not have a visible label, developers should use "aria-label" so screen readers can announce the purpose of the toggle.`,
this.el
);
this.hasLoggedDeprecationWarning = true;
}
const { activated, color, checked, disabled, el, inputId, name } = this;
const mode = getIonMode(this);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
const value = this.getValue();
@ -232,22 +364,12 @@ export class Toggle implements ComponentInterface {
'toggle-activated': activated,
'toggle-checked': checked,
'toggle-disabled': disabled,
'legacy-toggle': true,
interactive: true,
[`toggle-${rtl}`]: true,
})}
>
<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">
{enableOnOffLabels && mode === 'md' && this.renderOnOffSwitchLabels(mode, checked)}
</div>
</div>
</div>
{this.renderToggleControl()}
<label htmlFor={inputId}>{labelText}</label>
<input
type="checkbox"