-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathbutton.ts
More file actions
197 lines (171 loc) · 6.01 KB
/
button.ts
File metadata and controls
197 lines (171 loc) · 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../focus/md-focus-ring.js';
import '../../ripple/ripple.js';
import {html, isServer, LitElement, nothing} from 'lit';
import {property, query, queryAssignedElements} from 'lit/decorators.js';
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {mixinDelegatesAria} from '../../internal/aria/delegate.js';
import {
dispatchActivationClick,
isActivationClick,
} from '../../internal/events/form-label-activation.js';
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
import {mixinFormAssociated} from '../../labs/behaviors/form-associated.js';
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';
// Separate variable needed for closure.
const buttonBaseClass = mixinDelegatesAria(
mixinFormSubmitter(mixinFormAssociated(mixinElementInternals(LitElement))),
);
/**
* A button component.
*/
export abstract class Button extends buttonBaseClass {
/** @nocollapse */
static override shadowRootOptions: ShadowRootInit = {
mode: 'open',
delegatesFocus: true,
};
/**
* Whether or not the button is disabled.
*/
declare disabled: boolean; // for jsdoc until lit-analyzer is updated
/**
* Whether or not the button is "soft-disabled" (disabled but still
* focusable).
*
* Use this when a button needs increased visibility when disabled. See
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls
* for more guidance on when this is needed.
*/
@property({type: Boolean, attribute: 'soft-disabled', reflect: true})
softDisabled = false;
/**
* The URL that the link button points to.
*/
@property() href = '';
/**
* The filename to use when downloading the linked resource.
* If not specified, the browser will determine a filename.
* This is only applicable when the button is used as a link (`href` is set).
*/
@property() download = '';
/**
* Where to display the linked `href` URL for a link button. Common options
* include `_blank` to open in a new tab.
*/
@property() target: '_blank' | '_parent' | '_self' | '_top' | '' = '';
/**
* 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: 'trailing-icon', reflect: true})
trailingIcon = false;
/**
* Whether to display the icon or not.
*/
@property({type: Boolean, attribute: 'has-icon', reflect: true}) hasIcon =
false;
@query('.button') private readonly buttonElement!: HTMLElement | null;
@queryAssignedElements({slot: 'icon', flatten: true})
private readonly assignedIcons!: HTMLElement[];
constructor() {
super();
if (!isServer) {
this.addEventListener('click', this.handleClick.bind(this));
}
}
override focus() {
this.buttonElement?.focus();
}
override blur() {
this.buttonElement?.blur();
}
protected override render() {
const isRippleDisabled = this.disabled || this.softDisabled;
const buttonOrLink = this.href ? this.renderLink() : this.renderButton();
// TODO(b/310046938): due to a limitation in focus ring/ripple, we can't use
// the same ID for different elements, so we change the ID instead.
const buttonId = this.href ? 'link' : 'button';
return html`
${this.renderElevationOrOutline?.()}
<div class="background"></div>
<md-focus-ring part="focus-ring" for=${buttonId}></md-focus-ring>
<md-ripple
part="ripple"
for=${buttonId}
?disabled="${isRippleDisabled}"></md-ripple>
${buttonOrLink}
`;
}
// Buttons can override this to add elevation or an outline. Use this and
// return `<md-elevation>` (for elevated, filled, and tonal buttons)
// or `<div class="outline">` (for outlined buttons).
// Text buttons that have neither do not need to implement this.
protected renderElevationOrOutline?(): unknown;
private renderButton() {
// Needed for closure conformance
const {ariaLabel, ariaHasPopup, ariaExpanded} = this as ARIAMixinStrict;
return html`<button
id="button"
class="button"
?disabled=${this.disabled}
aria-disabled=${this.softDisabled || nothing}
aria-label="${ariaLabel || nothing}"
aria-haspopup="${ariaHasPopup || nothing}"
aria-expanded="${ariaExpanded || nothing}">
${this.renderContent()}
</button>`;
}
private renderLink() {
// Needed for closure conformance
const {ariaLabel, ariaHasPopup, ariaExpanded} = this as ARIAMixinStrict;
return html`<a
id="link"
class="button"
aria-label="${ariaLabel || nothing}"
aria-haspopup="${ariaHasPopup || nothing}"
aria-expanded="${ariaExpanded || nothing}"
aria-disabled=${this.disabled || this.softDisabled || nothing}
tabindex="${this.disabled && !this.softDisabled ? -1 : nothing}"
href=${this.href}
download=${this.download || nothing}
target=${this.target || nothing}
>${this.renderContent()}
</a>`;
}
private renderContent() {
const icon = html`<slot
name="icon"
@slotchange="${this.handleSlotChange}"></slot>`;
return html`
<span class="touch"></span>
${this.trailingIcon ? nothing : icon}
<span class="label"><slot></slot></span>
${this.trailingIcon ? icon : nothing}
`;
}
private handleClick(event: MouseEvent) {
// If the button is soft-disabled or a disabled link, we need to explicitly
// prevent the click from propagating to other event listeners as well as
// prevent the default action.
if (this.softDisabled || (this.disabled && this.href)) {
event.stopImmediatePropagation();
event.preventDefault();
return;
}
if (!isActivationClick(event) || !this.buttonElement) {
return;
}
this.focus();
dispatchActivationClick(this.buttonElement);
}
private handleSlotChange() {
this.hasIcon = this.assignedIcons.length > 0;
}
}