UNPKG

@material/web

Version:
180 lines 6.66 kB
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { activateFirstItem, activateLastItem, activateNextItem, activatePreviousItem, getActiveItem, getFirstActivatableItem, } from './list-navigation-helpers.js'; // TODO: move this file to List and make List use this /** * Default keys that trigger navigation. */ // tslint:disable:enforce-name-casing Following Enum style export const NavigableKeys = { ArrowDown: 'ArrowDown', ArrowLeft: 'ArrowLeft', ArrowUp: 'ArrowUp', ArrowRight: 'ArrowRight', Home: 'Home', End: 'End', }; /** * A controller that handles list keyboard navigation and item management. */ export class ListController { constructor(config) { /** * Handles keyboard navigation. Should be bound to the node that will act as * the List. */ this.handleKeydown = (event) => { const key = event.key; if (event.defaultPrevented || !this.isNavigableKey(key)) { return; } // do not use this.items directly in upcoming calculations so we don't // re-query the DOM unnecessarily const items = this.items; if (!items.length) { return; } const activeItemRecord = getActiveItem(items, this.isActivatable); if (activeItemRecord) { activeItemRecord.item.tabIndex = -1; } event.preventDefault(); const isRtl = this.isRtl(); const inlinePrevious = isRtl ? NavigableKeys.ArrowRight : NavigableKeys.ArrowLeft; const inlineNext = isRtl ? NavigableKeys.ArrowLeft : NavigableKeys.ArrowRight; switch (key) { // Activate the next item case NavigableKeys.ArrowDown: case inlineNext: activateNextItem(items, activeItemRecord, this.isActivatable); break; // Activate the previous item case NavigableKeys.ArrowUp: case inlinePrevious: activatePreviousItem(items, activeItemRecord, this.isActivatable); break; // Activate the first item case NavigableKeys.Home: activateFirstItem(items, this.isActivatable); break; // Activate the last item case NavigableKeys.End: activateLastItem(items, this.isActivatable); break; default: break; } }; /** * Listener to be bound to the `deactivate-items` item event. */ this.onDeactivateItems = () => { const items = this.items; for (const item of items) { this.deactivateItem(item); } }; /** * Listener to be bound to the `request-activation` item event.. */ this.onRequestActivation = (event) => { this.onDeactivateItems(); const target = event.target; this.activateItem(target); target.focus(); }; /** * Listener to be bound to the `slotchange` event for the slot that renders * the items. */ this.onSlotchange = () => { const items = this.items; // Whether we have encountered an item that has been activated let encounteredActivated = false; for (const item of items) { const isActivated = !item.disabled && item.tabIndex > -1; if (isActivated && !encounteredActivated) { encounteredActivated = true; item.tabIndex = 0; continue; } // Deactivate the rest including disabled item.tabIndex = -1; } if (encounteredActivated) { return; } const firstActivatableItem = getFirstActivatableItem(items, this.isActivatable); if (!firstActivatableItem) { return; } firstActivatableItem.tabIndex = 0; }; const { isItem, getPossibleItems, isRtl, deactivateItem, activateItem, isNavigableKey, isActivatable, } = config; this.isItem = isItem; this.getPossibleItems = getPossibleItems; this.isRtl = isRtl; this.deactivateItem = deactivateItem; this.activateItem = activateItem; this.isNavigableKey = isNavigableKey; this.isActivatable = isActivatable; } /** * The items being managed by the list. Additionally, attempts to see if the * object has a sub-item in the `.item` property. */ get items() { const maybeItems = this.getPossibleItems(); const items = []; for (const itemOrParent of maybeItems) { const isItem = this.isItem(itemOrParent); // if the item is a list item, add it to the list of items if (isItem) { items.push(itemOrParent); continue; } // If the item exposes an `item` property check if it is a list item. const subItem = itemOrParent.item; if (subItem && this.isItem(subItem)) { items.push(subItem); } } return items; } /** * Activates the next item in the list. If at the end of the list, the first * item will be activated. * * @return The activated list item or `null` if there are no items. */ activateNextItem() { const items = this.items; const activeItemRecord = getActiveItem(items, this.isActivatable); if (activeItemRecord) { activeItemRecord.item.tabIndex = -1; } return activateNextItem(items, activeItemRecord, this.isActivatable); } /** * Activates the previous item in the list. If at the start of the list, the * last item will be activated. * * @return The activated list item or `null` if there are no items. */ activatePreviousItem() { const items = this.items; const activeItemRecord = getActiveItem(items, this.isActivatable); if (activeItemRecord) { activeItemRecord.item.tabIndex = -1; } return activatePreviousItem(items, activeItemRecord, this.isActivatable); } } //# sourceMappingURL=list-controller.js.map