/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import '../../focus/md-focus-ring.js'; import '../../ripple/ripple.js'; import {html, isServer, LitElement, nothing, PropertyValues} from 'lit'; import {property, query, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {mixinDelegatesAria} from '../../internal/aria/delegate.js'; import { dispatchActivationClick, isActivationClick, } from '../../internal/events/form-label-activation.js'; import {redispatchEvent} from '../../internal/events/redispatch-event.js'; import { createValidator, getValidityAnchor, mixinConstraintValidation, } from '../../labs/behaviors/constraint-validation.js'; import {mixinElementInternals} from '../../labs/behaviors/element-internals.js'; import { getFormState, getFormValue, mixinFormAssociated, } from '../../labs/behaviors/form-associated.js'; import {CheckboxValidator} from '../../labs/behaviors/validators/checkbox-validator.js'; // Separate variable needed for closure. const checkboxBaseClass = mixinDelegatesAria( mixinConstraintValidation( mixinFormAssociated(mixinElementInternals(LitElement)), ), ); /** * A checkbox component. * * * @fires change {Event} The native `change` event on * [``](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) * --bubbles * @fires input {InputEvent} The native `input` event on * [``](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event) * --bubbles --composed */ export class Checkbox extends checkboxBaseClass { /** @nocollapse */ static override shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; /** * Whether or not the checkbox is selected. */ @property({type: Boolean}) checked = false; /** * Whether or not the checkbox is indeterminate. * * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#indeterminate_state_checkboxes */ @property({type: Boolean}) indeterminate = false; /** * When true, require the checkbox 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; /** * The value of the checkbox that is submitted with a form when selected. * * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#value */ @property() value = 'on'; @state() private prevChecked = false; @state() private prevDisabled = false; @state() private prevIndeterminate = false; @query('input') private readonly input!: HTMLInputElement | null; constructor() { super(); if (!isServer) { this.addEventListener('click', (event: MouseEvent) => { if (!isActivationClick(event) || !this.input) { return; } this.focus(); dispatchActivationClick(this.input); }); } } protected override update(changed: PropertyValues) { if ( changed.has('checked') || changed.has('disabled') || changed.has('indeterminate') ) { this.prevChecked = changed.get('checked') ?? this.checked; this.prevDisabled = changed.get('disabled') ?? this.disabled; this.prevIndeterminate = changed.get('indeterminate') ?? this.indeterminate; } super.update(changed); } protected override render() { const prevNone = !this.prevChecked && !this.prevIndeterminate; const prevChecked = this.prevChecked && !this.prevIndeterminate; const prevIndeterminate = this.prevIndeterminate; const isChecked = this.checked && !this.indeterminate; const isIndeterminate = this.indeterminate; const containerClasses = classMap({ 'disabled': this.disabled, 'selected': isChecked || isIndeterminate, 'unselected': !isChecked && !isIndeterminate, 'checked': isChecked, 'indeterminate': isIndeterminate, 'prev-unselected': prevNone, 'prev-checked': prevChecked, 'prev-indeterminate': prevIndeterminate, 'prev-disabled': this.prevDisabled, }); // Needed for closure conformance const {ariaLabel, ariaInvalid} = this as ARIAMixinStrict; // Note: needs to be rendered before the for // form.reportValidity() to work in Chrome. return html`
`; } private handleInput(event: Event) { const target = event.target as HTMLInputElement; this.checked = target.checked; this.indeterminate = target.indeterminate; // 'input' event bubbles and is composed, don't re-dispatch it. } private handleChange(event: Event) { // 'change' event is not composed, re-dispatch it. redispatchEvent(this, event); } // Writable mixin properties for lit-html binding, needed for lit-analyzer declare disabled: boolean; declare name: string; override [getFormValue]() { if (!this.checked || this.indeterminate) { return null; } return this.value; } override [getFormState]() { return String(this.checked); } override formResetCallback() { // The checked property does not reflect, so the original attribute set by // the user is used to determine the default value. this.checked = this.hasAttribute('checked'); } override formStateRestoreCallback(state: string) { this.checked = state === 'true'; } override [createValidator]() { return new CheckboxValidator(() => this); } override [getValidityAnchor]() { return this.input; } }