UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

177 lines (176 loc) 6.43 kB
"use client"; import React, { useCallback, useRef, useState } from 'react'; import clsx from 'clsx'; import { useMenuContext } from "./MenuContext.js"; import useIsomorphicLayoutEffect from "../../shared/helpers/useIsomorphicLayoutEffect.js"; import { jsx as _jsx } from "react/jsx-runtime"; export default function MenuList(props) { const { children, className, maxVisibleListItems, style, ...rest } = props; const context = useMenuContext(); const ulRef = useRef(null); const hasValidMaxVisibleListItems = typeof maxVisibleListItems === 'number' && Number.isFinite(maxVisibleListItems) && maxVisibleListItems > 0; const fallbackMaxHeight = hasValidMaxVisibleListItems ? `calc(var(--menu-action-min-height, 2.5rem) * ${maxVisibleListItems} + var(--menu-content-padding, 0.25rem) * 2)` : undefined; const [measuredMaxHeight, setMeasuredMaxHeight] = useState(undefined); const measureMaxHeight = useCallback(() => { if (!hasValidMaxVisibleListItems || style !== null && style !== void 0 && style.maxHeight) { setMeasuredMaxHeight(undefined); return; } const height = getVisibleMenuItemsHeight(ulRef.current, maxVisibleListItems); setMeasuredMaxHeight(height ? `${height}px` : undefined); }, [hasValidMaxVisibleListItems, maxVisibleListItems, style === null || style === void 0 ? void 0 : style.maxHeight]); useIsomorphicLayoutEffect(() => { measureMaxHeight(); }, [children, measureMaxHeight]); useIsomorphicLayoutEffect(() => { if (!hasValidMaxVisibleListItems || style !== null && style !== void 0 && style.maxHeight) { return undefined; } window.addEventListener('resize', measureMaxHeight); return () => { window.removeEventListener('resize', measureMaxHeight); }; }, [hasValidMaxVisibleListItems, measureMaxHeight, style === null || style === void 0 ? void 0 : style.maxHeight]); useIsomorphicLayoutEffect(() => { if (context !== null && context !== void 0 && context.menuRef) { context.menuRef.current = ulRef.current; } }); const getNavigableItems = useCallback(() => { const menuEl = ulRef.current; if (!menuEl) { return []; } return Array.from(menuEl.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')); }, []); const focusByDomOrder = useCallback(element => { if (!context) { return; } element.focus({ preventScroll: true }); const refIndex = context.itemRefs.current.findIndex(r => (r === null || r === void 0 ? void 0 : r.current) === element); if (refIndex !== -1) { context.setActiveIndex(refIndex); } }, [context]); const handleKeyDown = useCallback(event => { if (!context) { return; } const items = getNavigableItems(); if (items.length === 0) { return; } let currentIdx = items.indexOf(document.activeElement); if (currentIdx === -1 && context.activeIndex >= 0) { const activeRef = context.itemRefs.current[context.activeIndex]; if (activeRef !== null && activeRef !== void 0 && activeRef.current) { currentIdx = items.indexOf(activeRef.current); } } switch (event.key) { case 'ArrowDown': { event.preventDefault(); event.stopPropagation(); if (currentIdx === -1) { focusByDomOrder(items[0]); } else { focusByDomOrder(items[(currentIdx + 1) % items.length]); } break; } case 'ArrowUp': { event.preventDefault(); event.stopPropagation(); if (currentIdx === -1) { focusByDomOrder(items[items.length - 1]); } else { focusByDomOrder(items[(currentIdx - 1 + items.length) % items.length]); } break; } case 'Home': case 'PageUp': { event.preventDefault(); event.stopPropagation(); focusByDomOrder(items[0]); break; } case 'End': case 'PageDown': { event.preventDefault(); event.stopPropagation(); focusByDomOrder(items[items.length - 1]); break; } case 'Escape': case 'Tab': { break; } default: { if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) { const char = event.key.toLowerCase(); const startIdx = currentIdx === -1 ? 0 : (currentIdx + 1) % items.length; for (let i = 0; i < items.length; i++) { var _items$idx$textConten; const idx = (startIdx + i) % items.length; const text = (_items$idx$textConten = items[idx].textContent) === null || _items$idx$textConten === void 0 ? void 0 : _items$idx$textConten.trim().toLowerCase(); if (text !== null && text !== void 0 && text.startsWith(char)) { focusByDomOrder(items[idx]); break; } } } } } }, [context, getNavigableItems, focusByDomOrder]); const resolvedMaxHeight = style !== null && style !== void 0 && style.maxHeight ? undefined : measuredMaxHeight || fallbackMaxHeight; const listStyle = { ...(resolvedMaxHeight ? { maxHeight: resolvedMaxHeight, overflowY: 'auto' } : null), ...style }; return _jsx("ul", { ref: ulRef, role: "menu", tabIndex: -1, className: clsx("dnb-menu__list dnb-no-focus", className), style: Object.keys(listStyle).length > 0 ? listStyle : undefined, onKeyDown: handleKeyDown, ...rest, children: children }); } function getVisibleMenuItemsHeight(ulElement, maxVisibleListItems) { if (!ulElement) { return null; } const items = Array.from(ulElement.children).filter(element => element instanceof HTMLElement); const firstVisibleItem = items[0]; const lastVisibleItem = items[maxVisibleListItems - 1]; if (!firstVisibleItem || !lastVisibleItem) { return null; } const contentHeight = Math.ceil(lastVisibleItem.offsetTop + lastVisibleItem.offsetHeight - firstVisibleItem.offsetTop); const computedStyle = getComputedStyle(ulElement); const paddingTop = parseFloat(computedStyle.paddingTop) || 0; const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; return contentHeight + paddingTop + paddingBottom; } //# sourceMappingURL=MenuList.js.map