/**
* @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);
event.preventDefault();
const isRtl = this.isRtl();
const inlinePrevious = isRtl
? NavigableKeys.ArrowRight
: NavigableKeys.ArrowLeft;
const inlineNext = isRtl
? NavigableKeys.ArrowLeft
: NavigableKeys.ArrowRight;
let nextActiveItem = null;
switch (key) {
// Activate the next item
case NavigableKeys.ArrowDown:
case inlineNext:
nextActiveItem = activateNextItem(items, activeItemRecord, this.isActivatable, this.wrapNavigation());
break;
// Activate the previous item
case NavigableKeys.ArrowUp:
case inlinePrevious:
nextActiveItem = activatePreviousItem(items, activeItemRecord, this.isActivatable, this.wrapNavigation());
break;
// Activate the first item
case NavigableKeys.Home:
nextActiveItem = activateFirstItem(items, this.isActivatable);
break;
// Activate the last item
case NavigableKeys.End:
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;
}
};
/**
* 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, wrapNavigation, } = config;
this.isItem = isItem;
this.getPossibleItems = getPossibleItems;
this.isRtl = isRtl;
this.deactivateItem = deactivateItem;
this.activateItem = activateItem;
this.isNavigableKey = isNavigableKey;
this.isActivatable = isActivatable;
this.wrapNavigation = wrapNavigation ?? (() => true);
}
/**
* 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, this.wrapNavigation());
}
/**
* 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, this.wrapNavigation());
}
}
//# sourceMappingURL=list-controller.js.map