Files
webawesome/src/components/button/button.ts

313 lines
12 KiB
TypeScript
Raw Normal View History

2024-04-17 11:20:27 -04:00
import '../icon/icon.js';
import '../spinner/spinner.js';
import { classMap } from 'lit/directives/class-map.js';
2024-05-08 16:16:14 -04:00
import { customElement, property, query, state } from 'lit/decorators.js';
2024-04-17 11:20:27 -04:00
import { HasSlotController } from '../../internal/slot.js';
import { html, literal } from 'lit/static-html.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
2024-05-08 14:24:11 -04:00
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
2024-04-17 11:20:27 -04:00
import { watch } from '../../internal/watch.js';
2024-05-08 14:24:11 -04:00
import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js';
2024-04-17 11:20:27 -04:00
import componentStyles from '../../styles/component.styles.js';
import styles from './button.styles.js';
import type { CSSResultGroup } from 'lit';
2024-04-17 11:20:27 -04:00
/**
* @summary Buttons represent actions that are available to the user.
* @documentation https://shoelace.style/components/button
* @status stable
* @since 2.0
*
* @dependency wa-icon
* @dependency wa-spinner
*
* @event wa-blur - Emitted when the button loses focus.
* @event wa-focus - Emitted when the button gains focus.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @slot - The button's label.
* @slot prefix - A presentational prefix icon or similar element.
* @slot suffix - A presentational suffix icon or similar element.
*
* @csspart base - The component's base wrapper.
* @csspart prefix - The container that wraps the prefix.
* @csspart label - The button's label.
* @csspart suffix - The container that wraps the suffix.
* @csspart caret - The button's caret icon, a `<wa-icon>` element.
* @csspart spinner - The spinner that shows when the button is in the loading state.
*
* @cssproperty --background - The button's background styles.
* @cssproperty --background-active - The color of the button's background when active.
* @cssproperty --background-hover - The color of the button's background on hover.
* @cssproperty --border-color - The color of the button's border.
* @cssproperty --border-color-active - The color of the button's border when active.
* @cssproperty --border-color-hover - The color of the button's border on hover.
* @cssproperty --border-radius - The radius of the button's corners.
* @cssproperty --border-style - The style of the button's border.
* @cssproperty --border-width - The width of the button's border. Expects a single value.
* @cssproperty --box-shadow - The shadow effects around the edges of the button.
* @cssproperty --box-shadow-color - A bespoke shadow color for the button.
* @cssproperty --label-color - The color of the button's label.
* @cssproperty --label-color-active - The color of the button's label when active.
* @cssproperty --label-color-hover - The color of the button's label on hover.
*/
2024-05-08 16:16:14 -04:00
@customElement('wa-button')
2024-05-08 14:24:11 -04:00
export default class WaButton extends WebAwesomeFormAssociated {
2024-04-17 11:20:27 -04:00
static styles: CSSResultGroup = [componentStyles, styles];
2024-05-08 16:16:14 -04:00
static get validators() {
2024-05-11 03:28:23 -04:00
return [
...super.validators,
MirrorValidator()
];
2024-05-08 14:24:11 -04:00
}
2024-05-08 16:16:14 -04:00
assumeInteractionOn = ['click'];
2024-04-17 11:20:27 -04:00
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
private readonly localize = new LocalizeController(this);
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
@state() private hasFocus = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
/** The button's theme variant. */
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'text' = 'neutral';
/** The button's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** Draws the button with a caret. Used to indicate that the button triggers a dropdown menu or similar behavior. */
@property({ type: Boolean, reflect: true }) caret = false;
/** Disables the button. */
2024-05-08 14:24:11 -04:00
@property({ type: Boolean }) disabled = false;
2024-04-17 11:20:27 -04:00
/** Draws the button in a loading state. */
@property({ type: Boolean, reflect: true }) loading = false;
/** Draws an outlined button. */
@property({ type: Boolean, reflect: true }) outline = false;
/** Draws a pill-style button with rounded edges. */
@property({ type: Boolean, reflect: true }) pill = false;
/**
* The type of button. Note that the default value is `button` instead of `submit`, which is opposite of how native
* `<button>` elements behave. When the type is `submit`, the button will submit the surrounding form.
*/
@property() type: 'button' | 'submit' | 'reset' = 'button';
/**
* The name of the button, submitted as a name/value pair with form data, but only when this button is the submitter.
* This attribute is ignored when `href` is present.
*/
@property() name = '';
/**
* The value of the button, submitted as a pair with the button's name as part of the form data, but only when this
* button is the submitter. This attribute is ignored when `href` is present.
*/
2024-05-08 14:24:11 -04:00
@property({ reflect: true }) value = '';
2024-04-17 11:20:27 -04:00
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
@property() href = '';
/** Tells the browser where to open the link. Only used when `href` is present. */
@property() target: '_blank' | '_parent' | '_self' | '_top';
/**
* When using `href`, this attribute will map to the underlying link's `rel` attribute. Unlike regular links, the
* default is `noreferrer noopener` to prevent security exploits. However, if you're using `target` to point to a
* specific tab/window, this will prevent that from working correctly. You can remove or change the default value by
* setting the attribute to an empty string or a value of your choice, respectively.
*/
@property() rel = 'noreferrer noopener';
/** Tells the browser to download the linked file as this filename. Only used when `href` is present. */
@property() download?: string;
/**
* The "form owner" to associate the button with. If omitted, the closest containing form will be used instead. The
* value of this attribute must be an id of a form in the same document or shadow root as the button.
*/
2024-05-08 16:16:14 -04:00
@property({ reflect: true }) form: string | null = null;
2024-04-17 11:20:27 -04:00
/** Used to override the form owner's `action` attribute. */
@property({ attribute: 'formaction' }) formAction: string;
/** Used to override the form owner's `enctype` attribute. */
@property({ attribute: 'formenctype' })
formEnctype: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
/** Used to override the form owner's `method` attribute. */
@property({ attribute: 'formmethod' }) formMethod: 'post' | 'get';
/** Used to override the form owner's `novalidate` attribute. */
@property({ attribute: 'formnovalidate', type: Boolean }) formNoValidate: boolean;
/** Used to override the form owner's `target` attribute. */
@property({ attribute: 'formtarget' }) formTarget: '_self' | '_blank' | '_parent' | '_top' | string;
private handleBlur() {
this.hasFocus = false;
this.emit('wa-blur');
}
private handleFocus() {
this.hasFocus = true;
this.emit('wa-focus');
}
private handleClick() {
2024-05-08 16:16:14 -04:00
const form = this.getForm();
2024-04-17 11:20:27 -04:00
2024-05-08 16:16:14 -04:00
if (!form) return;
2024-05-08 14:24:11 -04:00
2024-05-08 16:16:14 -04:00
const lightDOMButton = this.constructLightDOMButton();
2024-05-08 14:24:11 -04:00
// form.append(lightDOMButton);
2024-05-08 16:16:14 -04:00
this.parentElement?.append(lightDOMButton);
2024-05-08 14:24:11 -04:00
lightDOMButton.click();
lightDOMButton.remove();
2024-04-17 11:20:27 -04:00
}
2024-05-08 16:16:14 -04:00
private constructLightDOMButton() {
2024-05-08 14:24:11 -04:00
const button = document.createElement('button');
button.type = this.type;
button.style.position = 'absolute';
button.style.width = '0';
button.style.height = '0';
button.style.clipPath = 'inset(50%)';
button.style.overflow = 'hidden';
button.style.whiteSpace = 'nowrap';
button.name = this.name;
button.value = this.value;
['form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
if (this.hasAttribute(attr)) {
button.setAttribute(attr, this.getAttribute(attr)!);
}
});
2024-05-08 16:16:14 -04:00
return button;
2024-05-08 14:24:11 -04:00
}
private handleInvalid() {
2024-05-08 16:16:14 -04:00
this.emit('wa-invalid');
2024-04-17 11:20:27 -04:00
}
private isButton() {
return this.href ? false : true;
}
private isLink() {
return this.href ? true : false;
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
2024-05-08 16:16:14 -04:00
this.updateValidity();
2024-05-08 14:24:11 -04:00
}
// eslint-disable-next-line
2024-05-08 16:16:14 -04:00
setValue(..._args: Parameters<WebAwesomeFormAssociated['setValue']>) {
2024-05-08 14:24:11 -04:00
// This is just a stub. We dont ever actually want to set a value on the form. That happens when the button is clicked and added
// via the light dom button.
2024-04-17 11:20:27 -04:00
}
/** Simulates a click on the button. */
click() {
this.button.click();
}
/** Sets focus on the button. */
focus(options?: FocusOptions) {
this.button.focus(options);
}
/** Removes focus from the button. */
blur() {
this.button.blur();
}
render() {
const isLink = this.isLink();
const tag = isLink ? literal`a` : literal`button`;
/* eslint-disable lit/no-invalid-html */
/* eslint-disable lit/binding-positions */
return html`
<${tag}
part="base"
class=${classMap({
button: true,
'button--brand': this.variant === 'brand',
'button--success': this.variant === 'success',
'button--neutral': this.variant === 'neutral',
'button--warning': this.variant === 'warning',
'button--danger': this.variant === 'danger',
'button--text': this.variant === 'text',
'button--small': this.size === 'small',
'button--medium': this.size === 'medium',
'button--large': this.size === 'large',
'button--caret': this.caret,
'button--disabled': this.disabled,
'button--focused': this.hasFocus,
'button--loading': this.loading,
'button--standard': !this.outline,
'button--outline': this.outline,
'button--pill': this.pill,
'button--rtl': this.localize.dir() === 'rtl',
'button--has-label': this.hasSlotController.test('[default]'),
'button--has-prefix': this.hasSlotController.test('prefix'),
'button--has-suffix': this.hasSlotController.test('suffix')
})}
?disabled=${ifDefined(isLink ? undefined : this.disabled)}
type=${ifDefined(isLink ? undefined : this.type)}
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${ifDefined(isLink ? undefined : this.name)}
value=${ifDefined(isLink ? undefined : this.value)}
href=${ifDefined(isLink ? this.href : undefined)}
target=${ifDefined(isLink ? this.target : undefined)}
download=${ifDefined(isLink ? this.download : undefined)}
rel=${ifDefined(isLink ? this.rel : undefined)}
role=${ifDefined(isLink ? undefined : 'button')}
aria-disabled=${this.disabled ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@invalid=${this.isButton() ? this.handleInvalid : null}
@click=${this.handleClick}
>
<slot name="prefix" part="prefix" class="button__prefix"></slot>
<slot part="label" class="button__label"></slot>
<slot name="suffix" part="suffix" class="button__suffix"></slot>
${
this.caret
? html`
<wa-icon
part="caret"
class="button__caret"
library="system"
name="chevron-down"
variant="solid"
></wa-icon>
`
: ''
}
${this.loading ? html`<wa-spinner part="spinner"></wa-spinner>` : ''}
</${tag}>
`;
/* eslint-enable lit/no-invalid-html */
/* eslint-enable lit/binding-positions */
}
}
declare global {
interface HTMLElementTagNameMap {
2023-09-08 13:45:49 -04:00
'wa-button': WaButton;
}
}