UNPKG

@hashicorp/design-system-components

Version:
269 lines (266 loc) 8.32 kB
import Component from '@glimmer/component'; import { modifier } from 'ember-modifier'; import { hash } from '@ember/helper'; import { guidFor } from '@ember/object/internals'; import { findFirstEnabled, handleKey } from './navigation.js'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@ember/component'; function getElementId(element, prefix) { if (element.id !== '') { return element.id; } return `${prefix}-${guidFor(element)}`; } function sortByDOMPosition(items) { const indexedItems = items.map((item, index) => ({ item, index })); return indexedItems.slice().sort((leftEntry, rightEntry) => { const { item: left, index: leftIndex } = leftEntry; const { item: right, index: rightIndex } = rightEntry; if (!left.element.isConnected && right.element.isConnected) { return 1; } if (left.element.isConnected && !right.element.isConnected) { return -1; } const position = left.element.compareDocumentPosition(right.element); if ((position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) { return -1; } if ((position & Node.DOCUMENT_POSITION_PRECEDING) !== 0) { return 1; } // keep sort stable when DOM position cannot be resolved return leftIndex - rightIndex; }).map(entry => entry.item); } function findGroupId(element, groups) { for (const group of groups) { if (group.element.contains(element)) { return group.id; } } return undefined; } class Composite extends Component { // not tracked because registration happens inside modifier installation _items = []; _groups = []; // undefined = uninitialized (auto-select first enabled on registration) // null = explicitly no active item (container gets tabindex="0") // string = active item id _currentId = undefined; _compositeElement = null; _preserveTabIndex = false; constructor(owner, args) { super(owner, args); if (args.defaultCurrentId !== undefined) { this._currentId = args.defaultCurrentId; } } get _resolvedCurrentId() { // explicitly null (no active item) if (this._currentId === null) { return null; } const requestedItem = this._items.find(item => item.id === this._currentId); if (requestedItem !== undefined && requestedItem.disabled === false) { return this._currentId; } return this._firstEnabledItem !== undefined ? this._firstEnabledItem.id : undefined; } get _currentItem() { const activeId = this._resolvedCurrentId; if (activeId === null || activeId === undefined) { return undefined; } return this._items.find(item => item.id === activeId); } get _firstEnabledItem() { return findFirstEnabled(this._items); } get _currentIndex() { const activeId = this._resolvedCurrentId; if (activeId === null || activeId === undefined) { return -1; } return this._items.findIndex(item => item.id === activeId); } get _config() { return { orientation: this.args.orientation, loop: this.args.loop ?? false, wrap: this.args.wrap ?? false }; } get _navigationSnapshot() { return { items: this._items, groups: this._groups, currentItem: this._currentItem, currentIndex: this._currentIndex }; } _registerItem(newItem) { newItem.groupId = findGroupId(newItem.element, this._groups); this._items = sortByDOMPosition([...this._items.filter(item => item.id !== newItem.id), newItem]); this._syncAllElements(); } _unregisterItem(id) { const wasCurrent = this._resolvedCurrentId === id; this._items = this._items.filter(item => item.id !== id); if (wasCurrent) { // reset to "auto" so newly rendered first items can become active this._currentId = undefined; this._syncAllElements(); return; } this._syncAllElements(); } _registerGroup(newGroup) { this._groups = sortByDOMPosition([...this._groups.filter(group => group.id !== newGroup.id), newGroup]); this._reassociateGroups(); } _unregisterGroup(id) { this._groups = this._groups.filter(group => group.id !== id); this._reassociateGroups(); const currentExists = this._items.some(item => item.id === this._resolvedCurrentId); if (!currentExists && this._firstEnabledItem) { // reset to "auto" so group reordering/mount order can settle first this._currentId = undefined; this._syncAllElements(); } } _reassociateGroups() { for (const item of this._items) { item.groupId = findGroupId(item.element, this._groups); } } _moveTo(id) { if (this._currentId === id) { return; } const previousItem = this._currentItem; this._currentId = id; const nextItem = this._currentItem; if (previousItem !== undefined) { this._syncItemElement(previousItem); } if (nextItem !== undefined) { this._syncItemElement(nextItem); } this._syncCompositeElement(); } _syncAllElements() { for (const item of this._items) { this._syncItemElement(item); } this._syncCompositeElement(); } _syncItemElement(item) { const element = item.element; const isCurrent = item.id === this._resolvedCurrentId; if (item.disabled === true) { element.setAttribute('aria-disabled', 'true'); element.setAttribute('disabled', ''); element.setAttribute('tabindex', '-1'); } else { element.removeAttribute('aria-disabled'); element.removeAttribute('disabled'); element.setAttribute('tabindex', isCurrent === true ? '0' : '-1'); } if (isCurrent === true) { element.setAttribute('data-active-item', ''); } else { element.removeAttribute('data-active-item'); } element.setAttribute('tabindex', isCurrent === true ? '0' : '-1'); } _syncCompositeElement() { const element = this._compositeElement; if (element === null || this._preserveTabIndex === true) { return; } const activeId = this._resolvedCurrentId; if (activeId === null || activeId === undefined) { element.setAttribute('tabindex', '0'); } else { element.removeAttribute('tabindex'); } } compositeModifier = modifier(element => { this._compositeElement = element; this._preserveTabIndex = element.hasAttribute('tabindex'); this._syncCompositeElement(); const onKeyDown = event => { const managedKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown']; if (managedKeys.includes(event.key)) { event.preventDefault(); } const target = handleKey(event, this._navigationSnapshot, this._config); if (target === undefined) { return; } this._moveTo(target.id); target.element.focus(); }; element.addEventListener('keydown', onKeyDown); return () => { element.removeEventListener('keydown', onKeyDown); if (this._compositeElement === element) { this._compositeElement = null; } }; }); itemModifier = modifier((element, _positional, named) => { const disabled = named.disabled ?? false; const id = getElementId(element, 'composite-item'); element.id = id; this._registerItem({ id, element, disabled }); const onFocus = () => { const item = this._items.find(registered => registered.id === id); if (item === undefined || item.disabled === true) { return; } this._moveTo(id); }; element.addEventListener('focus', onFocus); return () => { element.removeEventListener('focus', onFocus); this._unregisterItem(id); }; }); groupModifier = modifier(element => { const id = getElementId(element, 'composite-group'); element.id = id; this._registerGroup({ id, element }); return () => { this._unregisterGroup(id); }; }); static { setComponentTemplate(precompileTemplate("{{yield (hash composite=this.compositeModifier item=this.itemModifier group=this.groupModifier)}}", { strictMode: true, scope: () => ({ hash }) }), this); } } export { Composite as default }; //# sourceMappingURL=index.js.map