mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
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:
@ -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"
|
||||
|
Reference in New Issue
Block a user