UNPKG

@conectate/ct-checkbox

Version:

HTML checkbox web component based on Material design

278 lines (275 loc) 8.38 kB
import { __decorate } from "tslib"; import "@conectate/ct-icon"; import { CtLit, css, customElement, html, property, query } from "@conectate/ct-lit"; import { classMap } from "lit/directives/class-map.js"; /** * ## `ct-checkbox` * A customizable checkbox component built with LitElement. * * ### Usage * ```html * <ct-checkbox>Check me</ct-checkbox> * <ct-checkbox checked>I'm checked</ct-checkbox> * <ct-checkbox indeterminate>Indeterminate state</ct-checkbox> * <ct-checkbox disabled>Can't check me</ct-checkbox> * ``` * * ### Events * - `checked`: Fired when the checked state changes, with `detail: {checked: boolean}` * - `change`: Standard input change event (redispatched) * * ### Styling * Custom properties available for styling: * - `--ct-checkbox-box-size`: Size of the checkbox (default: 24px) * - `--ct-checkbox-box-border-radius`: Border radius of checkbox (default: 8px) * - `--ct-checkbox-height`: Height of the checkbox component (default: same as box size) * - `--ct-checkbox-box-border-size`: Border size of unchecked box (default: 3px) * - `--color-primary`: Color used for the checked state * - `--color-on-primary`: Color of the checkmark * - `--color-on-background`: Color of the unchecked checkbox border * * @group ct-elements * @element ct-checkbox * @fires checked - Fired when checked state changes * @fires change - Standard input change event */ let CtCheckbox = class CtCheckbox extends CtLit { constructor() { super(...arguments); /** * When true, the checkbox is in the indeterminate state */ this.indeterminate = false; /** * When true, the checkbox is disabled and cannot be interacted with */ this.disabled = false; /** * When true, the checkbox is in the checked state */ this.checked = false; /** * Name attribute for form submission */ this.name = ""; /** * Text label (alternative to using the slot) */ this.label = ""; } static { this.styles = [ css ` :host { display: inline-flex; position: relative; /* --ct-checkbox-box-size: 24px; --ct-checkbox-box-border-radius: 8px; --ct-checkbox-height: var(--ct-checkbox-box-size); --ct-checkbox-box-border-size: 3px; */ } #input:focus-visible + .c { border-radius: var(--ct-checkbox-box-border-radius, 8px); box-shadow: 0 0 0 1px var(--color-primary); } :host([disabled]), :host([disabled]) #input:focus-visible { pointer-events: none; opacity: 0.33; } .c { display: flex; flex-direction: row; align-items: center; } #input { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0px; overflow: visible; box-sizing: border-box; padding: 0px; position: absolute; width: 100%; height: 100%; opacity: 0.0001; z-index: 1; cursor: pointer; } `, // Checkmark css ` #checkmark { opacity: 0; transform: scale(0); transition: opacity 0.13s ease-in-out, transform 0.13s ease-in-out; } :host([indeterminate]) #box #checkmark, #input:checked + .c > #box #checkmark { transform: scale(1); opacity: 1; } #checkmark { font-size: calc(var(--ct-checkbox-box-size, 24px) - 4px); height: calc(var(--ct-checkbox-box-size, 24px) - 4px); width: calc(var(--ct-checkbox-box-size, 24px) - 4px); overflow: hidden; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } #checkmark.rotate { animation: rotate 0.3s none ease-out; animation: rotate 0.3s none cubic-bezier(0.6, 0.49, 0.46, 0.85); } `, // Box css ` #box { display: flex; align-items: center; justify-content: center; position: relative; box-sizing: border-box; width: var(--ct-checkbox-box-size, 24px); height: var(--ct-checkbox-box-size, 24px); margin: calc((var(--ct-checkbox-height, var(--ct-checkbox-box-size, 24px)) - var(--ct-checkbox-box-size, 24px)) / 2) 0; flex-grow: 0; color: var(--color-on-primary, #fff); flex-shrink: 0; margin: 8px; } #box::before { display: block; z-index: 0; inset: 0; content: ""; box-sizing: border-box; position: absolute; width: var(--ct-checkbox-box-size, 24px); height: var(--ct-checkbox-box-size, 24px); border-radius: var(--ct-checkbox-box-border-radius, 8px); border-width: var(--ct-checkbox-box-border-size, 3px); border-style: solid; border-color: var(--color-on-background, #535353); transition: border 0.13s ease-in-out, box-shadow 0.13s ease-in-out; } :host([indeterminate]) #box::before, #input:checked + .c > #box::before { border-width: calc(var(--ct-checkbox-box-size, 24px) / 2); } #input:checked + .c > #box::before, :host([indeterminate]) #box::before { border-color: var(--color-primary, #2cb5e8); color: var(--color-on-primary, #fff); } ` ]; } /** * Renders the checkbox component * @returns {TemplateResult} The rendered template */ render() { return html ` <input id="input" type="checkbox" @click=${this.toogleCheck} .checked=${this.checked} .disabled=${this.disabled} @change=${this.handleChange} /> <div class="c"> <span id="box"> <ct-icon id="checkmark" class=${classMap({ rotate: this.indeterminate == false && this.checked })} icon="${this.indeterminate ? "horizontal_rule" : `check`}" dir="ltr"></ct-icon> </span> <label id="label" for="input">${this.label}<slot></slot></label> </div> `; } /** * Handles property updates and triggers events when checked state changes * @param {PropertyValueMap<any> | Map<PropertyKey, unknown>} _changedProperties - Map of changed properties */ updated(_changedProperties) { if (_changedProperties.has("checked") && _changedProperties.get("checked") != undefined) { this.change(); } } /** * Programmatically clicks the checkbox */ click() { this.$input.click(); } /** * Updates the checked state based on the input element's checked state * @private */ toogleCheck() { this.checked = this.$input.checked; } /** * Handles the change event from the input element * @param {Event} event - The change event * @private */ handleChange(event) { const target = event.target; this.checked = target.checked; this.indeterminate = target.indeterminate; redispatchEvent(this, event); } /** * Dispatches a checked event when the checked state changes * @private */ change() { if (this.checked) this.indeterminate = false; this.dispatchEvent(new CustomEvent("checked", { detail: { checked: this.$input.checked } })); } }; __decorate([ property({ type: Boolean, reflect: true }) ], CtCheckbox.prototype, "indeterminate", void 0); __decorate([ property({ type: Boolean, reflect: true }) ], CtCheckbox.prototype, "disabled", void 0); __decorate([ property({ type: Boolean, reflect: true }) ], CtCheckbox.prototype, "checked", void 0); __decorate([ property({ type: Object }) ], CtCheckbox.prototype, "value", void 0); __decorate([ property({ type: String }) ], CtCheckbox.prototype, "name", void 0); __decorate([ property({ type: String }) ], CtCheckbox.prototype, "label", void 0); __decorate([ query("#input") ], CtCheckbox.prototype, "$input", void 0); CtCheckbox = __decorate([ customElement("ct-checkbox") ], CtCheckbox); export { CtCheckbox }; /** * Redispatches an event from a web component * @param {Element} wc - The web component element * @param {Event} event - The event to redispatch * @returns {boolean} Whether the event was dispatched successfully * @private */ function redispatchEvent(wc, event) { if (event.bubbles && (!wc.shadowRoot || event.composed)) event.stopPropagation(); const copy = Reflect.construct(event.constructor, [event.type, event]); const dispatched = wc.dispatchEvent(copy); if (!dispatched) event.preventDefault(); return dispatched; }