-
Notifications
You must be signed in to change notification settings - Fork 2.8k
/
Copy pathbutton.base.ts
421 lines (372 loc) · 11.3 KB
/
button.base.ts
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element';
import { keyEnter, keySpace } from '@microsoft/fast-web-utilities';
import { ButtonFormTarget, ButtonType } from './button.options.js';
/**
* A Button Custom HTML Element.
* Based largely on the {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button | `<button>`} element.
*
* @slot start - Content which can be provided before the button content
* @slot end - Content which can be provided after the button content
* @slot - The default slot for button content
*
* @csspart content - The button content container
*
*/
export class BaseButton extends FASTElement {
/**
* Indicates the button should be focused when the page is loaded.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#autofocus | `autofocus`} attribute
*
* @public
* @remarks
* HTML Attribute: `autofocus`
*/
@attr({ mode: 'boolean' })
public autofocus!: boolean;
/**
* Default slotted content.
*
* @public
*/
@observable
public defaultSlottedContent!: HTMLElement[];
/**
* Sets the element's disabled state.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#disabled | `disabled`} attribute
*
* @public
* @remarks
* HTML Attribute: `disabled`
*/
@attr({ mode: 'boolean' })
disabled?: boolean;
/**
* Indicates that the button is focusable while disabled.
*
* @public
* @remarks
* HTML Attribute: `disabled-focusable`
*/
@attr({ attribute: 'disabled-focusable', mode: 'boolean' })
public disabledFocusable: boolean = false;
/**
* Sets that the button tabindex attribute
*
* @public
* @remarks
* HTML Attribute: `tabindex`
*/
@attr({ attribute: 'tabindex', mode: 'fromView', converter: nullableNumberConverter })
public override tabIndex: number = 0;
/**
* Sets the element's internal disabled state when the element is focusable while disabled.
*
* @param previous - the previous disabledFocusable value
* @param next - the current disabledFocusable value
* @internal
*/
public disabledFocusableChanged(previous: boolean, next: boolean): void {
if (this.$fastController.isConnected) {
this.elementInternals.ariaDisabled = `${!!next}`;
}
}
/**
* The internal {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
*
* @internal
*/
public elementInternals: ElementInternals = this.attachInternals();
/**
* The associated form element.
*
* @public
*/
public get form(): HTMLFormElement | null {
return this.elementInternals.form;
}
/**
* The URL that processes the form submission.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formaction | `formaction`} attribute
*
* @public
* @remarks
* HTML Attribute: `formaction`
*/
@attr({ attribute: 'formaction' })
public formAction?: string;
/**
* The form-associated flag.
* @see {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example | Form-associated custom elements}
*
* @public
*/
static readonly formAssociated = true;
/**
* The id of a form to associate the element to.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#form | `form`} attribute
*
* @public
* @remarks
* HTML Attribute: `form`
*/
@attr({ attribute: 'form' })
public formAttribute?: string;
/**
* The encoding type for the form submission.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formenctype | `formenctype`} attribute
*
* @public
* @remarks
* HTML Attribute: `formenctype`
*/
@attr({ attribute: 'formenctype' })
public formEnctype?: string;
/**
* The HTTP method that the browser uses to submit the form.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formmethod | `formmethod`} attribute
*
* @public
* @remarks
* HTML Attribute: `formmethod`
*/
@attr({ attribute: 'formmethod' })
public formMethod?: string;
/**
* Indicates that the form will not be validated when submitted.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formnovalidate | `formnovalidate`} attribute
*
* @public
* @remarks
* HTML Attribute: `formnovalidate`
*/
@attr({ attribute: 'formnovalidate', mode: 'boolean' })
public formNoValidate?: boolean;
/**
* The internal form submission fallback control.
*
* @internal
*/
private formSubmissionFallbackControl?: HTMLButtonElement;
/**
* The internal slot for the form submission fallback control.
*
* @internal
*/
private formSubmissionFallbackControlSlot?: HTMLSlotElement;
/**
* The target frame or window to open the form submission in.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formtarget | `formtarget`} attribute
*
* @public
* @remarks
* HTML Attribute: `formtarget`
*/
@attr({ attribute: 'formtarget' })
public formTarget?: ButtonFormTarget;
/**
* A reference to all associated label elements.
*
* @public
*/
public get labels(): ReadonlyArray<Node> {
return Object.freeze(Array.from(this.elementInternals.labels));
}
/**
* The name of the element. This element's value will be surfaced during form submission under the provided name.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#name | `name`} attribute
*
* @public
* @remarks
* HTML Attribute: `name`
*/
@attr
public name?: string;
/**
* The button type.
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#type | `type`} attribute
*
* @public
* @remarks
* HTML Attribute: `type`
*/
@attr
public type!: ButtonType;
/**
* Removes the form submission fallback control when the type changes.
*
* @param previous - the previous type value
* @param next - the new type value
* @internal
*/
public typeChanged(previous: ButtonType, next: ButtonType): void {
if (next !== ButtonType.submit) {
this.formSubmissionFallbackControl?.remove();
this.shadowRoot?.querySelector('slot[name="internal"]')?.remove();
}
}
/**
* The value attribute.
*
* @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#value | `value`} attribute
*
* @public
* @remarks
* HTML Attribute: `value`
*/
@attr
public value?: string;
/**
* Handles the button click event.
*
* @param e - The event object
* @internal
*/
public clickHandler(e: Event): boolean | void {
if (e && this.disabledFocusable) {
e.stopImmediatePropagation();
return;
}
this.press();
return true;
}
connectedCallback(): void {
super.connectedCallback();
this.elementInternals.ariaDisabled = `${!!this.disabledFocusable}`;
}
constructor() {
super();
this.elementInternals.role = 'button';
}
/**
* This fallback creates a new slot, then creates a submit button to mirror the custom element's
* properties. The submit button is then appended to the slot and the form is submitted.
*
* @internal
* @privateRemarks
* This is a workaround until {@link https://github.com/WICG/webcomponents/issues/814 | WICG/webcomponents/issues/814} is resolved.
*/
private createAndInsertFormSubmissionFallbackControl(): void {
const internalSlot = this.formSubmissionFallbackControlSlot ?? document.createElement('slot');
internalSlot.setAttribute('name', 'internal');
this.shadowRoot?.appendChild(internalSlot);
this.formSubmissionFallbackControlSlot = internalSlot;
const fallbackControl = this.formSubmissionFallbackControl ?? document.createElement('button');
fallbackControl.style.display = 'none';
fallbackControl.setAttribute('type', 'submit');
fallbackControl.setAttribute('slot', 'internal');
if (this.formNoValidate) {
fallbackControl.toggleAttribute('formnovalidate', true);
}
if (this.elementInternals.form?.id) {
fallbackControl.setAttribute('form', this.elementInternals.form.id);
}
if (this.name) {
fallbackControl.setAttribute('name', this.name);
}
if (this.value) {
fallbackControl.setAttribute('value', this.value);
}
if (this.formAction) {
fallbackControl.setAttribute('formaction', this.formAction ?? '');
}
if (this.formEnctype) {
fallbackControl.setAttribute('formenctype', this.formEnctype ?? '');
}
if (this.formMethod) {
fallbackControl.setAttribute('formmethod', this.formMethod ?? '');
}
if (this.formTarget) {
fallbackControl.setAttribute('formtarget', this.formTarget ?? '');
}
this.append(fallbackControl);
this.formSubmissionFallbackControl = fallbackControl;
}
/**
* Invoked when a connected component's form or fieldset has its disabled state changed.
*
* @param disabled - the disabled value of the form / fieldset
*
* @internal
*/
public formDisabledCallback(disabled: boolean): void {
this.disabled = disabled;
}
/**
* Handles keypress events for the button.
*
* @param e - the keyboard event
* @returns - the return value of the click handler
* @public
*/
public keypressHandler(e: KeyboardEvent): boolean | void {
if (e && this.disabledFocusable) {
e.stopImmediatePropagation();
return;
}
if (e.key === keyEnter || e.key === keySpace) {
this.click();
return;
}
return true;
}
/**
* Presses the button.
*
* @public
*/
protected press(): void {
switch (this.type) {
case ButtonType.reset: {
this.resetForm();
break;
}
case ButtonType.submit: {
this.submitForm();
break;
}
}
}
/**
* Resets the associated form.
*
* @public
*/
public resetForm(): void {
this.elementInternals.form?.reset();
}
/**
* Submits the associated form.
*
* @internal
*/
private submitForm(): void {
if (!this.elementInternals.form || this.disabled || this.type !== ButtonType.submit) {
return;
}
// workaround: if the button doesn't have any form overrides, the form can be submitted directly.
if (
!this.name &&
!this.formAction &&
!this.formEnctype &&
!this.form &&
!this.formMethod &&
!this.formNoValidate &&
!this.formTarget
) {
this.elementInternals.form.requestSubmit();
return;
}
try {
this.elementInternals.setFormValue(this.value ?? '');
this.elementInternals.form.requestSubmit(this);
} catch (e) {
// `requestSubmit` throws an error since custom elements may not be able to submit the form.
// This fallback creates a new slot, then creates a submit button to mirror the custom element's
// properties. The submit button is then appended to the slot and the form is submitted.
this.createAndInsertFormSubmissionFallbackControl();
// workaround: the form value is reset since the fallback control will handle the form submission.
this.elementInternals.setFormValue(null);
this.elementInternals.form.requestSubmit(this.formSubmissionFallbackControl);
}
}
}