2021-09-24 21:31:43 +03:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2021 Google LLC
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
*/
|
|
|
|
|
2023-07-24 20:42:56 +03:00
|
|
|
import '../../focus/md-focus-ring.js';
|
2022-10-18 19:28:12 +03:00
|
|
|
import '../../ripple/ripple.js';
|
2021-09-24 21:31:43 +03:00
|
|
|
|
2023-05-17 03:57:49 +03:00
|
|
|
import {html, isServer, LitElement, nothing, PropertyValues, TemplateResult} from 'lit';
|
2023-05-26 20:05:54 +03:00
|
|
|
import {property, query} from 'lit/decorators.js';
|
2022-08-24 21:00:50 +03:00
|
|
|
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
|
2021-12-17 00:36:39 +03:00
|
|
|
|
2023-05-30 20:37:05 +03:00
|
|
|
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
2023-08-24 10:54:16 +03:00
|
|
|
import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../internal/controller/events.js';
|
2022-10-18 19:28:12 +03:00
|
|
|
|
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 {
|
2023-04-25 00:10:06 +03:00
|
|
|
static {
|
2023-08-08 23:59:44 +03:00
|
|
|
requestUpdateOnAriaChange(Switch);
|
2023-04-25 00:10:06 +03:00
|
|
|
}
|
|
|
|
|
2023-06-12 21:41:58 +03:00
|
|
|
/** @nocollapse */
|
2021-12-29 20:45:59 +03:00
|
|
|
static override shadowRootOptions:
|
|
|
|
ShadowRootInit = {mode: 'open', delegatesFocus: true};
|
|
|
|
|
2023-06-05 21:34:12 +03:00
|
|
|
/** @nocollapse */
|
2023-08-19 00:57:07 +03:00
|
|
|
static readonly formAssociated = true;
|
2022-12-17 04:06:12 +03:00
|
|
|
|
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.
|
|
|
|
*/
|
2023-06-22 03:55:24 +03:00
|
|
|
@property({type: Boolean, attribute: 'show-only-selected-icon'})
|
|
|
|
showOnlySelectedIcon = false;
|
2021-09-24 21:31:43 +03:00
|
|
|
|
2023-08-24 10:54:16 +03:00
|
|
|
/**
|
|
|
|
* When true, require the switch to be selected when participating in
|
|
|
|
* form submission.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
|
|
|
|
*/
|
|
|
|
@property({type: Boolean}) required = false;
|
2022-09-16 23:51:46 +03:00
|
|
|
|
2022-12-17 04:06:12 +03:00
|
|
|
/**
|
2023-05-17 03:57:49 +03:00
|
|
|
* The value associated with this switch on form submission. `null` is
|
|
|
|
* submitted when `selected` is `false`.
|
2022-12-17 04:06:12 +03:00
|
|
|
*/
|
2023-05-17 03:57:49 +03:00
|
|
|
@property() value = 'on';
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The HTML name to use in form submission.
|
|
|
|
*/
|
2023-05-17 03:57:49 +03:00
|
|
|
get name() {
|
|
|
|
return this.getAttribute('name') ?? '';
|
|
|
|
}
|
|
|
|
set name(name: string) {
|
|
|
|
this.setAttribute('name', name);
|
|
|
|
}
|
2022-12-13 06:01:29 +03:00
|
|
|
|
|
|
|
/**
|
2023-05-17 03:57:49 +03:00
|
|
|
* The associated form element with which this element's value will submit.
|
2022-12-13 06:01:29 +03:00
|
|
|
*/
|
2023-05-17 03:57:49 +03:00
|
|
|
get form() {
|
|
|
|
return this.internals.form;
|
|
|
|
}
|
2022-12-13 06:01:29 +03:00
|
|
|
|
2023-05-17 03:57:49 +03:00
|
|
|
/**
|
|
|
|
* The labels this element is associated with.
|
|
|
|
*/
|
|
|
|
get labels() {
|
|
|
|
return this.internals.labels;
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
|
|
|
|
2023-08-24 10:54:16 +03:00
|
|
|
/**
|
|
|
|
* Returns a ValidityState object that represents the validity states of the
|
|
|
|
* switch.
|
|
|
|
*
|
|
|
|
* Note that switches will only set `valueMissing` if `required` and not
|
|
|
|
* selected.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
|
|
|
|
*/
|
|
|
|
get validity() {
|
|
|
|
this.syncValidity();
|
|
|
|
return this.internals.validity;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the native validation error message.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
|
|
|
|
*/
|
|
|
|
get validationMessage() {
|
|
|
|
this.syncValidity();
|
|
|
|
return this.internals.validationMessage;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether an element will successfully validate based on forms
|
|
|
|
* validation rules and constraints.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
|
|
|
|
*/
|
|
|
|
get willValidate() {
|
|
|
|
this.syncValidity();
|
|
|
|
return this.internals.willValidate;
|
|
|
|
}
|
|
|
|
|
|
|
|
@query('input') private readonly input!: HTMLInputElement|null;
|
2023-09-12 04:39:20 +03:00
|
|
|
// Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
|
|
|
|
// Replace with this.internals.validity.customError when resolved.
|
|
|
|
private hasCustomValidityError = false;
|
2023-05-17 03:57:49 +03:00
|
|
|
private readonly internals =
|
|
|
|
(this as HTMLElement /* needed for closure */).attachInternals();
|
|
|
|
|
2021-12-29 20:45:59 +03:00
|
|
|
constructor() {
|
|
|
|
super();
|
2023-04-19 23:24:26 +03:00
|
|
|
if (!isServer) {
|
|
|
|
this.addEventListener('click', (event: MouseEvent) => {
|
|
|
|
if (!isActivationClick(event)) {
|
|
|
|
return;
|
|
|
|
}
|
2023-08-24 10:54:16 +03:00
|
|
|
this.focus();
|
|
|
|
dispatchActivationClick(this.input!);
|
2023-04-19 23:24:26 +03:00
|
|
|
});
|
|
|
|
}
|
2021-12-29 20:45:59 +03:00
|
|
|
}
|
2021-09-24 21:31:43 +03:00
|
|
|
|
2023-08-24 10:54:16 +03:00
|
|
|
/**
|
|
|
|
* Checks the switch's native validation and returns whether or not the
|
|
|
|
* element is valid.
|
|
|
|
*
|
|
|
|
* If invalid, this method will dispatch the `invalid` event.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity
|
|
|
|
*
|
|
|
|
* @return true if the switch is valid, or false if not.
|
|
|
|
*/
|
|
|
|
checkValidity() {
|
|
|
|
this.syncValidity();
|
|
|
|
return this.internals.checkValidity();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks the switch's native validation and returns whether or not the
|
|
|
|
* element is valid.
|
|
|
|
*
|
|
|
|
* If invalid, this method will dispatch the `invalid` event.
|
|
|
|
*
|
|
|
|
* The `validationMessage` is reported to the user by the browser. Use
|
|
|
|
* `setCustomValidity()` to customize the `validationMessage`.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity
|
|
|
|
*
|
|
|
|
* @return true if the switch is valid, or false if not.
|
|
|
|
*/
|
|
|
|
reportValidity() {
|
|
|
|
this.syncValidity();
|
|
|
|
return this.internals.reportValidity();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the switch's native validation error message. This is used to
|
|
|
|
* customize `validationMessage`.
|
|
|
|
*
|
|
|
|
* When the error is not an empty string, the switch is considered invalid
|
|
|
|
* and `validity.customError` will be true.
|
|
|
|
*
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity
|
|
|
|
*
|
|
|
|
* @param error The error message to display.
|
|
|
|
*/
|
|
|
|
setCustomValidity(error: string) {
|
2023-09-12 04:39:20 +03:00
|
|
|
this.hasCustomValidityError = !!error;
|
2023-08-24 10:54:16 +03:00
|
|
|
this.internals.setValidity({customError: !!error}, error, this.getInput());
|
|
|
|
}
|
|
|
|
|
2023-05-17 03:57:49 +03:00
|
|
|
protected override update(changed: PropertyValues<Switch>) {
|
|
|
|
const state = String(this.selected);
|
|
|
|
this.internals.setFormValue(this.selected ? this.value : null, state);
|
|
|
|
super.update(changed);
|
|
|
|
}
|
|
|
|
|
2021-09-24 21:31:43 +03:00
|
|
|
protected override render(): TemplateResult {
|
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`
|
2023-08-24 10:54:16 +03:00
|
|
|
<div class="switch ${classMap(this.getRenderClasses())}">
|
|
|
|
<input
|
|
|
|
id="switch"
|
|
|
|
class="touch"
|
|
|
|
type="checkbox"
|
|
|
|
role="switch"
|
|
|
|
aria-label=${(this as ARIAMixin).ariaLabel || nothing}
|
|
|
|
?checked=${this.selected}
|
|
|
|
?disabled=${this.disabled}
|
|
|
|
?required=${this.required}
|
|
|
|
@change=${this.handleChange}
|
|
|
|
>
|
|
|
|
|
|
|
|
<md-focus-ring part="focus-ring" for="switch"></md-focus-ring>
|
2023-05-10 06:16:54 +03:00
|
|
|
<span class="track">
|
2021-09-24 21:31:43 +03:00
|
|
|
${this.renderHandle()}
|
2022-12-20 01:03:21 +03:00
|
|
|
</span>
|
2023-08-24 10:54:16 +03:00
|
|
|
</div>
|
2021-09-24 21:31:43 +03:00
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2023-08-24 10:54:16 +03:00
|
|
|
protected override updated() {
|
|
|
|
// Sync validity when properties change, since validation properties may
|
|
|
|
// have changed.
|
|
|
|
this.syncValidity();
|
|
|
|
}
|
|
|
|
|
2022-12-20 01:03:21 +03:00
|
|
|
private getRenderClasses(): ClassInfo {
|
2021-09-24 21:31:43 +03:00
|
|
|
return {
|
2023-08-24 10:54:16 +03:00
|
|
|
'selected': this.selected,
|
|
|
|
'unselected': !this.selected,
|
|
|
|
'disabled': this.disabled,
|
2021-09-24 21:31:43 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-25 20:40:12 +03:00
|
|
|
private renderHandle() {
|
2022-05-11 02:01:43 +03:00
|
|
|
const classes = {
|
2023-08-17 23:42:33 +03:00
|
|
|
'with-icon': this.showOnlySelectedIcon ? this.selected : this.icons,
|
2022-05-11 02:01:43 +03:00
|
|
|
};
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2023-08-11 20:53:28 +03:00
|
|
|
${this.renderTouchTarget()}
|
2023-05-10 06:16:54 +03:00
|
|
|
<span class="handle-container">
|
2023-05-26 20:05:54 +03:00
|
|
|
<md-ripple for="switch" ?disabled="${this.disabled}"></md-ripple>
|
2023-05-10 06:16:54 +03:00
|
|
|
<span class="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>
|
|
|
|
</span>
|
2021-09-24 21:31:43 +03:00
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2023-04-25 20:40:12 +03:00
|
|
|
private renderIcons() {
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2023-05-10 06:16:54 +03:00
|
|
|
<div class="icons">
|
2022-04-29 21:43:31 +03:00
|
|
|
${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
|
|
|
|
*/
|
2023-04-25 20:40:12 +03:00
|
|
|
private renderOnIcon() {
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2023-05-10 06:16:54 +03:00
|
|
|
<svg class="icon 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
|
|
|
|
*/
|
2023-04-25 20:40:12 +03:00
|
|
|
private renderOffIcon() {
|
2021-09-24 21:31:43 +03:00
|
|
|
return html`
|
2023-05-10 06:16:54 +03:00
|
|
|
<svg class="icon 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>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2023-04-25 20:40:12 +03:00
|
|
|
private renderTouchTarget() {
|
2023-05-10 06:16:54 +03:00
|
|
|
return html`<span class="touch"></span>`;
|
2022-05-06 20:25:04 +03:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-08-24 10:54:16 +03:00
|
|
|
private handleChange(event: Event) {
|
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
|
this.selected = target.checked;
|
|
|
|
redispatchEvent(this, event);
|
|
|
|
}
|
|
|
|
|
|
|
|
private syncValidity() {
|
|
|
|
// Sync the internal <input>'s validity and the host's ElementInternals
|
|
|
|
// validity. We do this to re-use native `<input>` validation messages.
|
|
|
|
const input = this.getInput();
|
2023-09-12 04:39:20 +03:00
|
|
|
if (this.hasCustomValidityError) {
|
2023-08-24 10:54:16 +03:00
|
|
|
input.setCustomValidity(this.internals.validationMessage);
|
|
|
|
} else {
|
|
|
|
input.setCustomValidity('');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.internals.setValidity(
|
|
|
|
input.validity, input.validationMessage, this.getInput());
|
|
|
|
}
|
|
|
|
|
|
|
|
private getInput() {
|
|
|
|
if (!this.input) {
|
|
|
|
// If the input is not yet defined, synchronously render.
|
|
|
|
this.connectedCallback();
|
|
|
|
this.performUpdate();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.isUpdatePending) {
|
|
|
|
// If there are pending updates, synchronously perform them. This ensures
|
|
|
|
// that constraint validation properties (like `required`) are synced
|
|
|
|
// before interacting with input APIs that depend on them.
|
|
|
|
this.scheduleUpdate();
|
2022-03-23 04:18:05 +03:00
|
|
|
}
|
|
|
|
|
2023-08-24 10:54:16 +03:00
|
|
|
return this.input!;
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|
2023-05-17 03:57:49 +03:00
|
|
|
|
|
|
|
/** @private */
|
|
|
|
formResetCallback() {
|
|
|
|
// The selected property does not reflect, so the original attribute set by
|
|
|
|
// the user is used to determine the default value.
|
|
|
|
this.selected = this.hasAttribute('selected');
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @private */
|
|
|
|
formStateRestoreCallback(state: string) {
|
|
|
|
this.selected = state === 'true';
|
|
|
|
}
|
2021-09-24 21:31:43 +03:00
|
|
|
}
|