@aurelia-mdc-web/all 9.3.0-au2 → 9.3.1-au2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/chips/mdc-chip/mdc-chip.js.map +1 -1
  2. package/dist/data-table/mdc-data-table.js.map +1 -1
  3. package/dist/dialog/mdc-dialog-service.js.map +1 -1
  4. package/dist/expandable/mdc-expandable.js.map +1 -1
  5. package/dist/form-field/mdc-form-field.js.map +1 -1
  6. package/dist/list/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.js.map +1 -1
  7. package/dist/list/mdc-list-item/mdc-list-item.js.map +1 -1
  8. package/dist/menu/mdc-menu.js.map +1 -1
  9. package/dist/segmented-button/mdc-segmented-button-segment/mdc-segmented-button-segment.js.map +1 -1
  10. package/dist/select/mdc-select-value-observer.js.map +1 -1
  11. package/dist/select/mdc-select.js.map +1 -1
  12. package/dist/text-field/mdc-text-field.js.map +1 -1
  13. package/dist/tree-view/mdc-tree-view.js.map +1 -1
  14. package/package.json +2 -2
  15. package/src/chips/mdc-chip/mdc-chip.ts +290 -290
  16. package/src/data-table/mdc-data-table.ts +432 -432
  17. package/src/dialog/mdc-dialog-service.ts +80 -80
  18. package/src/expandable/mdc-expandable.ts +104 -104
  19. package/src/form-field/mdc-form-field.ts +60 -60
  20. package/src/list/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.ts +108 -108
  21. package/src/list/mdc-list-item/mdc-list-item.ts +136 -136
  22. package/src/menu/mdc-menu.ts +340 -340
  23. package/src/segmented-button/mdc-segmented-button-segment/mdc-segmented-button-segment.ts +170 -170
  24. package/src/select/mdc-select-value-observer.ts +346 -346
  25. package/src/select/mdc-select.ts +480 -480
  26. package/src/text-field/mdc-text-field.ts +535 -535
  27. package/src/tree-view/mdc-tree-view.ts +147 -147
@@ -1,480 +1,480 @@
1
- import { MdcComponent, IValidatedElement, IError, booleanAttr } from '../base';
2
- import { cssClasses, MDCSelectFoundationMap, MDCSelectEventDetail, strings } from '@material/select';
3
- import { inject, customElement, INode, IPlatform, bindable } from 'aurelia';
4
- import { MdcSelectIcon, IMdcSelectIconElement, mdcIconStrings } from './mdc-select-icon';
5
- import { MdcSelectHelperText, mdcHelperTextCssClasses } from './mdc-select-helper-text/mdc-select-helper-text';
6
- import { MDCNotchedOutline } from '@material/notched-outline';
7
- import { MDCMenuItemEvent, Corner } from '@material/menu';
8
- import { MDCSelectFoundationAurelia } from './mdc-select-foundation-aurelia';
9
- import { MDCSelectAdapterAurelia } from './mdc-select-adapter-aurelia';
10
- import { MDCMenuDistance } from '@material/menu-surface';
11
- import { processContent, BindingMode, CustomElement, CustomAttribute } from '@aurelia/runtime-html';
12
- import { MdcDefaultSelectConfiguration } from './mdc-default-select-configuration';
13
- import template from './mdc-select.html?raw';
14
- import { MdcFloatingLabel } from '../floating-label/mdc-floating-label';
15
- import { MdcLineRipple } from '../line-ripple/mdc-line-ripple';
16
- import { MdcListItem } from '../list/mdc-list-item/mdc-list-item';
17
- import { MdcMenu } from '../menu/mdc-menu';
18
-
19
- strings.CHANGE_EVENT = strings.CHANGE_EVENT.toLowerCase();
20
-
21
- let selectId = 0;
22
-
23
- /**
24
- * @selector mdc-select
25
- * @emits mdcselect:change | Emitted if user changed the value
26
- */
27
- @inject(Element, IPlatform, MdcDefaultSelectConfiguration)
28
- @customElement({ name: 'mdc-select', template })
29
- @processContent(function processContent(node: INode, platform: IPlatform) {
30
- const el = node as Element;
31
-
32
- const leadingIcon = el.querySelector(`[${mdcIconStrings.ATTRIBUTE}]`);
33
- leadingIcon?.remove();
34
-
35
- const template = platform.document.createElement('template');
36
- template.setAttribute('au-slot', '');
37
- template.innerHTML = el.innerHTML;
38
- el.innerHTML = '';
39
- el.appendChild(template);
40
-
41
- // move icon to the slot - this allows omitting slot specification
42
- if (leadingIcon) {
43
- const div = platform.document.createElement('div');
44
- div.appendChild(leadingIcon);
45
- const iconTemplate = platform.document.createElement('template');
46
- iconTemplate.setAttribute('au-slot', 'leading-icon');
47
- iconTemplate.innerHTML = div.innerHTML;
48
- el.appendChild(iconTemplate);
49
- }
50
- }
51
- )
52
- export class MdcSelect extends MdcComponent<MDCSelectFoundationAurelia> {
53
- constructor(root: HTMLElement, private platform: IPlatform, private defaultConfiguration: MdcDefaultSelectConfiguration) {
54
- super(root);
55
- this.outlined = this.defaultConfiguration.outlined;
56
- defineMdcSelectElementApis(this.root);
57
- this.root.id = this.id;
58
- }
59
-
60
- id: string = `mdc-select-${++selectId}`;
61
- public menu: MdcMenu;
62
- private selectAnchor: HTMLElement;
63
- private selectedText: HTMLElement;
64
-
65
- private menuElement?: Element;
66
-
67
- get items(): MdcListItem[] | undefined {
68
- return this.menu.list_?.items;
69
- }
70
-
71
- private leadingIcon?: MdcSelectIcon;
72
-
73
- private helperText?: MdcSelectHelperText;
74
- private lineRipple?: MdcLineRipple;
75
- private mdcLabel: MdcFloatingLabel;
76
- private outline?: MDCNotchedOutline;
77
- errors = new Map<IError, boolean>();
78
-
79
- /** Sets the select label */
80
- @bindable()
81
- label: string;
82
- labelChanged() {
83
- this.platform.domQueue.queueTask(() => this.foundation?.layout());
84
- }
85
-
86
- /** Styles the select as an outlined select */
87
- @bindable({ set: booleanAttr })
88
- outlined?: boolean;
89
- outlinedChanged() {
90
- this.platform.domQueue.queueTask(() => this.foundation?.layout());
91
- }
92
-
93
- /** Makes the value required */
94
- @bindable({ set: booleanAttr })
95
- required: boolean;
96
- requiredChanged() {
97
- if (this.required) {
98
- this.selectAnchor?.setAttribute('aria-required', 'true');
99
- } else {
100
- this.selectAnchor?.removeAttribute('aria-required');
101
- }
102
- this.foundation?.setRequired(this.required ?? false);
103
- this.platform.domWriteQueue.queueTask(() => this.foundation?.layout());
104
- }
105
-
106
- /** Enables/disables the select */
107
- @bindable({ set: booleanAttr })
108
- disabled?: boolean;
109
- disabledChanged() {
110
- if (this.disabled !== undefined) {
111
- this.foundation?.setDisabled(this.disabled);
112
- }
113
- }
114
-
115
- /** Hoists the select DOM to document.body */
116
- @bindable({ set: booleanAttr, mode: BindingMode.oneTime })
117
- hoistToBody: boolean;
118
-
119
- /** Sets the select DOM position to fixed */
120
- @bindable({ set: booleanAttr, mode: BindingMode.oneTime })
121
- fixed: boolean;
122
-
123
- /** Sets the margin between the select input and the dropdown */
124
- @bindable()
125
- anchorMargin: Partial<MDCMenuDistance>;
126
-
127
- /** Sets the select dropdown width to match content */
128
- @bindable({ set: booleanAttr })
129
- naturalWidth: boolean;
130
-
131
- private _value: unknown;
132
- get value(): unknown {
133
- if (this.foundation) {
134
- return this.foundation.getValue();
135
- } else {
136
- return this._value;
137
- }
138
- }
139
-
140
- set value(value: unknown) {
141
- this.setValue(value);
142
- }
143
-
144
- setValue(value: unknown, skipNotify: boolean = false) {
145
- this._value = value;
146
- if (this.foundation) {
147
- this.foundation.setValue(value, skipNotify);
148
- this.foundation.layout();
149
- }
150
- }
151
-
152
- get valid(): boolean {
153
- return this.foundation?.isValid() ?? true;
154
- }
155
-
156
- set valid(value: boolean) {
157
- this.foundation?.setValid(value);
158
- }
159
-
160
- get selectedIndex(): number {
161
- return this.foundation!.getSelectedIndex();
162
- }
163
-
164
- set selectedIndex(selectedIndex: number) {
165
- this.foundation?.setSelectedIndex(selectedIndex, /** closeMenu */ true);
166
- }
167
-
168
- addError(error: IError) {
169
- this.errors.set(error, true);
170
- this.valid = false;
171
- }
172
-
173
- removeError(error: IError) {
174
- this.errors.delete(error);
175
- this.valid = this.errors.size === 0;
176
- }
177
-
178
- renderErrors() {
179
- if (this.helperText) {
180
- this.helperText.errors = Array.from(this.errors.keys()).filter(x => x.message !== null).map(x => x.message!);
181
- }
182
- }
183
-
184
- async attaching() {
185
- const nextSibling = this.root.nextElementSibling;
186
- if (nextSibling?.tagName === mdcHelperTextCssClasses.ROOT.toUpperCase()) {
187
- this.helperText = CustomElement.for<MdcSelectHelperText>(nextSibling).viewModel;
188
- await this.helperText.initialised;
189
- }
190
- }
191
-
192
- beforeFoundationCreated() {
193
- const leadingIconEl = this.root.querySelector<IMdcSelectIconElement>(`${strings.LEADING_ICON_SELECTOR}`);
194
- if (leadingIconEl) {
195
- this.leadingIcon = CustomAttribute.for<MdcSelectIcon>(leadingIconEl, mdcIconStrings.ATTRIBUTE)?.viewModel;
196
- }
197
- this.menu.list_!.singleSelection = true;
198
- }
199
-
200
- initialSyncWithDOM() {
201
- // set initial value without emitting change events
202
- this.foundation?.setValue(this._value, true);
203
- this.foundation?.layout();
204
- this.errors = new Map<IError, boolean>();
205
- this.valid = true;
206
-
207
- this.labelChanged();
208
- this.disabledChanged();
209
- this.outlinedChanged();
210
- this.requiredChanged();
211
- }
212
-
213
- getDefaultFoundation() {
214
- // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
215
- // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
216
- const adapter: MDCSelectAdapterAurelia = {
217
- ...this.getSelectAdapterMethods(),
218
- ...this.getCommonAdapterMethods(),
219
- ...this.getOutlineAdapterMethods(),
220
- ...this.getLabelAdapterMethods(),
221
- };
222
- return new MDCSelectFoundationAurelia(adapter, this.getFoundationMap());
223
- }
224
-
225
- private getSelectAdapterMethods() {
226
- return {
227
- setSelectedText: (text: string) => {
228
- this.selectedText.textContent = text;
229
- },
230
- isSelectAnchorFocused: () => document.activeElement === this.selectAnchor,
231
- getSelectAnchorAttr: (attr: string) => this.selectAnchor.getAttribute(attr),
232
- setSelectAnchorAttr: (attr: string, value: string) => {
233
- this.selectAnchor.setAttribute(attr, value);
234
- },
235
- removeSelectAnchorAttr: (attr: string) => {
236
- this.selectAnchor.removeAttribute(attr);
237
- },
238
- addMenuClass: (className: string) => {
239
- this.menuElement?.classList.add(className);
240
- },
241
- removeMenuClass: (className: string) => {
242
- this.menuElement?.classList.remove(className);
243
- },
244
- openMenu: () => {
245
- this.menu.open = true;
246
- this.menu.root.style.minWidth = this.menu.root.style.maxWidth = (this.hoistToBody || this.fixed) && !this.naturalWidth
247
- ? `${this.root.clientWidth}px`
248
- : '';
249
- },
250
- closeMenu: () => { this.menu.open = false; },
251
- getAnchorElement: () => this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR)!,
252
- setMenuAnchorElement: (anchorEl: HTMLElement) => {
253
- this.menu.anchor = anchorEl;
254
- },
255
- setMenuAnchorCorner: (anchorCorner: Corner) => {
256
- this.menu.setAnchorCorner(anchorCorner);
257
- },
258
- setMenuWrapFocus: (wrapFocus: boolean) => {
259
- this.menu.wrapFocus = wrapFocus;
260
- },
261
- getSelectedIndex: () => {
262
- const index = this.menu.selectedIndex;
263
- return index instanceof Array ? index[0] : index;
264
- },
265
- setSelectedIndex: (index: number) => {
266
- this.menu.selectedIndex = index;
267
- },
268
- removeAttributeAtIndex: (index: number, attributeName: string) => {
269
- this.menu.items[index].removeAttribute(attributeName);
270
- },
271
- focusMenuItemAtIndex: (index: number) => {
272
- (this.menu.items[index] as HTMLElement).focus();
273
- },
274
- getMenuItemCount: () => this.menu.items.length,
275
- getMenuItemValues: () => this.menu.items.map(x => CustomElement.for<MdcListItem>(x).viewModel.value),
276
- getMenuItemTextAtIndex: (index: number) => this.menu.getPrimaryTextAtIndex(index),
277
- isTypeaheadInProgress: () => this.menu.typeaheadInProgress,
278
- typeaheadMatchItem: (nextChar: string, startingIndex: number) => this.menu.typeaheadMatchItem(nextChar, startingIndex),
279
- };
280
- }
281
-
282
- private getCommonAdapterMethods() {
283
- return {
284
- addClass: (className: string) => {
285
- this.root.classList.add(className);
286
- },
287
- removeClass: (className: string) => {
288
- this.root.classList.remove(className);
289
- },
290
- hasClass: (className: string) => this.root.classList.contains(className),
291
- setRippleCenter: (normalizedX: number) => this.lineRipple?.setRippleCenter(normalizedX),
292
- activateBottomLine: () => this.lineRipple?.activate(),
293
- deactivateBottomLine: () => this.lineRipple?.deactivate(),
294
- notifyChange: (value: string) => {
295
- const index = this.selectedIndex;
296
- this.emit<MDCSelectEventDetail>(strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */);
297
- this.emit<MDCSelectEventDetail>('change', { value, index }, true /* shouldBubble */);
298
- },
299
- };
300
- }
301
-
302
- private getOutlineAdapterMethods() {
303
- return {
304
- hasOutline: () => Boolean(this.outline),
305
- notchOutline: (labelWidth: number) => this.outline?.notch(labelWidth),
306
- closeOutline: () => this.outline?.closeNotch(),
307
- };
308
- }
309
-
310
- private getLabelAdapterMethods() {
311
- return {
312
- hasLabel: () => !!this.mdcLabel,
313
- floatLabel: (shouldFloat: boolean) => this.mdcLabel?.float(shouldFloat),
314
- getLabelWidth: () => this.mdcLabel ? this.mdcLabel.getWidth() : 0,
315
- setLabelRequired: (isRequired: boolean) => this.mdcLabel?.setRequired(isRequired),
316
- };
317
- }
318
-
319
- handleChange() {
320
- this.foundation?.handleChange();
321
- this.emit('change', {}, true);
322
- }
323
-
324
- handleFocus() {
325
- this.foundation?.handleFocus();
326
- }
327
-
328
- handleBlur() {
329
- this.foundation?.handleBlur();
330
- // if class is set it means the menu is open,
331
- // do not emit blur since "conceptually" the element is still active
332
- if (!this.root.classList.contains(cssClasses.FOCUSED)) {
333
- this.emit('blur', {}, true);
334
- }
335
- }
336
-
337
- handleClick(evt: MouseEvent) {
338
- this.selectAnchor.focus();
339
- this.foundation?.handleClick(this.getNormalizedXCoordinate(evt));
340
- }
341
-
342
- handleKeydown(evt: KeyboardEvent) {
343
- this.foundation?.handleKeydown(evt);
344
- return true;
345
- }
346
-
347
- handleMenuItemAction(evt: MDCMenuItemEvent) {
348
- this.foundation?.handleMenuItemAction(evt.detail.index);
349
- }
350
-
351
- handleMenuOpened() {
352
- this.foundation?.handleMenuOpened();
353
- }
354
-
355
- handleMenuClosed() {
356
- this.foundation?.handleMenuClosed();
357
- if (!this.root.classList.contains(cssClasses.FOCUSED)) {
358
- this.emit('blur', {}, true);
359
- }
360
- }
361
-
362
- handleItemsChanged() {
363
- this.foundation?.layoutOptions();
364
- this.foundation?.layout();
365
- }
366
-
367
- focus() {
368
- this.selectAnchor.focus();
369
- }
370
-
371
- blur() {
372
- this.selectAnchor.blur();
373
- }
374
-
375
- /**
376
- * @hidden
377
- * Calculates where the line ripple should start based on the x coordinate within the component.
378
- */
379
- private getNormalizedXCoordinate(evt: MouseEvent | TouchEvent): number {
380
- const targetClientRect = (evt.target as Element).getBoundingClientRect();
381
- const xCoordinate =
382
- this.isTouchEvent(evt) ? evt.touches[0].clientX : evt.clientX;
383
- return xCoordinate - targetClientRect.left;
384
- }
385
-
386
- private isTouchEvent(evt: MouseEvent | TouchEvent): evt is TouchEvent {
387
- return Boolean((evt as TouchEvent).touches);
388
- }
389
-
390
- /**
391
- * @hidden
392
- * Returns a map of all subcomponents to subfoundations.
393
- */
394
- private getFoundationMap(): Partial<MDCSelectFoundationMap> {
395
- return {
396
- helperText: this.helperText?.foundation,
397
- leadingIcon: this.leadingIcon?.foundation
398
- };
399
- }
400
- }
401
-
402
- /** @hidden */
403
- export interface IMdcSelectElement extends IValidatedElement {
404
- $au: {
405
- 'au:resource:custom-element': {
406
- viewModel: MdcSelect;
407
- };
408
- };
409
- value: unknown;
410
- }
411
-
412
- function defineMdcSelectElementApis(element: HTMLElement) {
413
- Object.defineProperties(element, {
414
- value: {
415
- get(this: IMdcSelectElement) {
416
- return CustomElement.for<MdcSelect>(this).viewModel.value;
417
- },
418
- set(this: IMdcSelectElement, value: unknown) {
419
- // aurelia binding converts "undefined" and "null" into empty string
420
- // this does not translate well into "empty" menu items when several selects are bound to the same field
421
- CustomElement.for<MdcSelect>(this).viewModel.value = value === '' ? undefined : value;
422
- },
423
- configurable: true
424
- },
425
- options: {
426
- get(this: IMdcSelectElement) {
427
- return CustomElement.for<MdcSelect>(this).viewModel.root.querySelectorAll('.mdc-list-item');
428
- },
429
- configurable: true
430
- },
431
- selectedIndex: {
432
- get(this: IMdcSelectElement) {
433
- return CustomElement.for<MdcSelect>(this).viewModel.selectedIndex;
434
- },
435
- set(this: IMdcSelectElement, value: number) {
436
- CustomElement.for<MdcSelect>(this).viewModel.selectedIndex = value;
437
- },
438
- configurable: true
439
- },
440
- valid: {
441
- get(this: IMdcSelectElement) {
442
- return CustomElement.for<MdcSelect>(this).viewModel.valid;
443
- },
444
- set(this: IMdcSelectElement, value: boolean) {
445
- CustomElement.for<MdcSelect>(this).viewModel.valid = value;
446
- },
447
- configurable: true
448
- },
449
- addError: {
450
- value(this: IMdcSelectElement, error: IError) {
451
- CustomElement.for<MdcSelect>(this).viewModel.addError(error);
452
- },
453
- configurable: true
454
- },
455
- removeError: {
456
- value(this: IMdcSelectElement, error: IError) {
457
- CustomElement.for<MdcSelect>(this).viewModel.removeError(error);
458
- },
459
- configurable: true
460
- },
461
- renderErrors: {
462
- value(this: IMdcSelectElement): void {
463
- CustomElement.for<MdcSelect>(this).viewModel.renderErrors();
464
- },
465
- configurable: true
466
- },
467
- focus: {
468
- value(this: IMdcSelectElement) {
469
- CustomElement.for<MdcSelect>(this).viewModel.focus();
470
- },
471
- configurable: true
472
- },
473
- blur: {
474
- value(this: IMdcSelectElement) {
475
- CustomElement.for<MdcSelect>(this).viewModel.blur();
476
- },
477
- configurable: true
478
- }
479
- });
480
- }
1
+ import { MdcComponent, IValidatedElement, IError, booleanAttr } from '../base';
2
+ import { cssClasses, MDCSelectFoundationMap, MDCSelectEventDetail, strings } from '@material/select';
3
+ import { inject, customElement, INode, IPlatform, bindable } from 'aurelia';
4
+ import { MdcSelectIcon, IMdcSelectIconElement, mdcIconStrings } from './mdc-select-icon';
5
+ import { MdcSelectHelperText, mdcHelperTextCssClasses } from './mdc-select-helper-text/mdc-select-helper-text';
6
+ import { MDCNotchedOutline } from '@material/notched-outline';
7
+ import { MDCMenuItemEvent, Corner } from '@material/menu';
8
+ import { MDCSelectFoundationAurelia } from './mdc-select-foundation-aurelia';
9
+ import { MDCSelectAdapterAurelia } from './mdc-select-adapter-aurelia';
10
+ import { MDCMenuDistance } from '@material/menu-surface';
11
+ import { processContent, BindingMode, CustomElement, CustomAttribute } from '@aurelia/runtime-html';
12
+ import { MdcDefaultSelectConfiguration } from './mdc-default-select-configuration';
13
+ import template from './mdc-select.html?raw';
14
+ import { MdcFloatingLabel } from '../floating-label/mdc-floating-label';
15
+ import { MdcLineRipple } from '../line-ripple/mdc-line-ripple';
16
+ import { MdcListItem } from '../list/mdc-list-item/mdc-list-item';
17
+ import { MdcMenu } from '../menu/mdc-menu';
18
+
19
+ strings.CHANGE_EVENT = strings.CHANGE_EVENT.toLowerCase();
20
+
21
+ let selectId = 0;
22
+
23
+ /**
24
+ * @selector mdc-select
25
+ * @emits mdcselect:change | Emitted if user changed the value
26
+ */
27
+ @inject(Element, IPlatform, MdcDefaultSelectConfiguration)
28
+ @customElement({ name: 'mdc-select', template })
29
+ @processContent(function processContent(node: INode, platform: IPlatform) {
30
+ const el = node as Element;
31
+
32
+ const leadingIcon = el.querySelector(`[${mdcIconStrings.ATTRIBUTE}]`);
33
+ leadingIcon?.remove();
34
+
35
+ const template = platform.document.createElement('template');
36
+ template.setAttribute('au-slot', '');
37
+ template.innerHTML = el.innerHTML;
38
+ el.innerHTML = '';
39
+ el.appendChild(template);
40
+
41
+ // move icon to the slot - this allows omitting slot specification
42
+ if (leadingIcon) {
43
+ const div = platform.document.createElement('div');
44
+ div.appendChild(leadingIcon);
45
+ const iconTemplate = platform.document.createElement('template');
46
+ iconTemplate.setAttribute('au-slot', 'leading-icon');
47
+ iconTemplate.innerHTML = div.innerHTML;
48
+ el.appendChild(iconTemplate);
49
+ }
50
+ }
51
+ )
52
+ export class MdcSelect extends MdcComponent<MDCSelectFoundationAurelia> {
53
+ constructor(root: HTMLElement, private platform: IPlatform, private defaultConfiguration: MdcDefaultSelectConfiguration) {
54
+ super(root);
55
+ this.outlined = this.defaultConfiguration.outlined;
56
+ defineMdcSelectElementApis(this.root);
57
+ this.root.id = this.id;
58
+ }
59
+
60
+ id: string = `mdc-select-${++selectId}`;
61
+ public menu: MdcMenu;
62
+ private selectAnchor: HTMLElement;
63
+ private selectedText: HTMLElement;
64
+
65
+ private menuElement?: Element;
66
+
67
+ get items(): MdcListItem[] | undefined {
68
+ return this.menu.list_?.items;
69
+ }
70
+
71
+ private leadingIcon?: MdcSelectIcon;
72
+
73
+ private helperText?: MdcSelectHelperText;
74
+ private lineRipple?: MdcLineRipple;
75
+ private mdcLabel: MdcFloatingLabel;
76
+ private outline?: MDCNotchedOutline;
77
+ errors = new Map<IError, boolean>();
78
+
79
+ /** Sets the select label */
80
+ @bindable()
81
+ label: string;
82
+ labelChanged() {
83
+ this.platform.domQueue.queueTask(() => this.foundation?.layout());
84
+ }
85
+
86
+ /** Styles the select as an outlined select */
87
+ @bindable({ set: booleanAttr })
88
+ outlined?: boolean;
89
+ outlinedChanged() {
90
+ this.platform.domQueue.queueTask(() => this.foundation?.layout());
91
+ }
92
+
93
+ /** Makes the value required */
94
+ @bindable({ set: booleanAttr })
95
+ required: boolean;
96
+ requiredChanged() {
97
+ if (this.required) {
98
+ this.selectAnchor?.setAttribute('aria-required', 'true');
99
+ } else {
100
+ this.selectAnchor?.removeAttribute('aria-required');
101
+ }
102
+ this.foundation?.setRequired(this.required ?? false);
103
+ this.platform.domWriteQueue.queueTask(() => this.foundation?.layout());
104
+ }
105
+
106
+ /** Enables/disables the select */
107
+ @bindable({ set: booleanAttr })
108
+ disabled?: boolean;
109
+ disabledChanged() {
110
+ if (this.disabled !== undefined) {
111
+ this.foundation?.setDisabled(this.disabled);
112
+ }
113
+ }
114
+
115
+ /** Hoists the select DOM to document.body */
116
+ @bindable({ set: booleanAttr, mode: BindingMode.oneTime })
117
+ hoistToBody: boolean;
118
+
119
+ /** Sets the select DOM position to fixed */
120
+ @bindable({ set: booleanAttr, mode: BindingMode.oneTime })
121
+ fixed: boolean;
122
+
123
+ /** Sets the margin between the select input and the dropdown */
124
+ @bindable()
125
+ anchorMargin: Partial<MDCMenuDistance>;
126
+
127
+ /** Sets the select dropdown width to match content */
128
+ @bindable({ set: booleanAttr })
129
+ naturalWidth: boolean;
130
+
131
+ private _value: unknown;
132
+ get value(): unknown {
133
+ if (this.foundation) {
134
+ return this.foundation.getValue();
135
+ } else {
136
+ return this._value;
137
+ }
138
+ }
139
+
140
+ set value(value: unknown) {
141
+ this.setValue(value);
142
+ }
143
+
144
+ setValue(value: unknown, skipNotify: boolean = false) {
145
+ this._value = value;
146
+ if (this.foundation) {
147
+ this.foundation.setValue(value, skipNotify);
148
+ this.foundation.layout();
149
+ }
150
+ }
151
+
152
+ get valid(): boolean {
153
+ return this.foundation?.isValid() ?? true;
154
+ }
155
+
156
+ set valid(value: boolean) {
157
+ this.foundation?.setValid(value);
158
+ }
159
+
160
+ get selectedIndex(): number {
161
+ return this.foundation!.getSelectedIndex();
162
+ }
163
+
164
+ set selectedIndex(selectedIndex: number) {
165
+ this.foundation?.setSelectedIndex(selectedIndex, /** closeMenu */ true);
166
+ }
167
+
168
+ addError(error: IError) {
169
+ this.errors.set(error, true);
170
+ this.valid = false;
171
+ }
172
+
173
+ removeError(error: IError) {
174
+ this.errors.delete(error);
175
+ this.valid = this.errors.size === 0;
176
+ }
177
+
178
+ renderErrors() {
179
+ if (this.helperText) {
180
+ this.helperText.errors = Array.from(this.errors.keys()).filter(x => x.message !== null).map(x => x.message!);
181
+ }
182
+ }
183
+
184
+ async attaching() {
185
+ const nextSibling = this.root.nextElementSibling;
186
+ if (nextSibling?.tagName === mdcHelperTextCssClasses.ROOT.toUpperCase()) {
187
+ this.helperText = CustomElement.for<MdcSelectHelperText>(nextSibling).viewModel;
188
+ await this.helperText.initialised;
189
+ }
190
+ }
191
+
192
+ beforeFoundationCreated() {
193
+ const leadingIconEl = this.root.querySelector<IMdcSelectIconElement>(`${strings.LEADING_ICON_SELECTOR}`);
194
+ if (leadingIconEl) {
195
+ this.leadingIcon = CustomAttribute.for<MdcSelectIcon>(leadingIconEl, mdcIconStrings.ATTRIBUTE)?.viewModel;
196
+ }
197
+ this.menu.list_!.singleSelection = true;
198
+ }
199
+
200
+ initialSyncWithDOM() {
201
+ // set initial value without emitting change events
202
+ this.foundation?.setValue(this._value, true);
203
+ this.foundation?.layout();
204
+ this.errors = new Map<IError, boolean>();
205
+ this.valid = true;
206
+
207
+ this.labelChanged();
208
+ this.disabledChanged();
209
+ this.outlinedChanged();
210
+ this.requiredChanged();
211
+ }
212
+
213
+ getDefaultFoundation() {
214
+ // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
215
+ // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
216
+ const adapter: MDCSelectAdapterAurelia = {
217
+ ...this.getSelectAdapterMethods(),
218
+ ...this.getCommonAdapterMethods(),
219
+ ...this.getOutlineAdapterMethods(),
220
+ ...this.getLabelAdapterMethods(),
221
+ };
222
+ return new MDCSelectFoundationAurelia(adapter, this.getFoundationMap());
223
+ }
224
+
225
+ private getSelectAdapterMethods() {
226
+ return {
227
+ setSelectedText: (text: string) => {
228
+ this.selectedText.textContent = text;
229
+ },
230
+ isSelectAnchorFocused: () => document.activeElement === this.selectAnchor,
231
+ getSelectAnchorAttr: (attr: string) => this.selectAnchor.getAttribute(attr),
232
+ setSelectAnchorAttr: (attr: string, value: string) => {
233
+ this.selectAnchor.setAttribute(attr, value);
234
+ },
235
+ removeSelectAnchorAttr: (attr: string) => {
236
+ this.selectAnchor.removeAttribute(attr);
237
+ },
238
+ addMenuClass: (className: string) => {
239
+ this.menuElement?.classList.add(className);
240
+ },
241
+ removeMenuClass: (className: string) => {
242
+ this.menuElement?.classList.remove(className);
243
+ },
244
+ openMenu: () => {
245
+ this.menu.open = true;
246
+ this.menu.root.style.minWidth = this.menu.root.style.maxWidth = (this.hoistToBody || this.fixed) && !this.naturalWidth
247
+ ? `${this.root.clientWidth}px`
248
+ : '';
249
+ },
250
+ closeMenu: () => { this.menu.open = false; },
251
+ getAnchorElement: () => this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR)!,
252
+ setMenuAnchorElement: (anchorEl: HTMLElement) => {
253
+ this.menu.anchor = anchorEl;
254
+ },
255
+ setMenuAnchorCorner: (anchorCorner: Corner) => {
256
+ this.menu.setAnchorCorner(anchorCorner);
257
+ },
258
+ setMenuWrapFocus: (wrapFocus: boolean) => {
259
+ this.menu.wrapFocus = wrapFocus;
260
+ },
261
+ getSelectedIndex: () => {
262
+ const index = this.menu.selectedIndex;
263
+ return index instanceof Array ? index[0] : index;
264
+ },
265
+ setSelectedIndex: (index: number) => {
266
+ this.menu.selectedIndex = index;
267
+ },
268
+ removeAttributeAtIndex: (index: number, attributeName: string) => {
269
+ this.menu.items[index].removeAttribute(attributeName);
270
+ },
271
+ focusMenuItemAtIndex: (index: number) => {
272
+ (this.menu.items[index] as HTMLElement).focus();
273
+ },
274
+ getMenuItemCount: () => this.menu.items.length,
275
+ getMenuItemValues: () => this.menu.items.map(x => CustomElement.for<MdcListItem>(x).viewModel.value),
276
+ getMenuItemTextAtIndex: (index: number) => this.menu.getPrimaryTextAtIndex(index),
277
+ isTypeaheadInProgress: () => this.menu.typeaheadInProgress,
278
+ typeaheadMatchItem: (nextChar: string, startingIndex: number) => this.menu.typeaheadMatchItem(nextChar, startingIndex),
279
+ };
280
+ }
281
+
282
+ private getCommonAdapterMethods() {
283
+ return {
284
+ addClass: (className: string) => {
285
+ this.root.classList.add(className);
286
+ },
287
+ removeClass: (className: string) => {
288
+ this.root.classList.remove(className);
289
+ },
290
+ hasClass: (className: string) => this.root.classList.contains(className),
291
+ setRippleCenter: (normalizedX: number) => this.lineRipple?.setRippleCenter(normalizedX),
292
+ activateBottomLine: () => this.lineRipple?.activate(),
293
+ deactivateBottomLine: () => this.lineRipple?.deactivate(),
294
+ notifyChange: (value: string) => {
295
+ const index = this.selectedIndex;
296
+ this.emit<MDCSelectEventDetail>(strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */);
297
+ this.emit<MDCSelectEventDetail>('change', { value, index }, true /* shouldBubble */);
298
+ },
299
+ };
300
+ }
301
+
302
+ private getOutlineAdapterMethods() {
303
+ return {
304
+ hasOutline: () => Boolean(this.outline),
305
+ notchOutline: (labelWidth: number) => this.outline?.notch(labelWidth),
306
+ closeOutline: () => this.outline?.closeNotch(),
307
+ };
308
+ }
309
+
310
+ private getLabelAdapterMethods() {
311
+ return {
312
+ hasLabel: () => !!this.mdcLabel,
313
+ floatLabel: (shouldFloat: boolean) => this.mdcLabel?.float(shouldFloat),
314
+ getLabelWidth: () => this.mdcLabel ? this.mdcLabel.getWidth() : 0,
315
+ setLabelRequired: (isRequired: boolean) => this.mdcLabel?.setRequired(isRequired),
316
+ };
317
+ }
318
+
319
+ handleChange() {
320
+ this.foundation?.handleChange();
321
+ this.emit('change', {}, true);
322
+ }
323
+
324
+ handleFocus() {
325
+ this.foundation?.handleFocus();
326
+ }
327
+
328
+ handleBlur() {
329
+ this.foundation?.handleBlur();
330
+ // if class is set it means the menu is open,
331
+ // do not emit blur since "conceptually" the element is still active
332
+ if (!this.root.classList.contains(cssClasses.FOCUSED)) {
333
+ this.emit('blur', {}, true);
334
+ }
335
+ }
336
+
337
+ handleClick(evt: MouseEvent) {
338
+ this.selectAnchor.focus();
339
+ this.foundation?.handleClick(this.getNormalizedXCoordinate(evt));
340
+ }
341
+
342
+ handleKeydown(evt: KeyboardEvent) {
343
+ this.foundation?.handleKeydown(evt);
344
+ return true;
345
+ }
346
+
347
+ handleMenuItemAction(evt: MDCMenuItemEvent) {
348
+ this.foundation?.handleMenuItemAction(evt.detail.index);
349
+ }
350
+
351
+ handleMenuOpened() {
352
+ this.foundation?.handleMenuOpened();
353
+ }
354
+
355
+ handleMenuClosed() {
356
+ this.foundation?.handleMenuClosed();
357
+ if (!this.root.classList.contains(cssClasses.FOCUSED)) {
358
+ this.emit('blur', {}, true);
359
+ }
360
+ }
361
+
362
+ handleItemsChanged() {
363
+ this.foundation?.layoutOptions();
364
+ this.foundation?.layout();
365
+ }
366
+
367
+ focus() {
368
+ this.selectAnchor.focus();
369
+ }
370
+
371
+ blur() {
372
+ this.selectAnchor.blur();
373
+ }
374
+
375
+ /**
376
+ * @hidden
377
+ * Calculates where the line ripple should start based on the x coordinate within the component.
378
+ */
379
+ private getNormalizedXCoordinate(evt: MouseEvent | TouchEvent): number {
380
+ const targetClientRect = (evt.target as Element).getBoundingClientRect();
381
+ const xCoordinate =
382
+ this.isTouchEvent(evt) ? evt.touches[0].clientX : evt.clientX;
383
+ return xCoordinate - targetClientRect.left;
384
+ }
385
+
386
+ private isTouchEvent(evt: MouseEvent | TouchEvent): evt is TouchEvent {
387
+ return Boolean((evt as TouchEvent).touches);
388
+ }
389
+
390
+ /**
391
+ * @hidden
392
+ * Returns a map of all subcomponents to subfoundations.
393
+ */
394
+ private getFoundationMap(): Partial<MDCSelectFoundationMap> {
395
+ return {
396
+ helperText: this.helperText?.foundation,
397
+ leadingIcon: this.leadingIcon?.foundation
398
+ };
399
+ }
400
+ }
401
+
402
+ /** @hidden */
403
+ export interface IMdcSelectElement extends IValidatedElement {
404
+ $au: {
405
+ 'au:resource:custom-element': {
406
+ viewModel: MdcSelect;
407
+ };
408
+ };
409
+ value: unknown;
410
+ }
411
+
412
+ function defineMdcSelectElementApis(element: HTMLElement) {
413
+ Object.defineProperties(element, {
414
+ value: {
415
+ get(this: IMdcSelectElement) {
416
+ return CustomElement.for<MdcSelect>(this).viewModel.value;
417
+ },
418
+ set(this: IMdcSelectElement, value: unknown) {
419
+ // aurelia binding converts "undefined" and "null" into empty string
420
+ // this does not translate well into "empty" menu items when several selects are bound to the same field
421
+ CustomElement.for<MdcSelect>(this).viewModel.value = value === '' ? undefined : value;
422
+ },
423
+ configurable: true
424
+ },
425
+ options: {
426
+ get(this: IMdcSelectElement) {
427
+ return CustomElement.for<MdcSelect>(this).viewModel.root.querySelectorAll('.mdc-list-item');
428
+ },
429
+ configurable: true
430
+ },
431
+ selectedIndex: {
432
+ get(this: IMdcSelectElement) {
433
+ return CustomElement.for<MdcSelect>(this).viewModel.selectedIndex;
434
+ },
435
+ set(this: IMdcSelectElement, value: number) {
436
+ CustomElement.for<MdcSelect>(this).viewModel.selectedIndex = value;
437
+ },
438
+ configurable: true
439
+ },
440
+ valid: {
441
+ get(this: IMdcSelectElement) {
442
+ return CustomElement.for<MdcSelect>(this).viewModel.valid;
443
+ },
444
+ set(this: IMdcSelectElement, value: boolean) {
445
+ CustomElement.for<MdcSelect>(this).viewModel.valid = value;
446
+ },
447
+ configurable: true
448
+ },
449
+ addError: {
450
+ value(this: IMdcSelectElement, error: IError) {
451
+ CustomElement.for<MdcSelect>(this).viewModel.addError(error);
452
+ },
453
+ configurable: true
454
+ },
455
+ removeError: {
456
+ value(this: IMdcSelectElement, error: IError) {
457
+ CustomElement.for<MdcSelect>(this).viewModel.removeError(error);
458
+ },
459
+ configurable: true
460
+ },
461
+ renderErrors: {
462
+ value(this: IMdcSelectElement): void {
463
+ CustomElement.for<MdcSelect>(this).viewModel.renderErrors();
464
+ },
465
+ configurable: true
466
+ },
467
+ focus: {
468
+ value(this: IMdcSelectElement) {
469
+ CustomElement.for<MdcSelect>(this).viewModel.focus();
470
+ },
471
+ configurable: true
472
+ },
473
+ blur: {
474
+ value(this: IMdcSelectElement) {
475
+ CustomElement.for<MdcSelect>(this).viewModel.blur();
476
+ },
477
+ configurable: true
478
+ }
479
+ });
480
+ }