// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview 'cr-checkbox' is a component similar to native checkbox.
*
* Forked from ui/webui/resources/cr_elements/cr_checkbox/cr_checkbox.ts
*
* Fires a 'change' event *only* when its state changes as a result of a user
* interaction. By default it assumes there will be child(ren) passed in to be
* used as labels. If no label will be provided, a .no-label class should be
* added to hide the spacing between the checkbox and the label container.
*
* If a label is provided, it will be shown by default after the checkbox. A
* .label-first CSS class can be added to show the label before the checkbox.
*
* List of customizable styles:
* --cr-checkbox-border-size
* --cr-checkbox-checked-box-background-color
* --cr-checkbox-checked-box-color
* --cr-checkbox-label-color
* --cr-checkbox-label-padding-start
* --cr-checkbox-mark-color
* --cr-checkbox-ripple-checked-color
* --cr-checkbox-ripple-size
* --cr-checkbox-ripple-unchecked-color
* --cr-checkbox-size
* --cr-checkbox-unchecked-box-color
*/
import '../cr_shared_vars.css.js';
import {PaperRippleMixin} from '//resources/polymer/v3_0/paper-behaviors/paper-ripple-mixin.js';
import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './cr_checkbox.html.js';
const CrCheckboxElementBase = PaperRippleMixin(PolymerElement);
export interface CrCheckboxElement {
$: {
checkbox: HTMLElement,
};
}
export class CrCheckboxElement extends CrCheckboxElementBase {
static get is() {
return 'cr-checkbox';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
checked: {
type: Boolean,
value: false,
reflectToAttribute: true,
observer: 'checkedChanged_',
notify: true,
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
observer: 'disabledChanged_',
},
ariaDescription: String,
ariaLabelOverride: String,
tabIndex: {
type: Number,
value: 0,
observer: 'onTabIndexChanged_',
},
};
}
checked: boolean;
disabled: boolean;
override ariaDescription: string|null;
ariaLabelOverride: string;
override tabIndex: number;
/* eslint-disable-next-line @typescript-eslint/naming-convention */
override _rippleContainer: Element;
override ready() {
super.ready();
// TODO(b/309689294) Remove this once CrOS UIs migrate to Jellybean
// components and no longer use cr-elements.
// Force stamp the ripple element to enable CrOS focus styles. Ripple
// visibility is controlled by the event listeners below.
if (document.documentElement.hasAttribute('chrome-refresh-2023')) {
this.getRipple();
}
this.removeAttribute('unresolved');
this.addEventListener('click', this.onClick_.bind(this));
this.addEventListener('pointerup', this.hideRipple_.bind(this));
if (document.documentElement.hasAttribute('chrome-refresh-2023')) {
this.addEventListener('pointerdown', this.showRipple_.bind(this));
this.addEventListener('pointerleave', this.hideRipple_.bind(this));
} else {
this.addEventListener('blur', this.hideRipple_.bind(this));
this.addEventListener('focus', this.showRipple_.bind(this));
}
}
override focus() {
this.$.checkbox.focus();
}
getFocusableElement(): HTMLElement {
return this.$.checkbox;
}
private checkedChanged_() {
this.$.checkbox.setAttribute(
'aria-checked', this.checked ? 'true' : 'false');
}
private disabledChanged_(_current: boolean, previous: boolean) {
if (previous === undefined && !this.disabled) {
return;
}
this.tabIndex = this.disabled ? -1 : 0;
this.$.checkbox.setAttribute(
'aria-disabled', this.disabled ? 'true' : 'false');
}
private showRipple_() {
if (this.noink) {
return;
}
this.getRipple().showAndHoldDown();
}
private hideRipple_() {
this.getRipple().clear();
}
private onClick_(e: Event) {
if (this.disabled || (e.target as HTMLElement).tagName === 'A') {
return;
}
// Prevent |click| event from bubbling. It can cause parents of this
// elements to erroneously re-toggle this control.
e.stopPropagation();
e.preventDefault();
this.checked = !this.checked;
this.dispatchEvent(new CustomEvent(
'change', {bubbles: true, composed: true, detail: this.checked}));
}
private onKeyDown_(e: KeyboardEvent) {
if (e.key !== ' ' && e.key !== 'Enter') {
return;
}
e.preventDefault();
e.stopPropagation();
if (e.repeat) {
return;
}
if (e.key === 'Enter') {
this.click();
}
}
private onKeyUp_(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
}
if (e.key === ' ') {
this.click();
}
}
private onTabIndexChanged_() {
// :host shouldn't have a tabindex because it's set on #checkbox.
this.removeAttribute('tabindex');
}
// Overridden from PaperRippleMixin
/* eslint-disable-next-line @typescript-eslint/naming-convention */
override _createRipple() {
this._rippleContainer = this.$.checkbox;
const ripple = super._createRipple();
ripple.id = 'ink';
ripple.setAttribute('recenters', '');
ripple.classList.add('circle');
return ripple;
}
}
declare global {
interface HTMLElementTagNameMap {
'cr-checkbox': CrCheckboxElement;
}
}
customElements.define(CrCheckboxElement.is, CrCheckboxElement);