lion/packages/ui/components/button/LionButton.js
2022-10-31 16:55:07 +01:00

222 lines
6.6 KiB
JavaScript

import { html, LitElement, css } from 'lit';
import { browserDetection, DisabledWithTabIndexMixin } from '@lion/ui/core.js';
const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter';
const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ';
/**
* @typedef {import('@lion/core').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.
*/
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" id="${this._buttonId}"><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: #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._buttonId = `button-${Math.random().toString(36).substr(2, 10)}`;
if (browserDetection.isIE11) {
this.updateComplete.then(() => {
if (!this.hasAttribute('aria-labelledby')) {
this.setAttribute('aria-labelledby', this._buttonId);
}
});
}
this.__setupEvents();
}
connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button');
}
}
/**
* @param {import('@lion/core').PropertyValues } changedProperties
*/
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', `${this.disabled}`); // create mixin if we need it in more places
}
}
/**
* @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();
}
}
}