UNPKG

40.4 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2018 Google Inc.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 * THE SOFTWARE.
22 */
23import { __assign, __extends, __read, __spreadArray } from "tslib";
24import { MDCFoundation } from '@material/base/foundation';
25import { normalizeKey } from '@material/dom/keyboard';
26import { cssClasses, numbers, strings } from './constants';
27import { preventDefaultEvent } from './events';
28import * as typeahead from './typeahead';
29function isNumberArray(selectedIndex) {
30 return selectedIndex instanceof Array;
31}
32/** List of modifier keys to consider while handling keyboard events. */
33var handledModifierKeys = ['Alt', 'Control', 'Meta', 'Shift'];
34/** Checks if the event has the given modifier keys. */
35function createModifierChecker(event) {
36 var eventModifiers = new Set(event ? handledModifierKeys.filter(function (m) { return event.getModifierState(m); }) : []);
37 return function (modifiers) {
38 return modifiers.every(function (m) { return eventModifiers.has(m); }) &&
39 modifiers.length === eventModifiers.size;
40 };
41}
42var MDCListFoundation = /** @class */ (function (_super) {
43 __extends(MDCListFoundation, _super);
44 function MDCListFoundation(adapter) {
45 var _this = _super.call(this, __assign(__assign({}, MDCListFoundation.defaultAdapter), adapter)) || this;
46 _this.wrapFocus = false;
47 _this.isVertical = true;
48 _this.isSingleSelectionList = false;
49 _this.areDisabledItemsFocusable = true;
50 _this.selectedIndex = numbers.UNSET_INDEX;
51 _this.focusedItemIndex = numbers.UNSET_INDEX;
52 _this.useActivatedClass = false;
53 _this.useSelectedAttr = false;
54 _this.ariaCurrentAttrValue = null;
55 _this.isCheckboxList = false;
56 _this.isRadioList = false;
57 _this.lastSelectedIndex = null;
58 _this.hasTypeahead = false;
59 // Transiently holds current typeahead prefix from user.
60 _this.typeaheadState = typeahead.initState();
61 _this.sortedIndexByFirstChar = new Map();
62 return _this;
63 }
64 Object.defineProperty(MDCListFoundation, "strings", {
65 get: function () {
66 return strings;
67 },
68 enumerable: false,
69 configurable: true
70 });
71 Object.defineProperty(MDCListFoundation, "cssClasses", {
72 get: function () {
73 return cssClasses;
74 },
75 enumerable: false,
76 configurable: true
77 });
78 Object.defineProperty(MDCListFoundation, "numbers", {
79 get: function () {
80 return numbers;
81 },
82 enumerable: false,
83 configurable: true
84 });
85 Object.defineProperty(MDCListFoundation, "defaultAdapter", {
86 get: function () {
87 return {
88 addClassForElementIndex: function () { return undefined; },
89 focusItemAtIndex: function () { return undefined; },
90 getAttributeForElementIndex: function () { return null; },
91 getFocusedElementIndex: function () { return 0; },
92 getListItemCount: function () { return 0; },
93 hasCheckboxAtIndex: function () { return false; },
94 hasRadioAtIndex: function () { return false; },
95 isCheckboxCheckedAtIndex: function () { return false; },
96 isFocusInsideList: function () { return false; },
97 isRootFocused: function () { return false; },
98 listItemAtIndexHasClass: function () { return false; },
99 notifyAction: function () { return undefined; },
100 notifySelectionChange: function () { },
101 removeClassForElementIndex: function () { return undefined; },
102 setAttributeForElementIndex: function () { return undefined; },
103 setCheckedCheckboxOrRadioAtIndex: function () { return undefined; },
104 setTabIndexForListItemChildren: function () { return undefined; },
105 getPrimaryTextAtIndex: function () { return ''; },
106 };
107 },
108 enumerable: false,
109 configurable: true
110 });
111 MDCListFoundation.prototype.layout = function () {
112 if (this.adapter.getListItemCount() === 0) {
113 return;
114 }
115 // TODO(b/172274142): consider all items when determining the list's type.
116 if (this.adapter.hasCheckboxAtIndex(0)) {
117 this.isCheckboxList = true;
118 }
119 else if (this.adapter.hasRadioAtIndex(0)) {
120 this.isRadioList = true;
121 }
122 else {
123 this.maybeInitializeSingleSelection();
124 }
125 if (this.hasTypeahead) {
126 this.sortedIndexByFirstChar = this.typeaheadInitSortedIndex();
127 }
128 };
129 /** Returns the index of the item that was last focused. */
130 MDCListFoundation.prototype.getFocusedItemIndex = function () {
131 return this.focusedItemIndex;
132 };
133 /** Toggles focus wrapping with keyboard navigation. */
134 MDCListFoundation.prototype.setWrapFocus = function (value) {
135 this.wrapFocus = value;
136 };
137 /**
138 * Toggles orientation direction for keyboard navigation (true for vertical,
139 * false for horizontal).
140 */
141 MDCListFoundation.prototype.setVerticalOrientation = function (value) {
142 this.isVertical = value;
143 };
144 /** Toggles single-selection behavior. */
145 MDCListFoundation.prototype.setSingleSelection = function (value) {
146 this.isSingleSelectionList = value;
147 if (value) {
148 this.maybeInitializeSingleSelection();
149 this.selectedIndex = this.getSelectedIndexFromDOM();
150 }
151 };
152 MDCListFoundation.prototype.setDisabledItemsFocusable = function (value) {
153 this.areDisabledItemsFocusable = value;
154 };
155 /**
156 * Automatically determines whether the list is single selection list. If so,
157 * initializes the internal state to match the selected item.
158 */
159 MDCListFoundation.prototype.maybeInitializeSingleSelection = function () {
160 var selectedItemIndex = this.getSelectedIndexFromDOM();
161 if (selectedItemIndex === numbers.UNSET_INDEX)
162 return;
163 var hasActivatedClass = this.adapter.listItemAtIndexHasClass(selectedItemIndex, cssClasses.LIST_ITEM_ACTIVATED_CLASS);
164 if (hasActivatedClass) {
165 this.setUseActivatedClass(true);
166 }
167 this.isSingleSelectionList = true;
168 this.selectedIndex = selectedItemIndex;
169 };
170 /** @return Index of the first selected item based on the DOM state. */
171 MDCListFoundation.prototype.getSelectedIndexFromDOM = function () {
172 var selectedIndex = numbers.UNSET_INDEX;
173 var listItemsCount = this.adapter.getListItemCount();
174 for (var i = 0; i < listItemsCount; i++) {
175 var hasSelectedClass = this.adapter.listItemAtIndexHasClass(i, cssClasses.LIST_ITEM_SELECTED_CLASS);
176 var hasActivatedClass = this.adapter.listItemAtIndexHasClass(i, cssClasses.LIST_ITEM_ACTIVATED_CLASS);
177 if (!(hasSelectedClass || hasActivatedClass)) {
178 continue;
179 }
180 selectedIndex = i;
181 break;
182 }
183 return selectedIndex;
184 };
185 /**
186 * Sets whether typeahead is enabled on the list.
187 * @param hasTypeahead Whether typeahead is enabled.
188 */
189 MDCListFoundation.prototype.setHasTypeahead = function (hasTypeahead) {
190 this.hasTypeahead = hasTypeahead;
191 if (hasTypeahead) {
192 this.sortedIndexByFirstChar = this.typeaheadInitSortedIndex();
193 }
194 };
195 /**
196 * @return Whether typeahead is currently matching a user-specified prefix.
197 */
198 MDCListFoundation.prototype.isTypeaheadInProgress = function () {
199 return this.hasTypeahead &&
200 typeahead.isTypingInProgress(this.typeaheadState);
201 };
202 /** Toggle use of the "activated" CSS class. */
203 MDCListFoundation.prototype.setUseActivatedClass = function (useActivated) {
204 this.useActivatedClass = useActivated;
205 };
206 /**
207 * Toggles use of the selected attribute (true for aria-selected, false for
208 * aria-checked).
209 */
210 MDCListFoundation.prototype.setUseSelectedAttribute = function (useSelected) {
211 this.useSelectedAttr = useSelected;
212 };
213 MDCListFoundation.prototype.getSelectedIndex = function () {
214 return this.selectedIndex;
215 };
216 MDCListFoundation.prototype.setSelectedIndex = function (index, options) {
217 if (options === void 0) { options = {}; }
218 if (!this.isIndexValid(index)) {
219 return;
220 }
221 if (this.isCheckboxList) {
222 this.setCheckboxAtIndex(index, options);
223 }
224 else if (this.isRadioList) {
225 this.setRadioAtIndex(index, options);
226 }
227 else {
228 this.setSingleSelectionAtIndex(index, options);
229 }
230 };
231 /**
232 * Focus in handler for the list items.
233 */
234 MDCListFoundation.prototype.handleFocusIn = function (listItemIndex) {
235 if (listItemIndex >= 0) {
236 this.focusedItemIndex = listItemIndex;
237 this.adapter.setAttributeForElementIndex(listItemIndex, 'tabindex', '0');
238 this.adapter.setTabIndexForListItemChildren(listItemIndex, '0');
239 }
240 };
241 /**
242 * Focus out handler for the list items.
243 */
244 MDCListFoundation.prototype.handleFocusOut = function (listItemIndex) {
245 var _this = this;
246 if (listItemIndex >= 0) {
247 this.adapter.setAttributeForElementIndex(listItemIndex, 'tabindex', '-1');
248 this.adapter.setTabIndexForListItemChildren(listItemIndex, '-1');
249 }
250 /**
251 * Between Focusout & Focusin some browsers do not have focus on any
252 * element. Setting a delay to wait till the focus is moved to next element.
253 */
254 setTimeout(function () {
255 if (!_this.adapter.isFocusInsideList()) {
256 _this.setTabindexToFirstSelectedOrFocusedItem();
257 }
258 }, 0);
259 };
260 MDCListFoundation.prototype.isIndexDisabled = function (index) {
261 return this.adapter.listItemAtIndexHasClass(index, cssClasses.LIST_ITEM_DISABLED_CLASS);
262 };
263 /**
264 * Key handler for the list.
265 */
266 MDCListFoundation.prototype.handleKeydown = function (event, isRootListItem, listItemIndex) {
267 var _this = this;
268 var _a;
269 var isArrowLeft = normalizeKey(event) === 'ArrowLeft';
270 var isArrowUp = normalizeKey(event) === 'ArrowUp';
271 var isArrowRight = normalizeKey(event) === 'ArrowRight';
272 var isArrowDown = normalizeKey(event) === 'ArrowDown';
273 var isHome = normalizeKey(event) === 'Home';
274 var isEnd = normalizeKey(event) === 'End';
275 var isEnter = normalizeKey(event) === 'Enter';
276 var isSpace = normalizeKey(event) === 'Spacebar';
277 // The keys for forward and back differ based on list orientation.
278 var isForward = (this.isVertical && isArrowDown) || (!this.isVertical && isArrowRight);
279 var isBack = (this.isVertical && isArrowUp) || (!this.isVertical && isArrowLeft);
280 // Have to check both upper and lower case, because having caps lock on
281 // affects the value.
282 var isLetterA = event.key === 'A' || event.key === 'a';
283 var eventHasModifiers = createModifierChecker(event);
284 if (this.adapter.isRootFocused()) {
285 if ((isBack || isEnd) && eventHasModifiers([])) {
286 event.preventDefault();
287 this.focusLastElement();
288 }
289 else if ((isForward || isHome) && eventHasModifiers([])) {
290 event.preventDefault();
291 this.focusFirstElement();
292 }
293 else if (isBack && eventHasModifiers(['Shift']) && this.isCheckboxList) {
294 event.preventDefault();
295 var focusedIndex = this.focusLastElement();
296 if (focusedIndex !== -1) {
297 this.setSelectedIndexOnAction(focusedIndex, false);
298 }
299 }
300 else if (isForward && eventHasModifiers(['Shift']) && this.isCheckboxList) {
301 event.preventDefault();
302 var focusedIndex = this.focusFirstElement();
303 if (focusedIndex !== -1) {
304 this.setSelectedIndexOnAction(focusedIndex, false);
305 }
306 }
307 if (this.hasTypeahead) {
308 var handleKeydownOpts = {
309 event: event,
310 focusItemAtIndex: function (index) {
311 _this.focusItemAtIndex(index);
312 },
313 focusedItemIndex: -1,
314 isTargetListItem: isRootListItem,
315 sortedIndexByFirstChar: this.sortedIndexByFirstChar,
316 isItemAtIndexDisabled: function (index) { return _this.isIndexDisabled(index); },
317 };
318 typeahead.handleKeydown(handleKeydownOpts, this.typeaheadState);
319 }
320 return;
321 }
322 var currentIndex = this.adapter.getFocusedElementIndex();
323 if (currentIndex === -1) {
324 currentIndex = listItemIndex;
325 if (currentIndex < 0) {
326 // If this event doesn't have a mdc-list-item ancestor from the
327 // current list (not from a sublist), return early.
328 return;
329 }
330 }
331 if (isForward && eventHasModifiers([])) {
332 preventDefaultEvent(event);
333 this.focusNextElement(currentIndex);
334 }
335 else if (isBack && eventHasModifiers([])) {
336 preventDefaultEvent(event);
337 this.focusPrevElement(currentIndex);
338 }
339 else if (isForward && eventHasModifiers(['Shift']) && this.isCheckboxList) {
340 preventDefaultEvent(event);
341 var focusedIndex = this.focusNextElement(currentIndex);
342 if (focusedIndex !== -1) {
343 this.setSelectedIndexOnAction(focusedIndex, false);
344 }
345 }
346 else if (isBack && eventHasModifiers(['Shift']) && this.isCheckboxList) {
347 preventDefaultEvent(event);
348 var focusedIndex = this.focusPrevElement(currentIndex);
349 if (focusedIndex !== -1) {
350 this.setSelectedIndexOnAction(focusedIndex, false);
351 }
352 }
353 else if (isHome && eventHasModifiers([])) {
354 preventDefaultEvent(event);
355 this.focusFirstElement();
356 }
357 else if (isEnd && eventHasModifiers([])) {
358 preventDefaultEvent(event);
359 this.focusLastElement();
360 }
361 else if (isHome && eventHasModifiers(['Control', 'Shift']) &&
362 this.isCheckboxList) {
363 preventDefaultEvent(event);
364 if (this.isIndexDisabled(currentIndex)) {
365 return;
366 }
367 this.focusFirstElement();
368 this.toggleCheckboxRange(0, currentIndex, currentIndex);
369 }
370 else if (isEnd && eventHasModifiers(['Control', 'Shift']) &&
371 this.isCheckboxList) {
372 preventDefaultEvent(event);
373 if (this.isIndexDisabled(currentIndex)) {
374 return;
375 }
376 this.focusLastElement();
377 this.toggleCheckboxRange(currentIndex, this.adapter.getListItemCount() - 1, currentIndex);
378 }
379 else if (isLetterA && eventHasModifiers(['Control']) && this.isCheckboxList) {
380 event.preventDefault();
381 this.checkboxListToggleAll(this.selectedIndex === numbers.UNSET_INDEX ?
382 [] :
383 this.selectedIndex, true);
384 }
385 else if ((isEnter || isSpace) && eventHasModifiers([])) {
386 if (isRootListItem) {
387 // Return early if enter key is pressed on anchor element which triggers
388 // synthetic MouseEvent event.
389 var target = event.target;
390 if (target && target.tagName === 'A' && isEnter) {
391 return;
392 }
393 preventDefaultEvent(event);
394 if (this.isIndexDisabled(currentIndex)) {
395 return;
396 }
397 if (!this.isTypeaheadInProgress()) {
398 if (this.isSelectableList()) {
399 this.setSelectedIndexOnAction(currentIndex, false);
400 }
401 this.adapter.notifyAction(currentIndex);
402 }
403 }
404 }
405 else if ((isEnter || isSpace) && eventHasModifiers(['Shift']) &&
406 this.isCheckboxList) {
407 // Return early if enter key is pressed on anchor element which triggers
408 // synthetic MouseEvent event.
409 var target = event.target;
410 if (target && target.tagName === 'A' && isEnter) {
411 return;
412 }
413 preventDefaultEvent(event);
414 if (this.isIndexDisabled(currentIndex)) {
415 return;
416 }
417 if (!this.isTypeaheadInProgress()) {
418 this.toggleCheckboxRange((_a = this.lastSelectedIndex) !== null && _a !== void 0 ? _a : currentIndex, currentIndex, currentIndex);
419 this.adapter.notifyAction(currentIndex);
420 }
421 }
422 if (this.hasTypeahead) {
423 var handleKeydownOpts = {
424 event: event,
425 focusItemAtIndex: function (index) { _this.focusItemAtIndex(index); },
426 focusedItemIndex: this.focusedItemIndex,
427 isTargetListItem: isRootListItem,
428 sortedIndexByFirstChar: this.sortedIndexByFirstChar,
429 isItemAtIndexDisabled: function (index) { return _this.isIndexDisabled(index); },
430 };
431 typeahead.handleKeydown(handleKeydownOpts, this.typeaheadState);
432 }
433 };
434 /**
435 * Click handler for the list.
436 *
437 * @param index Index for the item that has been clicked.
438 * @param isCheckboxAlreadyUpdatedInAdapter Whether the checkbox for
439 * the list item has already been updated in the adapter. This attribute
440 * should be set to `true` when e.g. the click event directly landed on
441 * the underlying native checkbox element which would cause the checked
442 * state to be already toggled within `adapter.isCheckboxCheckedAtIndex`.
443 */
444 MDCListFoundation.prototype.handleClick = function (index, isCheckboxAlreadyUpdatedInAdapter, event) {
445 var _a;
446 var eventHasModifiers = createModifierChecker(event);
447 if (index === numbers.UNSET_INDEX) {
448 return;
449 }
450 if (this.isIndexDisabled(index)) {
451 return;
452 }
453 if (eventHasModifiers([])) {
454 if (this.isSelectableList()) {
455 this.setSelectedIndexOnAction(index, isCheckboxAlreadyUpdatedInAdapter);
456 }
457 this.adapter.notifyAction(index);
458 }
459 else if (this.isCheckboxList && eventHasModifiers(['Shift'])) {
460 this.toggleCheckboxRange((_a = this.lastSelectedIndex) !== null && _a !== void 0 ? _a : index, index, index);
461 this.adapter.notifyAction(index);
462 }
463 };
464 /**
465 * Focuses the next element on the list.
466 */
467 MDCListFoundation.prototype.focusNextElement = function (index) {
468 var count = this.adapter.getListItemCount();
469 var nextIndex = index;
470 var firstChecked = null;
471 do {
472 nextIndex++;
473 if (nextIndex >= count) {
474 if (this.wrapFocus) {
475 nextIndex = 0;
476 }
477 else {
478 // Return early because last item is already focused.
479 return index;
480 }
481 }
482 if (nextIndex === firstChecked) {
483 return -1;
484 }
485 firstChecked = firstChecked !== null && firstChecked !== void 0 ? firstChecked : nextIndex;
486 } while (!this.areDisabledItemsFocusable && this.isIndexDisabled(nextIndex));
487 this.focusItemAtIndex(nextIndex);
488 return nextIndex;
489 };
490 /**
491 * Focuses the previous element on the list.
492 */
493 MDCListFoundation.prototype.focusPrevElement = function (index) {
494 var count = this.adapter.getListItemCount();
495 var prevIndex = index;
496 var firstChecked = null;
497 do {
498 prevIndex--;
499 if (prevIndex < 0) {
500 if (this.wrapFocus) {
501 prevIndex = count - 1;
502 }
503 else {
504 // Return early because first item is already focused.
505 return index;
506 }
507 }
508 if (prevIndex === firstChecked) {
509 return -1;
510 }
511 firstChecked = firstChecked !== null && firstChecked !== void 0 ? firstChecked : prevIndex;
512 } while (!this.areDisabledItemsFocusable && this.isIndexDisabled(prevIndex));
513 this.focusItemAtIndex(prevIndex);
514 return prevIndex;
515 };
516 MDCListFoundation.prototype.focusFirstElement = function () {
517 // Pass -1 to `focusNextElement`, since it will incremement to 0 and focus
518 // the first element.
519 return this.focusNextElement(-1);
520 };
521 MDCListFoundation.prototype.focusLastElement = function () {
522 // Pass the length of the list to `focusNextElement` since it will decrement
523 // to length - 1 and focus the last element.
524 return this.focusPrevElement(this.adapter.getListItemCount());
525 };
526 MDCListFoundation.prototype.focusInitialElement = function () {
527 var initialIndex = this.getFirstSelectedOrFocusedItemIndex();
528 this.focusItemAtIndex(initialIndex);
529 return initialIndex;
530 };
531 /**
532 * @param itemIndex Index of the list item
533 * @param isEnabled Sets the list item to enabled or disabled.
534 */
535 MDCListFoundation.prototype.setEnabled = function (itemIndex, isEnabled) {
536 if (!this.isIndexValid(itemIndex, false)) {
537 return;
538 }
539 if (isEnabled) {
540 this.adapter.removeClassForElementIndex(itemIndex, cssClasses.LIST_ITEM_DISABLED_CLASS);
541 this.adapter.setAttributeForElementIndex(itemIndex, strings.ARIA_DISABLED, 'false');
542 }
543 else {
544 this.adapter.addClassForElementIndex(itemIndex, cssClasses.LIST_ITEM_DISABLED_CLASS);
545 this.adapter.setAttributeForElementIndex(itemIndex, strings.ARIA_DISABLED, 'true');
546 }
547 };
548 MDCListFoundation.prototype.setSingleSelectionAtIndex = function (index, options) {
549 if (options === void 0) { options = {}; }
550 if (this.selectedIndex === index && !options.forceUpdate) {
551 return;
552 }
553 var selectedClassName = cssClasses.LIST_ITEM_SELECTED_CLASS;
554 if (this.useActivatedClass) {
555 selectedClassName = cssClasses.LIST_ITEM_ACTIVATED_CLASS;
556 }
557 if (this.selectedIndex !== numbers.UNSET_INDEX) {
558 this.adapter.removeClassForElementIndex(this.selectedIndex, selectedClassName);
559 }
560 this.setAriaForSingleSelectionAtIndex(index);
561 this.setTabindexAtIndex(index);
562 if (index !== numbers.UNSET_INDEX) {
563 this.adapter.addClassForElementIndex(index, selectedClassName);
564 }
565 this.selectedIndex = index;
566 // If the selected value has changed through user interaction,
567 // we want to notify the selection change to the adapter.
568 if (options.isUserInteraction && !options.forceUpdate) {
569 this.adapter.notifySelectionChange([index]);
570 }
571 };
572 /**
573 * Sets aria attribute for single selection at given index.
574 */
575 MDCListFoundation.prototype.setAriaForSingleSelectionAtIndex = function (index) {
576 // Detect the presence of aria-current and get the value only during list
577 // initialization when it is in unset state.
578 if (this.selectedIndex === numbers.UNSET_INDEX) {
579 this.ariaCurrentAttrValue =
580 this.adapter.getAttributeForElementIndex(index, strings.ARIA_CURRENT);
581 }
582 var isAriaCurrent = this.ariaCurrentAttrValue !== null;
583 var ariaAttribute = isAriaCurrent ? strings.ARIA_CURRENT : strings.ARIA_SELECTED;
584 if (this.selectedIndex !== numbers.UNSET_INDEX) {
585 this.adapter.setAttributeForElementIndex(this.selectedIndex, ariaAttribute, 'false');
586 }
587 if (index !== numbers.UNSET_INDEX) {
588 var ariaAttributeValue = isAriaCurrent ? this.ariaCurrentAttrValue : 'true';
589 this.adapter.setAttributeForElementIndex(index, ariaAttribute, ariaAttributeValue);
590 }
591 };
592 /**
593 * Returns the attribute to use for indicating selection status.
594 */
595 MDCListFoundation.prototype.getSelectionAttribute = function () {
596 return this.useSelectedAttr ? strings.ARIA_SELECTED : strings.ARIA_CHECKED;
597 };
598 /**
599 * Toggles radio at give index. Radio doesn't change the checked state if it
600 * is already checked.
601 */
602 MDCListFoundation.prototype.setRadioAtIndex = function (index, options) {
603 if (options === void 0) { options = {}; }
604 var selectionAttribute = this.getSelectionAttribute();
605 this.adapter.setCheckedCheckboxOrRadioAtIndex(index, true);
606 if (this.selectedIndex === index && !options.forceUpdate) {
607 return;
608 }
609 if (this.selectedIndex !== numbers.UNSET_INDEX) {
610 this.adapter.setAttributeForElementIndex(this.selectedIndex, selectionAttribute, 'false');
611 }
612 this.adapter.setAttributeForElementIndex(index, selectionAttribute, 'true');
613 this.selectedIndex = index;
614 // If the selected value has changed through user interaction,
615 // we want to notify the selection change to the adapter.
616 if (options.isUserInteraction && !options.forceUpdate) {
617 this.adapter.notifySelectionChange([index]);
618 }
619 };
620 MDCListFoundation.prototype.setCheckboxAtIndex = function (index, options) {
621 if (options === void 0) { options = {}; }
622 var currentIndex = this.selectedIndex;
623 // If this update is not triggered by a user interaction, we do not
624 // need to know about the currently selected indices and can avoid
625 // constructing the `Set` for performance reasons.
626 var currentlySelected = options.isUserInteraction ?
627 new Set(currentIndex === numbers.UNSET_INDEX ? [] :
628 currentIndex) :
629 null;
630 var selectionAttribute = this.getSelectionAttribute();
631 var changedIndices = [];
632 for (var i = 0; i < this.adapter.getListItemCount(); i++) {
633 var previousIsChecked = currentlySelected === null || currentlySelected === void 0 ? void 0 : currentlySelected.has(i);
634 var newIsChecked = index.indexOf(i) >= 0;
635 // If the selection has changed for this item, we keep track of it
636 // so that we can notify the adapter.
637 if (newIsChecked !== previousIsChecked) {
638 changedIndices.push(i);
639 }
640 this.adapter.setCheckedCheckboxOrRadioAtIndex(i, newIsChecked);
641 this.adapter.setAttributeForElementIndex(i, selectionAttribute, newIsChecked ? 'true' : 'false');
642 }
643 this.selectedIndex = index;
644 // If the selected value has changed through user interaction,
645 // we want to notify the selection change to the adapter.
646 if (options.isUserInteraction && changedIndices.length) {
647 this.adapter.notifySelectionChange(changedIndices);
648 }
649 };
650 /**
651 * Toggles the state of all checkboxes in the given range (inclusive) based on
652 * the state of the checkbox at the `toggleIndex`. To determine whether to set
653 * the given range to checked or unchecked, read the value of the checkbox at
654 * the `toggleIndex` and negate it. Then apply that new checked state to all
655 * checkboxes in the range.
656 * @param fromIndex The start of the range of checkboxes to toggle
657 * @param toIndex The end of the range of checkboxes to toggle
658 * @param toggleIndex The index that will be used to determine the new state
659 * of the given checkbox range.
660 */
661 MDCListFoundation.prototype.toggleCheckboxRange = function (fromIndex, toIndex, toggleIndex) {
662 this.lastSelectedIndex = toggleIndex;
663 var currentlySelected = new Set(this.selectedIndex === numbers.UNSET_INDEX ?
664 [] :
665 this.selectedIndex);
666 var newIsChecked = !(currentlySelected === null || currentlySelected === void 0 ? void 0 : currentlySelected.has(toggleIndex));
667 var _a = __read([fromIndex, toIndex].sort(), 2), startIndex = _a[0], endIndex = _a[1];
668 var selectionAttribute = this.getSelectionAttribute();
669 var changedIndices = [];
670 for (var i = startIndex; i <= endIndex; i++) {
671 if (this.isIndexDisabled(i)) {
672 continue;
673 }
674 var previousIsChecked = currentlySelected.has(i);
675 // If the selection has changed for this item, we keep track of it
676 // so that we can notify the adapter.
677 if (newIsChecked !== previousIsChecked) {
678 changedIndices.push(i);
679 this.adapter.setCheckedCheckboxOrRadioAtIndex(i, newIsChecked);
680 this.adapter.setAttributeForElementIndex(i, selectionAttribute, "" + newIsChecked);
681 if (newIsChecked) {
682 currentlySelected.add(i);
683 }
684 else {
685 currentlySelected.delete(i);
686 }
687 }
688 }
689 // If the selected value has changed, update and notify the selection change
690 // to the adapter.
691 if (changedIndices.length) {
692 this.selectedIndex = __spreadArray([], __read(currentlySelected));
693 this.adapter.notifySelectionChange(changedIndices);
694 }
695 };
696 MDCListFoundation.prototype.setTabindexAtIndex = function (index) {
697 if (this.focusedItemIndex === numbers.UNSET_INDEX && index !== 0) {
698 // If some list item was selected set first list item's tabindex to -1.
699 // Generally, tabindex is set to 0 on first list item of list that has no
700 // preselected items.
701 this.adapter.setAttributeForElementIndex(0, 'tabindex', '-1');
702 }
703 else if (this.focusedItemIndex >= 0 && this.focusedItemIndex !== index) {
704 this.adapter.setAttributeForElementIndex(this.focusedItemIndex, 'tabindex', '-1');
705 }
706 // Set the previous selection's tabindex to -1. We need this because
707 // in selection menus that are not visible, programmatically setting an
708 // option will not change focus but will change where tabindex should be 0.
709 if (!(this.selectedIndex instanceof Array) &&
710 this.selectedIndex !== index) {
711 this.adapter.setAttributeForElementIndex(this.selectedIndex, 'tabindex', '-1');
712 }
713 if (index !== numbers.UNSET_INDEX) {
714 this.adapter.setAttributeForElementIndex(index, 'tabindex', '0');
715 }
716 };
717 /**
718 * @return Return true if it is single selectin list, checkbox list or radio
719 * list.
720 */
721 MDCListFoundation.prototype.isSelectableList = function () {
722 return this.isSingleSelectionList || this.isCheckboxList ||
723 this.isRadioList;
724 };
725 MDCListFoundation.prototype.setTabindexToFirstSelectedOrFocusedItem = function () {
726 var targetIndex = this.getFirstSelectedOrFocusedItemIndex();
727 this.setTabindexAtIndex(targetIndex);
728 };
729 MDCListFoundation.prototype.getFirstSelectedOrFocusedItemIndex = function () {
730 // Action lists retain focus on the most recently focused item.
731 if (!this.isSelectableList()) {
732 return Math.max(this.focusedItemIndex, 0);
733 }
734 // Single-selection lists focus the selected item.
735 if (typeof this.selectedIndex === 'number' &&
736 this.selectedIndex !== numbers.UNSET_INDEX) {
737 return this.selectedIndex;
738 }
739 // Multiple-selection lists focus the first selected item.
740 if (isNumberArray(this.selectedIndex) && this.selectedIndex.length > 0) {
741 return this.selectedIndex.reduce(function (minIndex, currentIndex) { return Math.min(minIndex, currentIndex); });
742 }
743 // Selection lists without a selection focus the first item.
744 return 0;
745 };
746 MDCListFoundation.prototype.isIndexValid = function (index, validateListType) {
747 var _this = this;
748 if (validateListType === void 0) { validateListType = true; }
749 if (index instanceof Array) {
750 if (!this.isCheckboxList && validateListType) {
751 throw new Error('MDCListFoundation: Array of index is only supported for checkbox based list');
752 }
753 if (index.length === 0) {
754 return true;
755 }
756 else {
757 return index.some(function (i) { return _this.isIndexInRange(i); });
758 }
759 }
760 else if (typeof index === 'number') {
761 if (this.isCheckboxList && validateListType) {
762 throw new Error("MDCListFoundation: Expected array of index for checkbox based list but got number: " + index);
763 }
764 return this.isIndexInRange(index) ||
765 this.isSingleSelectionList && index === numbers.UNSET_INDEX;
766 }
767 else {
768 return false;
769 }
770 };
771 MDCListFoundation.prototype.isIndexInRange = function (index) {
772 var listSize = this.adapter.getListItemCount();
773 return index >= 0 && index < listSize;
774 };
775 /**
776 * Sets selected index on user action, toggles checkboxes in checkbox lists
777 * by default, unless `isCheckboxAlreadyUpdatedInAdapter` is set to `true`.
778 *
779 * In cases where `isCheckboxAlreadyUpdatedInAdapter` is set to `true`, the
780 * UI is just updated to reflect the value returned by the adapter.
781 *
782 * When calling this, make sure user interaction does not toggle disabled
783 * list items.
784 */
785 MDCListFoundation.prototype.setSelectedIndexOnAction = function (index, isCheckboxAlreadyUpdatedInAdapter) {
786 this.lastSelectedIndex = index;
787 if (this.isCheckboxList) {
788 this.toggleCheckboxAtIndex(index, isCheckboxAlreadyUpdatedInAdapter);
789 this.adapter.notifySelectionChange([index]);
790 }
791 else {
792 this.setSelectedIndex(index, { isUserInteraction: true });
793 }
794 };
795 MDCListFoundation.prototype.toggleCheckboxAtIndex = function (index, isCheckboxAlreadyUpdatedInAdapter) {
796 var selectionAttribute = this.getSelectionAttribute();
797 var adapterIsChecked = this.adapter.isCheckboxCheckedAtIndex(index);
798 // By default the checked value from the adapter is toggled unless the
799 // checked state in the adapter has already been updated beforehand.
800 // This can be happen when the underlying native checkbox has already
801 // been updated through the native click event.
802 var newCheckedValue;
803 if (isCheckboxAlreadyUpdatedInAdapter) {
804 newCheckedValue = adapterIsChecked;
805 }
806 else {
807 newCheckedValue = !adapterIsChecked;
808 this.adapter.setCheckedCheckboxOrRadioAtIndex(index, newCheckedValue);
809 }
810 this.adapter.setAttributeForElementIndex(index, selectionAttribute, newCheckedValue ? 'true' : 'false');
811 // If none of the checkbox items are selected and selectedIndex is not
812 // initialized then provide a default value.
813 var selectedIndexes = this.selectedIndex === numbers.UNSET_INDEX ?
814 [] :
815 this.selectedIndex.slice();
816 if (newCheckedValue) {
817 selectedIndexes.push(index);
818 }
819 else {
820 selectedIndexes = selectedIndexes.filter(function (i) { return i !== index; });
821 }
822 this.selectedIndex = selectedIndexes;
823 };
824 MDCListFoundation.prototype.focusItemAtIndex = function (index) {
825 this.adapter.focusItemAtIndex(index);
826 this.focusedItemIndex = index;
827 };
828 MDCListFoundation.prototype.checkboxListToggleAll = function (currentlySelectedIndexes, isUserInteraction) {
829 var count = this.adapter.getListItemCount();
830 // If all items are selected, deselect everything.
831 if (currentlySelectedIndexes.length === count) {
832 this.setCheckboxAtIndex([], { isUserInteraction: isUserInteraction });
833 }
834 else {
835 // Otherwise select all enabled options.
836 var allIndexes = [];
837 for (var i = 0; i < count; i++) {
838 if (!this.isIndexDisabled(i) ||
839 currentlySelectedIndexes.indexOf(i) > -1) {
840 allIndexes.push(i);
841 }
842 }
843 this.setCheckboxAtIndex(allIndexes, { isUserInteraction: isUserInteraction });
844 }
845 };
846 /**
847 * Given the next desired character from the user, adds it to the typeahead
848 * buffer. Then, attempts to find the next option matching the buffer. Wraps
849 * around if at the end of options.
850 *
851 * @param nextChar The next character to add to the prefix buffer.
852 * @param startingIndex The index from which to start matching. Only relevant
853 * when starting a new match sequence. To start a new match sequence,
854 * clear the buffer using `clearTypeaheadBuffer`, or wait for the buffer
855 * to clear after a set interval defined in list foundation. Defaults to
856 * the currently focused index.
857 * @return The index of the matched item, or -1 if no match.
858 */
859 MDCListFoundation.prototype.typeaheadMatchItem = function (nextChar, startingIndex, skipFocus) {
860 var _this = this;
861 if (skipFocus === void 0) { skipFocus = false; }
862 var opts = {
863 focusItemAtIndex: function (index) {
864 _this.focusItemAtIndex(index);
865 },
866 focusedItemIndex: startingIndex ? startingIndex : this.focusedItemIndex,
867 nextChar: nextChar,
868 sortedIndexByFirstChar: this.sortedIndexByFirstChar,
869 skipFocus: skipFocus,
870 isItemAtIndexDisabled: function (index) { return _this.isIndexDisabled(index); }
871 };
872 return typeahead.matchItem(opts, this.typeaheadState);
873 };
874 /**
875 * Initializes the MDCListTextAndIndex data structure by indexing the current
876 * list items by primary text.
877 *
878 * @return The primary texts of all the list items sorted by first character.
879 */
880 MDCListFoundation.prototype.typeaheadInitSortedIndex = function () {
881 return typeahead.initSortedIndex(this.adapter.getListItemCount(), this.adapter.getPrimaryTextAtIndex);
882 };
883 /**
884 * Clears the typeahead buffer.
885 */
886 MDCListFoundation.prototype.clearTypeaheadBuffer = function () {
887 typeahead.clearBuffer(this.typeaheadState);
888 };
889 return MDCListFoundation;
890}(MDCFoundation));
891export { MDCListFoundation };
892// tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
893export default MDCListFoundation;
894//# sourceMappingURL=foundation.js.map
\No newline at end of file