Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
refactor(select): Use pre-existing single-selection list logic.
Browse files Browse the repository at this point in the history
Also fix list foundation so that tabindex is set correctly (closes #6143)

PiperOrigin-RevId: 318506841
  • Loading branch information
allan-chen authored and copybara-github committed Jun 26, 2020
1 parent 69a35e8 commit df7154f
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 166 deletions.
14 changes: 2 additions & 12 deletions packages/mdc-list/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,6 @@ export class MDCList extends MDCComponent<MDCListFoundation> {
initializeListType() {
const checkboxListItems =
this.root.querySelectorAll(strings.ARIA_ROLE_CHECKBOX_SELECTOR);
const singleSelectedListItem = this.root.querySelector(`
.${cssClasses.LIST_ITEM_ACTIVATED_CLASS},
.${cssClasses.LIST_ITEM_SELECTED_CLASS}
`);
const radioSelectedListItem =
this.root.querySelector(strings.ARIA_CHECKED_RADIO_SELECTOR);

Expand All @@ -153,13 +149,6 @@ export class MDCList extends MDCComponent<MDCListFoundation> {
this.root.querySelectorAll(strings.ARIA_CHECKED_CHECKBOX_SELECTOR);
this.selectedIndex =
[].map.call(preselectedItems, (listItem: Element) => this.listElements.indexOf(listItem)) as number[];
} else if (singleSelectedListItem) {
if (singleSelectedListItem.classList.contains(cssClasses.LIST_ITEM_ACTIVATED_CLASS)) {
this.foundation.setUseActivatedClass(true);
}

this.singleSelection = true;
this.selectedIndex = this.listElements.indexOf(singleSelectedListItem);
} else if (radioSelectedListItem) {
this.selectedIndex = this.listElements.indexOf(radioSelectedListItem);
}
Expand Down Expand Up @@ -226,7 +215,8 @@ export class MDCList extends MDCComponent<MDCListFoundation> {
return toggleEl!.checked;
},
isFocusInsideList: () => {
return this.root.contains(document.activeElement);
return this.root !== document.activeElement &&
this.root.contains(document.activeElement);
},
isRootFocused: () => document.activeElement === this.root,
listItemAtIndexHasClass: (index, className) =>
Expand Down
85 changes: 67 additions & 18 deletions packages/mdc-list/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
* THE SOFTWARE.
*/

// TODO(b/152410470): Remove trailing underscores from private properties
// tslint:disable:strip-private-property-underscore

import {MDCFoundation} from '@material/base/foundation';
import {normalizeKey} from '@material/dom/keyboard';

Expand Down Expand Up @@ -97,6 +100,8 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
this.isCheckboxList_ = true;
} else if (this.adapter.hasRadioAtIndex(0)) {
this.isRadioList_ = true;
} else {
this.maybeInitializeSingleSelection();
}

if (this.hasTypeahead) {
Expand All @@ -123,6 +128,33 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
*/
setSingleSelection(value: boolean) {
this.isSingleSelectionList_ = value;
if (value) {
this.maybeInitializeSingleSelection();
}
}

/**
* Automatically determines whether the list is single selection list. If so,
* initializes the internal state to match the selected item.
*/
private maybeInitializeSingleSelection() {
for (let i = 0; i < this.adapter.getListItemCount(); i++) {
const hasSelectedClass = this.adapter.listItemAtIndexHasClass(
i, cssClasses.LIST_ITEM_SELECTED_CLASS);
const hasActivatedClass = this.adapter.listItemAtIndexHasClass(
i, cssClasses.LIST_ITEM_ACTIVATED_CLASS);
if (!(hasSelectedClass || hasActivatedClass)) {
continue;
}

if (hasActivatedClass) {
this.setUseActivatedClass(true);
}

this.isSingleSelectionList_ = true;
this.selectedIndex_ = i;
return;
}
}

/**
Expand Down Expand Up @@ -175,6 +207,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
handleFocusIn(_: FocusEvent, listItemIndex: number) {
if (listItemIndex >= 0) {
this.focusedItemIndex = listItemIndex;
this.adapter.setAttributeForElementIndex(listItemIndex, 'tabindex', '0');
this.adapter.setTabIndexForListItemChildren(listItemIndex, '0');
}
}
Expand All @@ -184,6 +217,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
*/
handleFocusOut(_: FocusEvent, listItemIndex: number) {
if (listItemIndex >= 0) {
this.adapter.setAttributeForElementIndex(listItemIndex, 'tabindex', '-1');
this.adapter.setTabIndexForListItemChildren(listItemIndex, '-1');
}

Expand All @@ -193,7 +227,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
*/
setTimeout(() => {
if (!this.adapter.isFocusInsideList()) {
this.setTabindexToFirstSelectedItem_();
this.setTabindexToFirstSelectedOrFocusedItem();
}
}, 0);
}
Expand Down Expand Up @@ -225,7 +259,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
const handleKeydownOpts: typeahead.HandleKeydownOpts = {
event,
focusItemAtIndex: (index) => {
this.focusItemAtIndex(index)
this.focusItemAtIndex(index);
},
focusedItemIndex: -1,
isTargetListItem: isRootListItem,
Expand Down Expand Up @@ -311,9 +345,6 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
return;
}

this.setTabindexAtIndex_(index);
this.focusedItemIndex = index;

if (this.adapter.listItemAtIndexHasClass(
index, cssClasses.LIST_ITEM_DISABLED_CLASS)) {
return;
Expand Down Expand Up @@ -409,8 +440,12 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
this.adapter.removeClassForElementIndex(
this.selectedIndex_ as number, selectedClassName);
}
this.adapter.addClassForElementIndex(index, selectedClassName);

this.setAriaForSingleSelectionAtIndex_(index);
this.setTabindexAtIndex_(index);
if (index !== numbers.UNSET_INDEX) {
this.adapter.addClassForElementIndex(index, selectedClassName);
}

this.selectedIndex_ = index;
}
Expand All @@ -433,9 +468,12 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
this.selectedIndex_ as number, ariaAttribute, 'false');
}

const ariaAttributeValue = isAriaCurrent ? this.ariaCurrentAttrValue_ : 'true';
this.adapter.setAttributeForElementIndex(
index, ariaAttribute, ariaAttributeValue as string);
if (index !== numbers.UNSET_INDEX) {
const ariaAttributeValue =
isAriaCurrent ? this.ariaCurrentAttrValue_ : 'true';
this.adapter.setAttributeForElementIndex(
index, ariaAttribute, ariaAttributeValue as string);
}
}

/**
Expand Down Expand Up @@ -472,15 +510,27 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {

private setTabindexAtIndex_(index: number) {
if (this.focusedItemIndex === numbers.UNSET_INDEX && index !== 0) {
// If no list item was selected set first list item's tabindex to -1.
// Generally, tabindex is set to 0 on first list item of list that has no preselected items.
// If some list item was selected set first list item's tabindex to -1.
// Generally, tabindex is set to 0 on first list item of list that has no
// preselected items.
this.adapter.setAttributeForElementIndex(0, 'tabindex', '-1');
} else if (this.focusedItemIndex >= 0 && this.focusedItemIndex !== index) {
this.adapter.setAttributeForElementIndex(
this.focusedItemIndex, 'tabindex', '-1');
}

this.adapter.setAttributeForElementIndex(index, 'tabindex', '0');
// Set the previous selection's tabindex to -1. We need this because
// in selection menus that are not visible, programmatically setting an
// option will not change focus but will change where tabindex should be 0.
if (!(this.selectedIndex_ instanceof Array) &&
this.selectedIndex_ !== index) {
this.adapter.setAttributeForElementIndex(
this.selectedIndex_, 'tabindex', '-1');
}

if (index !== numbers.UNSET_INDEX) {
this.adapter.setAttributeForElementIndex(index, 'tabindex', '0');
}
}

/**
Expand All @@ -490,9 +540,8 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
return this.isSingleSelectionList_ || this.isCheckboxList_ || this.isRadioList_;
}

private setTabindexToFirstSelectedItem_() {
let targetIndex = 0;

private setTabindexToFirstSelectedOrFocusedItem() {
let targetIndex = this.focusedItemIndex >= 0 ? this.focusedItemIndex : 0;
if (this.isSelectableList_()) {
if (typeof this.selectedIndex_ === 'number' && this.selectedIndex_ !== numbers.UNSET_INDEX) {
targetIndex = this.selectedIndex_;
Expand All @@ -519,7 +568,8 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
if (this.isCheckboxList_) {
throw new Error('MDCListFoundation: Expected array of index for checkbox based list but got number: ' + index);
}
return this.isIndexInRange_(index);
return this.isIndexInRange_(index) ||
this.isSingleSelectionList_ && index === numbers.UNSET_INDEX;
} else {
return false;
}
Expand Down Expand Up @@ -566,7 +616,6 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
}

private focusItemAtIndex(index: number) {
this.setTabindexAtIndex_(index);
this.adapter.focusItemAtIndex(index);
this.focusedItemIndex = index;
}
Expand All @@ -588,7 +637,7 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
nextChar: string, startingIndex?: number, skipFocus = false) {
const opts: typeahead.TypeaheadMatchItemOpts = {
focusItemAtIndex: (index) => {
this.focusItemAtIndex(index)
this.focusItemAtIndex(index);
},
focusedItemIndex: startingIndex ? startingIndex : this.focusedItemIndex,
nextChar,
Expand Down
39 changes: 0 additions & 39 deletions packages/mdc-list/test/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,45 +165,6 @@ describe('MDCList', () => {
expect(mockFoundation.setVerticalOrientation).toHaveBeenCalledTimes(1);
});

it('#initializeListType sets the selectedIndex if a list item has the --selected class',
() => {
const {root, component, mockFoundation} = setupTest();
(root.querySelector('.mdc-list-item') as HTMLElement)
.classList.add(
MDCListFoundation.cssClasses.LIST_ITEM_SELECTED_CLASS);
component.initializeListType();
expect(mockFoundation.setSelectedIndex).toHaveBeenCalledWith(0);
expect(mockFoundation.setSelectedIndex).toHaveBeenCalledTimes(1);
expect(mockFoundation.setSingleSelection).toHaveBeenCalledWith(true);
expect(mockFoundation.setSingleSelection).toHaveBeenCalledTimes(1);
});

it('#initializeListType sets the selectedIndex if a list item has the --activated class',
() => {
const {root, component, mockFoundation} = setupTest();
(root.querySelector('.mdc-list-item') as HTMLElement)
.classList.add(
MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS);
component.initializeListType();
expect(mockFoundation.setSelectedIndex).toHaveBeenCalledWith(0);
expect(mockFoundation.setSelectedIndex).toHaveBeenCalledTimes(1);
expect(mockFoundation.setSingleSelection).toHaveBeenCalledWith(true);
expect(mockFoundation.setSingleSelection).toHaveBeenCalledTimes(1);
});

it('#initializeListType calls the foundation if the --activated class is present',
() => {
const {root, component, mockFoundation} = setupTest();
(root.querySelector('.mdc-list-item') as HTMLElement)
.classList.add(
MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS);
component.initializeListType();
expect(mockFoundation.setUseActivatedClass).toHaveBeenCalledWith(true);
expect(mockFoundation.setUseActivatedClass).toHaveBeenCalledTimes(1);
expect(mockFoundation.setSingleSelection).toHaveBeenCalledWith(true);
expect(mockFoundation.setSingleSelection).toHaveBeenCalledTimes(1);
});

it('#initializeListType populates selectedIndex based on preselected checkbox items',
() => {
const {root, component, mockFoundation} = setupTest();
Expand Down
Loading

0 comments on commit df7154f

Please sign in to comment.