mirror of
https://github.com/material-components/material-web.git
synced 2024-08-17 09:30:27 +03:00
feat(menu): add no-navigation-wrap
to fix select accessibility
PiperOrigin-RevId: 610514684
This commit is contained in:
parent
ec0a8ebf6e
commit
c6ffd70fc8
@ -71,6 +71,11 @@ export interface ListControllerConfig<Item extends ListItem> {
|
||||
* disabled.
|
||||
*/
|
||||
isActivatable?: (item: Item) => boolean;
|
||||
/**
|
||||
* Whether or not navigating past the end of the list wraps to the beginning
|
||||
* and vice versa. Defaults to true.
|
||||
*/
|
||||
wrapNavigation?: () => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,6 +89,7 @@ export class ListController<Item extends ListItem> {
|
||||
private readonly activateItem: (item: Item) => void;
|
||||
private readonly isNavigableKey: (key: string) => boolean;
|
||||
private readonly isActivatable?: (item: Item) => boolean;
|
||||
private readonly wrapNavigation: () => boolean;
|
||||
|
||||
constructor(config: ListControllerConfig<Item>) {
|
||||
const {
|
||||
@ -94,6 +100,7 @@ export class ListController<Item extends ListItem> {
|
||||
activateItem,
|
||||
isNavigableKey,
|
||||
isActivatable,
|
||||
wrapNavigation,
|
||||
} = config;
|
||||
this.isItem = isItem;
|
||||
this.getPossibleItems = getPossibleItems;
|
||||
@ -102,6 +109,7 @@ export class ListController<Item extends ListItem> {
|
||||
this.activateItem = activateItem;
|
||||
this.isNavigableKey = isNavigableKey;
|
||||
this.isActivatable = isActivatable;
|
||||
this.wrapNavigation = wrapNavigation ?? (() => true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,10 +157,6 @@ export class ListController<Item extends ListItem> {
|
||||
|
||||
const activeItemRecord = getActiveItem(items, this.isActivatable);
|
||||
|
||||
if (activeItemRecord) {
|
||||
activeItemRecord.item.tabIndex = -1;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const isRtl = this.isRtl();
|
||||
@ -163,32 +167,53 @@ export class ListController<Item extends ListItem> {
|
||||
? NavigableKeys.ArrowLeft
|
||||
: NavigableKeys.ArrowRight;
|
||||
|
||||
let nextActiveItem: Item | null = null;
|
||||
switch (key) {
|
||||
// Activate the next item
|
||||
case NavigableKeys.ArrowDown:
|
||||
case inlineNext:
|
||||
activateNextItem(items, activeItemRecord, this.isActivatable);
|
||||
nextActiveItem = activateNextItem(
|
||||
items,
|
||||
activeItemRecord,
|
||||
this.isActivatable,
|
||||
this.wrapNavigation(),
|
||||
);
|
||||
break;
|
||||
|
||||
// Activate the previous item
|
||||
case NavigableKeys.ArrowUp:
|
||||
case inlinePrevious:
|
||||
activatePreviousItem(items, activeItemRecord, this.isActivatable);
|
||||
nextActiveItem = activatePreviousItem(
|
||||
items,
|
||||
activeItemRecord,
|
||||
this.isActivatable,
|
||||
this.wrapNavigation(),
|
||||
);
|
||||
break;
|
||||
|
||||
// Activate the first item
|
||||
case NavigableKeys.Home:
|
||||
activateFirstItem(items, this.isActivatable);
|
||||
nextActiveItem = activateFirstItem(items, this.isActivatable);
|
||||
break;
|
||||
|
||||
// Activate the last item
|
||||
case NavigableKeys.End:
|
||||
activateLastItem(items, this.isActivatable);
|
||||
nextActiveItem = activateLastItem(items, this.isActivatable);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
nextActiveItem &&
|
||||
activeItemRecord &&
|
||||
activeItemRecord.item !== nextActiveItem
|
||||
) {
|
||||
// If a new item was activated, remove the tabindex of the previous
|
||||
// activated item.
|
||||
activeItemRecord.item.tabIndex = -1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -203,7 +228,12 @@ export class ListController<Item extends ListItem> {
|
||||
if (activeItemRecord) {
|
||||
activeItemRecord.item.tabIndex = -1;
|
||||
}
|
||||
return activateNextItem(items, activeItemRecord, this.isActivatable);
|
||||
return activateNextItem(
|
||||
items,
|
||||
activeItemRecord,
|
||||
this.isActivatable,
|
||||
this.wrapNavigation(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,7 +248,12 @@ export class ListController<Item extends ListItem> {
|
||||
if (activeItemRecord) {
|
||||
activeItemRecord.item.tabIndex = -1;
|
||||
}
|
||||
return activatePreviousItem(items, activeItemRecord, this.isActivatable);
|
||||
return activatePreviousItem(
|
||||
items,
|
||||
activeItemRecord,
|
||||
this.isActivatable,
|
||||
this.wrapNavigation(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,15 +162,23 @@ export function getLastActivatableItem<Item extends ListItem>(
|
||||
* @param index {{index: number}} The index to search from.
|
||||
* @param isActivatable Function to determine if an item can be activated.
|
||||
* Defaults to non-disabled items.
|
||||
* @param wrap If true, then the next item at the end of the list is the first
|
||||
* item. Defaults to true.
|
||||
* @return The next activatable item or `null` if none are activatable.
|
||||
*/
|
||||
export function getNextItem<Item extends ListItem>(
|
||||
items: Item[],
|
||||
index: number,
|
||||
isActivatable = isItemNotDisabled<Item>,
|
||||
wrap = true,
|
||||
) {
|
||||
for (let i = 1; i < items.length; i++) {
|
||||
const nextIndex = (i + index) % items.length;
|
||||
if (nextIndex < index && !wrap) {
|
||||
// Return if the index loops back to the beginning and not wrapping.
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = items[nextIndex];
|
||||
if (isActivatable(item)) {
|
||||
return item;
|
||||
@ -187,15 +195,23 @@ export function getNextItem<Item extends ListItem>(
|
||||
* @param index {{index: number}} The index to search from.
|
||||
* @param isActivatable Function to determine if an item can be activated.
|
||||
* Defaults to non-disabled items.
|
||||
* @param wrap If true, then the previous item at the beginning of the list is
|
||||
* the last item. Defaults to true.
|
||||
* @return The previous activatable item or `null` if none are activatable.
|
||||
*/
|
||||
export function getPrevItem<Item extends ListItem>(
|
||||
items: Item[],
|
||||
index: number,
|
||||
isActivatable = isItemNotDisabled<Item>,
|
||||
wrap = true,
|
||||
) {
|
||||
for (let i = 1; i < items.length; i++) {
|
||||
const prevIndex = (index - i + items.length) % items.length;
|
||||
if (prevIndex > index && !wrap) {
|
||||
// Return if the index loops back to the end and not wrapping.
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = items[prevIndex];
|
||||
|
||||
if (isActivatable(item)) {
|
||||
@ -214,9 +230,15 @@ export function activateNextItem<Item extends ListItem>(
|
||||
items: Item[],
|
||||
activeItemRecord: null | ItemRecord<Item>,
|
||||
isActivatable = isItemNotDisabled<Item>,
|
||||
wrap = true,
|
||||
): Item | null {
|
||||
if (activeItemRecord) {
|
||||
const next = getNextItem(items, activeItemRecord.index, isActivatable);
|
||||
const next = getNextItem(
|
||||
items,
|
||||
activeItemRecord.index,
|
||||
isActivatable,
|
||||
wrap,
|
||||
);
|
||||
|
||||
if (next) {
|
||||
next.tabIndex = 0;
|
||||
@ -237,9 +259,15 @@ export function activatePreviousItem<Item extends ListItem>(
|
||||
items: Item[],
|
||||
activeItemRecord: null | ItemRecord<Item>,
|
||||
isActivatable = isItemNotDisabled<Item>,
|
||||
wrap = true,
|
||||
): Item | null {
|
||||
if (activeItemRecord) {
|
||||
const prev = getPrevItem(items, activeItemRecord.index, isActivatable);
|
||||
const prev = getPrevItem(
|
||||
items,
|
||||
activeItemRecord.index,
|
||||
isActivatable,
|
||||
wrap,
|
||||
);
|
||||
if (prev) {
|
||||
prev.tabIndex = 0;
|
||||
prev.focus();
|
||||
|
@ -226,6 +226,14 @@ export abstract class Menu extends LitElement {
|
||||
@property({attribute: 'default-focus'})
|
||||
defaultFocus: FocusState = FocusState.FIRST_ITEM;
|
||||
|
||||
/**
|
||||
* Turns off navigation wrapping. By default, navigating past the end of the
|
||||
* menu items will wrap focus back to the beginning and vice versa. Use this
|
||||
* for ARIA patterns that do not wrap focus, like combobox.
|
||||
*/
|
||||
@property({type: Boolean, attribute: 'no-navigation-wrap'})
|
||||
noNavigationWrap = false;
|
||||
|
||||
@queryAssignedElements({flatten: true}) protected slotItems!: HTMLElement[];
|
||||
@state() private typeaheadActive = true;
|
||||
|
||||
@ -282,6 +290,7 @@ export abstract class Menu extends LitElement {
|
||||
|
||||
return submenuNavKeys.has(key);
|
||||
},
|
||||
wrapNavigation: () => !this.noNavigationWrap,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -474,6 +474,7 @@ export abstract class Select extends selectBaseClass {
|
||||
? `${this.selectWidth}px`
|
||||
: undefined,
|
||||
})}
|
||||
no-navigation-wrap
|
||||
.open=${this.open}
|
||||
.quick=${this.quick}
|
||||
.positioning=${this.menuPositioning}
|
||||
|
Loading…
Reference in New Issue
Block a user