UNPKG

@lion/ui

Version:

A package of extendable web components

223 lines (195 loc) 6.61 kB
import { html, LitElement, css } from 'lit'; import { DisabledWithTabIndexMixin } from '@lion/ui/core.js'; const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter'; const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' '; // TODO: simplify html structure, as we got rid of the wrapper element for IE11 /** * @typedef {import('lit').TemplateResult} TemplateResult */ /** * Use LionButton (or LionButtonReset|LionButtonSubmit) when there is a need to extend HTMLButtonElement. * It allows to create complex shadow DOM for buttons needing this. Think of: * - a material Design button that needs a JS controlled ripple * - a LionSelectRich invoker that needs a complex shadow DOM structure * (for styling/maintainability purposes) * - a specialized button (for instance a primary button or icon button in a Design System) that * needs a simple api: `<my-button>text</my-button>` is always better than * `<button class="my-button"><div class="my-button__container">text</div><button>` * * In other cases, whenever you can, still use native HTMLButtonElement (`<button>`). * * Note that LionButton is meant for buttons with type="button". It's cleaner and more * lightweight than LionButtonReset and LionButtonSubmit, which should only be considered when native * `<form>` support is needed: * - When type="reset|submit" should be supported, use LionButtonReset. * - When implicit form submission should be supported on top, use LionButtonSubmit. * * @customElement lion-button */ export class LionButton extends DisabledWithTabIndexMixin(LitElement) { static get properties() { return { active: { type: Boolean, reflect: true }, type: { type: String, reflect: true }, }; } render() { return html` <div class="button-content"><slot></slot></div> `; } static get styles() { return [ css` :host { position: relative; display: inline-flex; box-sizing: border-box; vertical-align: middle; line-height: 24px; background-color: #eee; /* minimal styling to make it recognizable as btn */ padding: 8px; /* padding to fix with min-height */ outline: none; /* focus style handled below */ cursor: default; /* we should always see the default arrow, never a caret */ /* TODO: remove, native button also allows selection. Could be usability concern... */ user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } :host::before { content: ''; /* center vertically and horizontally */ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* Minimum click area to meet [WCAG Success Criterion 2.5.5 Target Size (Enhanced)](https://www.w3.org/TR/WCAG22/#target-size-enhanced) */ min-height: 44px; min-width: 44px; width: 100%; height: 100%; } .button-content { display: flex; align-items: center; justify-content: center; } /* Show focus styles on keyboard focus. */ :host(:focus:not([disabled])), :host(:focus-visible) { /* if you extend, please overwrite */ outline: 2px solid #bde4ff; } /* Hide focus styles if they're not needed, for example, when an element receives focus via the mouse. */ :host(:focus:not(:focus-visible)) { outline: 0; } :host(:hover) { /* if you extend, please overwrite */ background: #f4f6f7; } :host(:active), /* keep native :active to render quickly where possible */ :host([active]) /* use custom [active] to fix IE11 */ { /* if you extend, please overwrite */ background: gray; } :host([hidden]) { display: none; } :host([disabled]) { pointer-events: none; /* if you extend, please overwrite */ background: lightgray; color: #adadad; fill: #adadad; } `, ]; } constructor() { super(); this.type = 'button'; this.active = false; this.__setupEvents(); } connectedCallback() { super.connectedCallback(); if (!this.hasAttribute('role')) { this.setAttribute('role', 'button'); } } /** * @param {import('lit').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('disabled')) { if (this.disabled) { this.setAttribute('aria-disabled', 'true'); } else if (this.getAttribute('aria-disabled') !== null) { this.removeAttribute('aria-disabled'); } } } /** * @private */ __setupEvents() { this.addEventListener('mousedown', this.__mousedownHandler); this.addEventListener('keydown', this.__keydownHandler); this.addEventListener('keyup', this.__keyupHandler); } /** * @private */ __mousedownHandler() { this.active = true; const mouseupHandler = () => { this.active = false; document.removeEventListener('mouseup', mouseupHandler); this.removeEventListener('mouseup', mouseupHandler); }; document.addEventListener('mouseup', mouseupHandler); this.addEventListener('mouseup', mouseupHandler); } /** * @param {KeyboardEvent} event * @private */ __keydownHandler(event) { if (this.active || !isKeyboardClickEvent(event)) { if (isSpaceKeyboardClickEvent(event)) { event.preventDefault(); } return; } if (isSpaceKeyboardClickEvent(event)) { event.preventDefault(); } this.active = true; /** * @param {KeyboardEvent} keyupEvent */ const keyupHandler = keyupEvent => { if (isKeyboardClickEvent(keyupEvent)) { this.active = false; document.removeEventListener('keyup', keyupHandler, true); } }; document.addEventListener('keyup', keyupHandler, true); } /** * @param {KeyboardEvent} event * @private */ __keyupHandler(event) { if (isKeyboardClickEvent(event)) { // Fixes IE11 double submit/click. Enter keypress somehow triggers the __keyUpHandler on the native <button> if (event.target && event.target !== this) { return; } // dispatch click this.click(); } } }