2021-09-24 21:31:43 +03:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2021 Google LLC
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
*/
|
|
|
|
|
2022-12-13 06:01:29 +03:00
|
|
|
// tslint:disable:no-new-decorators
|
|
|
|
|
2022-10-18 19:28:12 +03:00
|
|
|
import '../../focus/focus-ring.js';
|
|
|
|
import '../../ripple/ripple.js';
|
2021-09-24 21:31:43 +03:00
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
import {html, LitElement, TemplateResult} from 'lit';
|
|
|
|
import {eventOptions, property, query, queryAsync, state} from 'lit/decorators.js';
|
2022-08-24 21:00:50 +03:00
|
|
|
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
|
|
|
|
import {ifDefined} from 'lit/directives/if-defined.js';
|
2022-12-20 01:03:21 +03:00
|
|
|
import {when} from 'lit/directives/when.js';
|
2021-12-17 00:36:39 +03:00
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
import {dispatchActivationClick, isActivationClick} from '../../controller/events.js';
|
2022-10-18 19:28:12 +03:00
|
|
|
import {FormController, getFormValue} from '../../controller/form-controller.js';
|
|
|
|
import {ariaProperty} from '../../decorators/aria-property.js';
|
|
|
|
import {pointerPress as focusRingPointerPress, shouldShowStrongFocus} from '../../focus/strong-focus.js';
|
2022-12-20 01:03:21 +03:00
|
|
|
import {ripple} from '../../ripple/directive.js';
|
2022-10-18 19:28:12 +03:00
|
|
|
import {MdRipple} from '../../ripple/ripple.js';
|
|
|
|
|
2022-12-15 03:49:11 +03:00
|
|
|
/**
|
|
|
|
* @fires input {InputEvent} Fired whenever `selected` changes due to user
|
|
|
|
* interaction (bubbles and composed).
|
|
|
|
* @fires change {Event} Fired whenever `selected` changes due to user
|
|
|
|
* interaction (bubbles).
|
|
|
|
*/
|
2022-12-20 01:03:21 +03:00
|
|
|
export class Switch extends LitElement {
|
2021-12-29 20:45:59 +03:00
|
|
|
static override shadowRootOptions:
|
|
|
|
ShadowRootInit = {mode: 'open', delegatesFocus: true};
|
|
|
|
|
2023-02-04 02:07:53 +03:00
|
|
|
/**
|
|
|
|
* @nocollapse
|
|
|
|
*/
|
2022-12-17 04:06:12 +03:00
|
|
|
static formAssociated = true;
|
|
|
|
|
2022-12-13 06:01:29 +03:00
|
|
|
/**
|
|
|
|
* Disables the switch and makes it non-interactive.
|
|
|
|
*/
|
2021-12-29 20:45:59 +03:00
|
|
|
@property({type: Boolean, reflect: true}) disabled = false;
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Puts the switch in the selected state and sets the form submission value to
|
|
|
|
* the `value` property.
|
|
|
|
*/
|
2021-09-24 21:31:43 +03:00
|
|
|
@property({type: Boolean}) selected = false;
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Shows both the selected and deselected icons.
|
|
|
|
*/
|
2022-04-29 21:43:31 +03:00
|
|
|
@property({type: Boolean}) icons = false;
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Shows only the selected icon, and not the deselected icon. If `true`,
|
|
|
|
* overrides the behavior of the `icons` property.
|
|
|
|
*/
|
2022-07-26 19:20:02 +03:00
|
|
|
@property({type: Boolean}) showOnlySelectedIcon = false;
|
2021-09-24 21:31:43 +03:00
|
|
|
|
2022-12-13 06:01:29 +03:00
|
|
|
@ariaProperty
|
2021-12-17 00:36:39 +03:00
|
|
|
@property({type: String, attribute: 'data-aria-label', noAccessor: true})
|
|
|
|
override ariaLabel!: string;
|
2021-09-24 21:31:43 +03:00
|
|
|
|
2022-12-13 06:01:29 +03:00
|
|
|
@ariaProperty
|
2022-09-28 23:33:19 +03:00
|
|
|
@property({type: String, attribute: 'data-aria-labelledby', noAccessor: true})
|
2021-09-24 21:31:43 +03:00
|
|
|
ariaLabelledBy = '';
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
@state() private showFocusRing = false;
|
|
|
|
@state() private showRipple = false;
|
2022-03-29 23:50:10 +03:00
|
|
|
|
2022-09-16 23:51:46 +03:00
|
|
|
// Ripple
|
2022-12-20 01:03:21 +03:00
|
|
|
@queryAsync('md-ripple') private readonly ripple!: Promise<MdRipple|null>;
|
|
|
|
|
|
|
|
// Button
|
|
|
|
@query('button') private readonly button!: HTMLButtonElement|null;
|
2022-09-16 23:51:46 +03:00
|
|
|
|
2022-12-17 04:06:12 +03:00
|
|
|
/**
|
|
|
|
* The associated form element with which this element's value will submit.
|
|
|
|
*/
|
2021-12-29 20:45:59 +03:00
|
|
|
get form() {
|
|
|
|
return this.closest('form');
|
|
|
|
}
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The HTML name to use in form submission.
|
|
|
|
*/
|
2021-09-24 21:31:43 +03:00
|
|
|
@property({type: String, reflect: true}) name = '';
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The value associated with this switch on form submission. `null` is
|
|
|
|
* submitted when `selected` is `false`.
|
|
|
|
*/
|
2021-09-24 21:31:43 +03:00
|
|
|
@property({type: String}) value = 'on';
|
2022-12-13 06:01:29 +03:00
|
|
|
|
2021-12-29 20:45:59 +03:00
|
|
|
[getFormValue]() {
|
|
|
|
return this.selected ? this.value : null;
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
|
|
|
|
2021-12-29 20:45:59 +03:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.addController(new FormController(this));
|
2022-12-17 04:06:12 +03:00
|
|
|
this.addEventListener('click', (event: MouseEvent) => {
|
|
|
|
if (!isActivationClick(event)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.button?.focus();
|
2022-12-20 01:03:21 +03:00
|
|
|
if (this.button != null) {
|
|
|
|
// this triggers the click behavior, and the ripple
|
|
|
|
dispatchActivationClick(this.button);
|
|
|
|
}
|
2022-12-17 04:06:12 +03:00
|
|
|
});
|
2021-12-29 20:45:59 +03:00
|
|
|
}
|
2021-09-24 21:31:43 +03:00
|
|
|
|
|
|
|
protected override render(): TemplateResult {
|
2022-07-01 21:29:51 +03:00
|
|
|
const ariaLabelValue = this.ariaLabel ? this.ariaLabel : undefined;
|
|
|
|
const ariaLabelledByValue =
|
|
|
|
this.ariaLabelledBy ? this.ariaLabelledBy : undefined;
|
2022-12-20 01:03:21 +03:00
|
|
|
// NOTE: buttons must use only [phrasing
|
|
|
|
// content](https://html.spec.whatwg.org/multipage/dom.html#phrasing-content)
|
|
|
|
// children, which includes custom elements, but not `div`s
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
|
|
|
<button
|
|
|
|
type="button"
|
2022-03-23 23:50:50 +03:00
|
|
|
class="md3-switch ${classMap(this.getRenderClasses())}"
|
2021-09-24 21:31:43 +03:00
|
|
|
role="switch"
|
|
|
|
aria-checked="${this.selected}"
|
2022-07-01 21:29:51 +03:00
|
|
|
aria-label="${ifDefined(ariaLabelValue)}"
|
|
|
|
aria-labelledby="${ifDefined(ariaLabelledByValue)}"
|
2022-07-23 01:26:44 +03:00
|
|
|
?disabled=${this.disabled}
|
2021-09-24 21:31:43 +03:00
|
|
|
@click=${this.handleClick}
|
|
|
|
@focus="${this.handleFocus}"
|
|
|
|
@blur="${this.handleBlur}"
|
2022-07-22 20:13:21 +03:00
|
|
|
@pointerdown=${this.handlePointerDown}
|
2022-12-20 01:03:21 +03:00
|
|
|
${ripple(this.getRipple)}
|
2021-09-24 21:31:43 +03:00
|
|
|
>
|
2022-12-20 01:03:21 +03:00
|
|
|
${when(this.showFocusRing, this.renderFocusRing)}
|
|
|
|
<span class="md3-switch__track">
|
2021-09-24 21:31:43 +03:00
|
|
|
${this.renderHandle()}
|
2022-12-20 01:03:21 +03:00
|
|
|
</span>
|
2021-09-24 21:31:43 +03:00
|
|
|
</button>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private getRenderClasses(): ClassInfo {
|
2021-09-24 21:31:43 +03:00
|
|
|
return {
|
2022-03-23 23:50:50 +03:00
|
|
|
'md3-switch--selected': this.selected,
|
|
|
|
'md3-switch--unselected': !this.selected,
|
2021-09-24 21:31:43 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private readonly renderRipple = () => {
|
2022-09-16 23:51:46 +03:00
|
|
|
return html`
|
2022-12-20 01:03:21 +03:00
|
|
|
<span class="md3-switch__ripple">
|
2022-09-16 23:51:46 +03:00
|
|
|
<md-ripple
|
|
|
|
?disabled="${this.disabled}"
|
|
|
|
unbounded>
|
|
|
|
</md-ripple>
|
2022-12-20 01:03:21 +03:00
|
|
|
</span>
|
2022-09-16 23:51:46 +03:00
|
|
|
`;
|
2022-12-20 01:03:21 +03:00
|
|
|
};
|
2022-09-16 23:51:46 +03:00
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private readonly getRipple = () => {
|
|
|
|
this.showRipple = true;
|
|
|
|
return this.ripple;
|
|
|
|
};
|
|
|
|
|
|
|
|
private readonly renderFocusRing = () => {
|
|
|
|
return html`<md-focus-ring visible></md-focus-ring>`;
|
|
|
|
};
|
2022-03-29 23:50:10 +03:00
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private renderHandle(): TemplateResult {
|
2022-05-11 02:01:43 +03:00
|
|
|
/** @classMap */
|
|
|
|
const classes = {
|
2022-07-26 19:20:02 +03:00
|
|
|
'md3-switch__handle--big': this.icons && !this.showOnlySelectedIcon,
|
2022-05-11 02:01:43 +03:00
|
|
|
};
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2022-12-20 01:03:21 +03:00
|
|
|
<span class="md3-switch__handle-container">
|
|
|
|
${when(this.showRipple, this.renderRipple)}
|
|
|
|
<span class="md3-switch__handle ${classMap(classes)}">
|
2022-05-11 02:01:43 +03:00
|
|
|
${this.shouldShowIcons() ? this.renderIcons() : html``}
|
2022-12-20 01:03:21 +03:00
|
|
|
</span>
|
2022-05-11 02:01:43 +03:00
|
|
|
${this.renderTouchTarget()}
|
2022-12-20 01:03:21 +03:00
|
|
|
</span>
|
2021-09-24 21:31:43 +03:00
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2022-05-06 20:25:04 +03:00
|
|
|
private renderIcons(): TemplateResult {
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2022-04-29 21:43:31 +03:00
|
|
|
<div class="md3-switch__icons">
|
|
|
|
${this.renderOnIcon()}
|
2022-07-26 19:20:02 +03:00
|
|
|
${this.showOnlySelectedIcon ? html`` : this.renderOffIcon()}
|
2021-09-24 21:31:43 +03:00
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2022-05-06 20:25:04 +03:00
|
|
|
/**
|
|
|
|
* https://fonts.google.com/icons?selected=Material%20Symbols%20Outlined%3Acheck%3AFILL%400%3Bwght%40500%3BGRAD%400%3Bopsz%4024
|
|
|
|
*/
|
2022-12-20 01:03:21 +03:00
|
|
|
private renderOnIcon(): TemplateResult {
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2022-03-23 23:50:50 +03:00
|
|
|
<svg class="md3-switch__icon md3-switch__icon--on" viewBox="0 0 24 24">
|
2022-05-06 20:25:04 +03:00
|
|
|
<path d="M9.55 18.2 3.65 12.3 5.275 10.675 9.55 14.95 18.725 5.775 20.35 7.4Z"/>
|
2021-09-24 21:31:43 +03:00
|
|
|
</svg>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2022-05-06 20:25:04 +03:00
|
|
|
/**
|
|
|
|
* https://fonts.google.com/icons?selected=Material%20Symbols%20Outlined%3Aclose%3AFILL%400%3Bwght%40500%3BGRAD%400%3Bopsz%4024
|
|
|
|
*/
|
2022-12-20 01:03:21 +03:00
|
|
|
private renderOffIcon(): TemplateResult {
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2022-03-23 23:50:50 +03:00
|
|
|
<svg class="md3-switch__icon md3-switch__icon--off" viewBox="0 0 24 24">
|
2022-05-06 20:25:04 +03:00
|
|
|
<path d="M6.4 19.2 4.8 17.6 10.4 12 4.8 6.4 6.4 4.8 12 10.4 17.6 4.8 19.2 6.4 13.6 12 19.2 17.6 17.6 19.2 12 13.6Z"/>
|
2021-09-24 21:31:43 +03:00
|
|
|
</svg>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2022-05-06 20:25:04 +03:00
|
|
|
private renderTouchTarget(): TemplateResult {
|
|
|
|
return html`<span class="md3-switch__touch"></span>`;
|
|
|
|
}
|
|
|
|
|
2022-05-11 02:01:43 +03:00
|
|
|
private shouldShowIcons(): boolean {
|
2022-07-26 19:20:02 +03:00
|
|
|
return this.icons || this.showOnlySelectedIcon;
|
2022-05-06 20:25:04 +03:00
|
|
|
}
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private handleClick() {
|
|
|
|
if (this.disabled) {
|
2022-03-23 04:18:05 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.selected = !this.selected;
|
2022-12-15 03:49:11 +03:00
|
|
|
this.dispatchEvent(
|
|
|
|
new InputEvent('input', {bubbles: true, composed: true}));
|
|
|
|
// Bubbles but does not compose to mimic native browser <input> & <select>
|
|
|
|
// Additionally, native change event is not an InputEvent.
|
|
|
|
this.dispatchEvent(new Event('change', {bubbles: true}));
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private handleFocus() {
|
2022-03-29 23:50:10 +03:00
|
|
|
this.showFocusRing = shouldShowStrongFocus();
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private handleBlur() {
|
2022-03-29 23:50:10 +03:00
|
|
|
this.showFocusRing = false;
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@eventOptions({passive: true})
|
2022-12-20 01:03:21 +03:00
|
|
|
private handlePointerDown() {
|
2022-04-29 21:43:31 +03:00
|
|
|
focusRingPointerPress();
|
|
|
|
this.showFocusRing = false;
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
|
|
|
}
|