UNPKG

@acusti/dropdown

Version:

React component that renders a dropdown with a trigger and supports searching, keyboard access, and more

130 lines (129 loc) 5.32 kB
import { getBestMatch } from '@acusti/matchmaking'; import { BODY_SELECTOR } from './styles.js'; export const ITEM_SELECTOR = `[data-ukt-item], [data-ukt-value]`; export const getItemElements = (dropdownElement) => { if (!dropdownElement) return null; const bodyElement = dropdownElement.querySelector(BODY_SELECTOR); if (!bodyElement) return null; let items = bodyElement.querySelectorAll(ITEM_SELECTOR); if (items.length) return items; // If no items found via [data-ukt-item] or [data-ukt-value] selector, // use first instance of multiple children found items = bodyElement.children; while (items.length === 1) { if (items[0].children == null) break; items = items[0].children; } // If unable to find an element with more than one child, treat direct child as items if (items.length === 1) { items = bodyElement.children; } return items; }; export const getActiveItemElement = (dropdownElement) => { if (!dropdownElement) return null; return dropdownElement.querySelector('[data-ukt-active]'); }; const clearItemElementsState = (itemElements) => { itemElements.forEach((itemElement) => { if (itemElement.hasAttribute('data-ukt-active')) { delete itemElement.dataset.uktActive; } }); }; export const setActiveItem = ({ dropdownElement, element, index, indexAddend, isExactMatch, text, }) => { const items = getItemElements(dropdownElement); if (!items) return; const itemElements = Array.from(items); if (!itemElements.length) return; const lastIndex = itemElements.length - 1; const currentActiveIndex = itemElements.findIndex((itemElement) => itemElement.hasAttribute('data-ukt-active'), ); let nextActiveIndex = currentActiveIndex; if (typeof index === 'number') { // Negative index means count back from the end nextActiveIndex = index < 0 ? itemElements.length + index : index; } if (element) { nextActiveIndex = itemElements.findIndex( (itemElement) => itemElement === element, ); } else if (typeof indexAddend === 'number') { // If there’s no currentActiveIndex and we are handling -1, start at lastIndex if (currentActiveIndex === -1 && indexAddend === -1) { nextActiveIndex = lastIndex; } else { nextActiveIndex += indexAddend; } // Keep it within the bounds of the items list if (nextActiveIndex < 0) { nextActiveIndex = 0; } else if (nextActiveIndex > lastIndex) { nextActiveIndex = lastIndex; } } else if (typeof text === 'string') { // If text is empty, clear existing active items and early return if (!text) { clearItemElementsState(itemElements); return; } const itemTexts = itemElements.map((itemElement) => itemElement.innerText); if (isExactMatch) { const textToCompare = text.toLowerCase(); nextActiveIndex = itemTexts.findIndex((itemText) => itemText.toLowerCase().startsWith(textToCompare), ); // If isExactMatch is required and no exact match was found, clear active items if (nextActiveIndex === -1) { clearItemElementsState(itemElements); } } else { const bestMatch = getBestMatch({ items: itemTexts, text }); nextActiveIndex = itemTexts.findIndex((itemText) => itemText === bestMatch); } } if (nextActiveIndex === -1 || nextActiveIndex === currentActiveIndex) return; // Clear any existing active dropdown body item state clearItemElementsState(itemElements); const nextActiveItem = items[nextActiveIndex]; if (nextActiveItem != null) { nextActiveItem.setAttribute('data-ukt-active', ''); // Find closest scrollable parent and ensure that next active item is visible let { parentElement } = nextActiveItem; let scrollableParent = null; while (!scrollableParent && parentElement && parentElement !== dropdownElement) { const isScrollable = parentElement.scrollHeight > parentElement.clientHeight + 15; if (isScrollable) { scrollableParent = parentElement; } else { parentElement = parentElement.parentElement; } } if (scrollableParent) { const parentRect = scrollableParent.getBoundingClientRect(); const itemRect = nextActiveItem.getBoundingClientRect(); const isAboveTop = itemRect.top < parentRect.top; const isBelowBottom = itemRect.bottom > parentRect.bottom; if (isAboveTop || isBelowBottom) { let { scrollTop } = scrollableParent; // Item isn’t fully visible; adjust scrollTop to put item within closest edge if (isAboveTop) { scrollTop -= parentRect.top - itemRect.top; } else { scrollTop += itemRect.bottom - parentRect.bottom; } scrollableParent.scrollTop = scrollTop; } } } }; //# sourceMappingURL=helpers.js.map