2024-04-17 11:20:27 -04:00
|
|
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
2024-12-14 17:00:28 -05:00
|
|
|
import { classMap } from 'lit/directives/class-map.js';
|
2024-04-17 11:20:27 -04:00
|
|
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
2024-12-14 17:00:28 -05:00
|
|
|
import { html, literal } from 'lit/static-html.js';
|
2024-05-31 14:28:19 -04:00
|
|
|
import { WaBlurEvent } from '../../events/blur.js';
|
|
|
|
|
import { WaFocusEvent } from '../../events/focus.js';
|
|
|
|
|
import { WaInvalidEvent } from '../../events/invalid.js';
|
2024-12-14 17:00:28 -05: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-23 16:16:45 -04:00
|
|
|
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
|
2024-12-16 12:18:45 -05:00
|
|
|
import nativeStyles from '../../styles/native/button.css';
|
2024-12-17 02:02:30 -05:00
|
|
|
import sizeStyles from '../../styles/shadow/size.css';
|
2024-12-17 03:03:55 -05:00
|
|
|
import appearanceStyles from '../../styles/utilities/appearance.css';
|
2024-12-17 02:02:30 -05:00
|
|
|
import variantStyles from '../../styles/utilities/variants.css';
|
2024-12-14 17:00:28 -05:00
|
|
|
import { LocalizeController } from '../../utilities/localize.js';
|
|
|
|
|
import '../icon/icon.js';
|
|
|
|
|
import '../spinner/spinner.js';
|
2024-12-12 12:30:13 -05:00
|
|
|
import styles from './button.css';
|
2023-08-11 10:09:44 -07:00
|
|
|
|
2024-04-17 11:20:27 -04:00
|
|
|
/**
|
|
|
|
|
* @summary Buttons represent actions that are available to the user.
|
2024-06-20 11:26:24 -04:00
|
|
|
* @documentation https://backers.webawesome.com/docs/components/button
|
2024-04-17 11:20:27 -04:00
|
|
|
* @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.
|
|
|
|
|
*
|
2024-12-17 02:02:30 -05:00
|
|
|
* @cssproperty --background-color - The button's background color when the button is not being interacted with.
|
2024-06-11 23:27:03 -04:00
|
|
|
* @cssproperty --background-color-active - The button's background color when active.
|
|
|
|
|
* @cssproperty --background-color-hover - The button's background color on hover.
|
2024-12-17 02:02:30 -05:00
|
|
|
* @cssproperty --border-color - The color of the button's border when the button is not being interacted with.
|
2024-04-17 11:20:27 -04:00
|
|
|
* @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.
|
2024-12-17 10:39:47 -05:00
|
|
|
* @cssproperty --text-color - The color of the button's label when the button is not being interacted with.
|
|
|
|
|
* @cssproperty --text-color-active - The color of the button's label when active.
|
|
|
|
|
* @cssproperty --text-color-hover - The color of the button's label on hover.
|
2024-04-17 11:20:27 -04:00
|
|
|
*/
|
|
|
|
|
@customElement('wa-button')
|
2024-05-23 16:16:45 -04:00
|
|
|
export default class WaButton extends WebAwesomeFormAssociatedElement {
|
2024-12-17 03:03:55 -05:00
|
|
|
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
|
2023-08-11 10:09:44 -07:00
|
|
|
|
2024-05-08 16:16:14 -04:00
|
|
|
static get validators() {
|
2024-05-11 03:29:35 -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 localize = new LocalizeController(this);
|
|
|
|
|
|
2024-12-17 02:02:30 -05:00
|
|
|
@query('.wa-button') button: HTMLButtonElement | HTMLLinkElement;
|
2024-04-17 11:20:27 -04:00
|
|
|
|
|
|
|
|
@state() private hasFocus = false;
|
2024-06-20 15:40:46 -04:00
|
|
|
@state() visuallyHiddenLabel = false;
|
2024-04-17 11:20:27 -04:00
|
|
|
@state() invalid = false;
|
|
|
|
|
@property() title = ''; // make reactive to pass through
|
|
|
|
|
|
|
|
|
|
/** The button's theme variant. */
|
2024-06-12 20:13:05 -04:00
|
|
|
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral';
|
|
|
|
|
|
|
|
|
|
/** The button's visual appearance. */
|
2024-06-20 16:40:13 -04:00
|
|
|
@property({ reflect: true }) appearance: 'filled' | 'tinted' | 'outlined' | 'text' = 'filled';
|
2024-04-17 11:20:27 -04:00
|
|
|
|
|
|
|
|
/** 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;
|
|
|
|
|
|
2024-06-17 16:17:09 -04:00
|
|
|
/** Disables the button. Does not apply to link buttons. */
|
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 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.
|
|
|
|
|
*/
|
2024-05-23 16:16:45 -04:00
|
|
|
@property({ reflect: true }) name: string | null = null;
|
2024-04-17 11:20:27 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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-09-11 10:25:42 -04:00
|
|
|
@property({ reflect: true }) value: string | null = null;
|
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';
|
|
|
|
|
|
2024-11-01 11:56:33 -04:00
|
|
|
/** When using `href`, this attribute will map to the underlying link's `rel` attribute. */
|
|
|
|
|
@property() rel?: string;
|
2024-04-17 11:20:27 -04:00
|
|
|
|
|
|
|
|
/** 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;
|
2024-05-31 14:28:19 -04:00
|
|
|
this.dispatchEvent(new WaBlurEvent());
|
2024-04-17 11:20:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleFocus() {
|
|
|
|
|
this.hasFocus = true;
|
2024-05-31 14:28:19 -04:00
|
|
|
this.dispatchEvent(new WaFocusEvent());
|
2024-04-17 11:20:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-04-17 11:20:27 -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';
|
2024-05-23 16:16:45 -04:00
|
|
|
if (this.name) {
|
|
|
|
|
button.name = this.name;
|
2024-04-17 11:20:27 -04:00
|
|
|
}
|
2024-09-11 10:25:42 -04:00
|
|
|
button.value = this.value || '';
|
2024-05-08 14:24:11 -04:00
|
|
|
|
|
|
|
|
['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-04-17 11:20:27 -04:00
|
|
|
}
|
|
|
|
|
|
2024-05-08 14:24:11 -04:00
|
|
|
private handleInvalid() {
|
2024-05-31 14:28:19 -04:00
|
|
|
this.dispatchEvent(new WaInvalidEvent());
|
2024-04-17 11:20:27 -04:00
|
|
|
}
|
|
|
|
|
|
2024-06-20 15:40:46 -04:00
|
|
|
private handleLabelSlotChange(event: Event) {
|
|
|
|
|
// If the only thing slotted in is a visually hidden element, we consider it a visually hidden label and apply a
|
|
|
|
|
// class so we can adjust styles accordingly.
|
|
|
|
|
const elements = (event.target as HTMLSlotElement).assignedElements({ flatten: true });
|
|
|
|
|
if (elements.length === 1 && elements[0].localName === 'wa-visually-hidden') {
|
|
|
|
|
this.visuallyHiddenLabel = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-23 16:16:45 -04:00
|
|
|
setValue(..._args: Parameters<WebAwesomeFormAssociatedElement['setValue']>) {
|
2024-06-20 15:40:46 -04:00
|
|
|
// This is just a stub. We don't ever actually want to set a value on the form. That happens when the button is clicked and added
|
2024-05-08 14:24:11 -04:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-17 02:02:30 -05:00
|
|
|
getBoundingClientRect(): DOMRect {
|
|
|
|
|
let rect = super.getBoundingClientRect();
|
|
|
|
|
let buttonRect = this.button.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
if (rect.width === 0 && buttonRect.width > 0) {
|
|
|
|
|
return buttonRect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rect;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 11:20:27 -04:00
|
|
|
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,
|
2024-12-17 02:02:30 -05:00
|
|
|
'wa-button': true,
|
|
|
|
|
caret: this.caret,
|
|
|
|
|
disabled: this.disabled,
|
|
|
|
|
focused: this.hasFocus,
|
|
|
|
|
loading: this.loading,
|
|
|
|
|
rtl: this.localize.dir() === 'rtl',
|
|
|
|
|
'visually-hidden-label': this.visuallyHiddenLabel,
|
2024-04-17 11:20:27 -04:00
|
|
|
})}
|
|
|
|
|
?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)}
|
2024-11-01 11:56:33 -04:00
|
|
|
rel=${ifDefined(isLink && this.rel ? this.rel : undefined)}
|
2024-04-17 11:20:27 -04:00
|
|
|
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}
|
|
|
|
|
>
|
2024-12-17 02:02:30 -05:00
|
|
|
<slot name="prefix" part="prefix" class="prefix"></slot>
|
|
|
|
|
<slot part="label" class="label" @slotchange=${this.handleLabelSlotChange}></slot>
|
|
|
|
|
<slot name="suffix" part="suffix" class="suffix"></slot>
|
2024-04-17 11:20:27 -04:00
|
|
|
${
|
|
|
|
|
this.caret
|
|
|
|
|
? html`
|
2024-12-17 02:02:30 -05:00
|
|
|
<wa-icon part="caret" class="caret" library="system" name="chevron-down" variant="solid"></wa-icon>
|
2024-04-17 11:20:27 -04:00
|
|
|
`
|
|
|
|
|
: ''
|
|
|
|
|
}
|
|
|
|
|
${this.loading ? html`<wa-spinner part="spinner"></wa-spinner>` : ''}
|
|
|
|
|
</${tag}>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-11 10:09:44 -07:00
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
2023-09-08 13:45:49 -04:00
|
|
|
'wa-button': WaButton;
|
2023-08-11 10:09:44 -07:00
|
|
|
}
|
|
|
|
|
}
|