material-web/button/lib/button.ts
Daniel Freedman 24298e696c feat(button): add label slot
PiperOrigin-RevId: 515467458
2023-03-09 16:03:11 -08:00

207 lines
5.8 KiB
TypeScript

/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// This is required for @ariaProperty
// tslint:disable:no-new-decorators
import '../../focus/focus-ring.js';
import '../../ripple/ripple.js';
import {html, LitElement, nothing, TemplateResult} from 'lit';
import {property, query, queryAssignedElements, queryAsync, state} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
import {dispatchActivationClick, isActivationClick} from '../../controller/events.js';
import {ariaProperty} from '../../decorators/aria-property.js';
import {pointerPress, shouldShowStrongFocus} from '../../focus/strong-focus.js';
import {ripple} from '../../ripple/directive.js';
import {MdRipple} from '../../ripple/ripple.js';
import {ARIAHasPopup} from '../../types/aria.js';
import {ButtonState} from './state.js';
// tslint:disable-next-line:enforce-comments-on-exported-symbols
export abstract class Button extends LitElement implements ButtonState {
static override shadowRootOptions:
ShadowRootInit = {mode: 'open', delegatesFocus: true};
@property({type: String, attribute: 'data-aria-has-popup', noAccessor: true})
@ariaProperty
override ariaHasPopup!: ARIAHasPopup;
@property({type: String, attribute: 'data-aria-label', noAccessor: true})
@ariaProperty
override ariaLabel!: string;
/**
* Whether or not the button is disabled.
*/
@property({type: Boolean, reflect: true}) disabled = false;
/**
* Whether to render the icon at the inline end of the label rather than the
* inline start.
*
* _Note:_ Link buttons cannot have trailing icons.
*/
@property({type: Boolean, attribute: 'trailingicon'}) trailingIcon = false;
// TODO(b/272598771): remove label property
/**
* The button's visible label.
*
* @deprecated Set text as content of the button instead.
*/
@property({type: String}) label = '';
/**
* Whether to display the icon or not.
*/
@property({type: Boolean}) hasIcon = false;
/**
* Whether `preventDefault()` should be called on the underlying button.
* Useful for preventing certain native functionalities like preventing form
* submissions.
*/
@property({type: Boolean}) preventClickDefault = false;
@query('.md3-button') protected buttonElement!: HTMLElement;
@queryAsync('md-ripple') protected ripple!: Promise<MdRipple|null>;
@state() protected showFocusRing = false;
@state() protected showRipple = false;
@queryAssignedElements({slot: 'icon', flatten: true})
protected assignedIcons!: HTMLElement[];
constructor() {
super();
this.addEventListener('click', this.handleActivationClick);
}
private readonly handleActivationClick = (event: MouseEvent) => {
if (!isActivationClick((event))) {
return;
}
this.focus();
dispatchActivationClick(this.buttonElement);
};
override focus() {
this.buttonElement.focus();
}
override blur() {
this.buttonElement.blur();
}
protected readonly getRipple = () => {
this.showRipple = true;
return this.ripple;
};
protected override render(): TemplateResult {
// TODO(b/237283903): Replace ifDefined(... || undefined) with ifTruthy(...)
return html`
<button
class="md3-button ${classMap(this.getRenderClasses())}"
?disabled="${this.disabled}"
aria-label="${this.ariaLabel || nothing}"
aria-haspopup="${this.ariaHasPopup || nothing}"
@pointerdown="${this.handlePointerDown}"
@focus="${this.handleFocus}"
@blur="${this.handleBlur}"
@click="${this.handleClick}"
${ripple(this.getRipple)}>
${this.renderFocusRing()}
${this.renderElevation()}
${when(this.showRipple, this.renderRipple)}
${this.renderOutline()}
${this.renderTouchTarget()}
${this.renderLeadingIcon()}
${this.renderLabel()}
${this.renderTrailingIcon()}
</button>`;
}
protected getRenderClasses(): ClassInfo {
return {
'md3-button--icon-leading': !this.trailingIcon && this.hasIcon,
'md3-button--icon-trailing': this.trailingIcon && this.hasIcon,
};
}
protected renderTouchTarget(): TemplateResult {
return html`
<span class="md3-button__touch"></span>
`;
}
protected renderElevation(): TemplateResult {
return html``;
}
protected renderRipple = () => {
return html`<md-ripple class="md3-button__ripple" ?disabled="${
this.disabled}"></md-ripple>`;
};
protected renderOutline(): TemplateResult {
return html``;
}
protected renderFocusRing(): TemplateResult {
return html`<md-focus-ring .visible="${
this.showFocusRing}"></md-focus-ring>`;
}
protected renderLabel(): TemplateResult {
// TODO(b/272598771): remove the ternary when label property is removed
return html`<span class="md3-button__label">${
this.label ? this.label : html`<slot></slot>`}</span>`;
}
protected renderLeadingIcon(): TemplateResult|string {
return this.trailingIcon ? '' : this.renderIcon();
}
protected renderTrailingIcon(): TemplateResult|string {
return this.trailingIcon ? this.renderIcon() : '';
}
protected renderIcon(): TemplateResult {
return html`<slot name="icon" @slotchange="${
this.handleSlotChange}"></slot>`;
}
protected handlePointerDown(e: PointerEvent) {
pointerPress();
this.showFocusRing = shouldShowStrongFocus();
}
protected handleClick(e: MouseEvent) {
if (this.preventClickDefault) {
e.preventDefault();
}
}
protected handleFocus() {
this.showFocusRing = shouldShowStrongFocus();
}
protected handleBlur() {
this.showFocusRing = false;
}
protected handleSlotChange() {
this.hasIcon = this.assignedIcons.length > 0;
}
}