@hashicorp/design-system-components
Version:
Helios Design System Components
269 lines (266 loc) • 8.32 kB
JavaScript
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