UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

240 lines 10.4 kB
import { createComponent, Shade } from '@furystack/shades'; import { buildTransition, cssVariableTheme } from '../../services/css-variable-theme.js'; import { getNavigableKeys } from './menu-types.js'; const renderItems = (items, options) => { return items.map((item) => { if (item.type === 'divider') { return createComponent("div", { role: "separator", className: "menu-divider" }); } if (item.type === 'group') { const isExpanded = !options.isInline || options.expandedGroups.includes(item.key); return (createComponent("div", { role: "group", "aria-label": item.label, className: "menu-group", "data-group-key": item.key }, createComponent("div", { className: `menu-group-label${options.isInline ? ' menu-group-label-inline' : ''}`, onclick: options.isInline ? () => { options.onToggleGroup(item.key); } : undefined }, createComponent("span", null, item.label), options.isInline ? (createComponent("span", { className: `menu-group-arrow${isExpanded ? ' expanded' : ''}` }, "\u25B8")) : null), createComponent("div", { className: "menu-group-children", style: { display: isExpanded ? '' : 'none' } }, renderItems(item.children, options)))); } const isSelected = options.selectedKey === item.key; const isFocused = options.focusedKey === item.key; const classNames = [ 'menu-item', isSelected ? 'selected' : '', item.disabled ? 'disabled' : '', isFocused ? 'focused' : '', ] .filter(Boolean) .join(' '); return (createComponent("div", { role: "menuitem", className: classNames, "aria-disabled": item.disabled ? 'true' : undefined, "aria-current": isSelected ? 'true' : undefined, "data-key": item.key, tabIndex: -1, onclick: () => { if (!item.disabled) { options.onSelect?.(item.key); } }, onmouseenter: () => { if (!item.disabled) { options.setFocusedKey(item.key); } } }, item.icon && createComponent("span", { className: "menu-item-icon" }, item.icon), createComponent("span", { className: "menu-item-label" }, item.label))); }); }; export const Menu = Shade({ customElementName: 'shade-menu', css: { display: 'flex', outline: 'none', listStyle: 'none', margin: '0', padding: `${cssVariableTheme.spacing.xs} 0`, fontFamily: cssVariableTheme.typography.fontFamily, fontSize: cssVariableTheme.typography.fontSize.md, color: cssVariableTheme.text.primary, '&[data-mode="horizontal"]': { flexDirection: 'row', alignItems: 'center', padding: '0', gap: '2px', }, '&[data-mode="vertical"], &:not([data-mode])': { flexDirection: 'column', }, '&[data-mode="inline"]': { flexDirection: 'column', }, // Menu item '& .menu-item': { display: 'flex', alignItems: 'center', gap: cssVariableTheme.spacing.sm, padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.md}`, cursor: 'pointer', userSelect: 'none', borderRadius: cssVariableTheme.shape.borderRadius.sm, transition: buildTransition(['background-color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default]), whiteSpace: 'nowrap', }, '& .menu-item:hover:not(.disabled), & .menu-item.focused:not(.disabled)': { backgroundColor: cssVariableTheme.action.hoverBackground, }, '& .menu-item.selected': { color: cssVariableTheme.palette.primary.main, backgroundColor: `color-mix(in srgb, ${cssVariableTheme.palette.primary.main} 10%, transparent)`, }, '& .menu-item.disabled': { opacity: '0.5', cursor: 'not-allowed', }, '& .menu-item-icon': { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: '0', width: '20px', }, '& .menu-item-label': { flex: '1', }, // Horizontal mode item tweaks '&[data-mode="horizontal"] .menu-item': { padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.md}`, borderRadius: cssVariableTheme.shape.borderRadius.md, }, '&[data-mode="horizontal"] .menu-item.selected': { boxShadow: `inset 0 -2px 0 ${cssVariableTheme.palette.primary.main}`, borderRadius: '0', }, // Divider '& .menu-divider': { height: '1px', margin: `${cssVariableTheme.spacing.xs} ${cssVariableTheme.spacing.sm}`, backgroundColor: cssVariableTheme.divider, }, '&[data-mode="horizontal"] .menu-divider': { width: '1px', height: 'auto', alignSelf: 'stretch', margin: '4px 2px', }, // Group '& .menu-group-label': { display: 'flex', alignItems: 'center', padding: `${cssVariableTheme.spacing.xs} ${cssVariableTheme.spacing.md}`, fontSize: cssVariableTheme.typography.fontSize.xs, fontWeight: cssVariableTheme.typography.fontWeight.bold, color: cssVariableTheme.text.secondary, textTransform: 'uppercase', letterSpacing: cssVariableTheme.typography.letterSpacing.wider, userSelect: 'none', }, '& .menu-group-label-inline': { cursor: 'pointer', justifyContent: 'space-between', padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.md}`, fontSize: cssVariableTheme.typography.fontSize.md, fontWeight: cssVariableTheme.typography.fontWeight.medium, textTransform: 'none', letterSpacing: 'normal', color: cssVariableTheme.text.primary, }, '& .menu-group-label-inline:hover': { backgroundColor: cssVariableTheme.action.hoverBackground, borderRadius: cssVariableTheme.shape.borderRadius.sm, }, '& .menu-group-arrow': { display: 'inline-block', transition: buildTransition([ 'transform', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default, ]), }, '& .menu-group-arrow.expanded': { transform: 'rotate(90deg)', }, '& .menu-group-children': { display: 'flex', flexDirection: 'column', }, '&[data-mode="inline"] .menu-group-children': { paddingLeft: cssVariableTheme.spacing.md, }, }, render: ({ props, useState, useHostProps }) => { const { items, mode = 'vertical', selectedKey, onSelect } = props; const [focusedKey, setFocusedKey] = useState('focusedKey', ''); const [expandedGroups, setExpandedGroups] = useState('expandedGroups', []); useHostProps({ role: mode === 'horizontal' ? 'menubar' : 'menu', 'data-mode': mode, tabIndex: 0, }); useHostProps({ onkeydown: (ev) => { const navigableKeys = getNavigableKeys(items); if (navigableKeys.length === 0) return; const isHorizontal = mode === 'horizontal'; const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown'; const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp'; switch (ev.key) { case nextKey: { ev.preventDefault(); const currentIndex = navigableKeys.indexOf(focusedKey); const nextIndex = currentIndex < navigableKeys.length - 1 ? currentIndex + 1 : 0; setFocusedKey(navigableKeys[nextIndex]); break; } case prevKey: { ev.preventDefault(); const currentIndex = navigableKeys.indexOf(focusedKey); const prevIndex = currentIndex > 0 ? currentIndex - 1 : navigableKeys.length - 1; setFocusedKey(navigableKeys[prevIndex]); break; } case 'Home': { ev.preventDefault(); setFocusedKey(navigableKeys[0]); break; } case 'End': { ev.preventDefault(); setFocusedKey(navigableKeys[navigableKeys.length - 1]); break; } case 'Enter': case ' ': { ev.preventDefault(); if (focusedKey) { onSelect?.(focusedKey); } break; } default: break; } }, }); const handleToggleGroup = (key) => { if (expandedGroups.includes(key)) { setExpandedGroups(expandedGroups.filter((k) => k !== key)); } else { setExpandedGroups([...expandedGroups, key]); } }; return (createComponent("div", { style: { display: 'contents' } }, renderItems(items, { selectedKey, focusedKey, expandedGroups, onSelect, setFocusedKey, onToggleGroup: handleToggleGroup, isInline: mode === 'inline', }))); }, }); //# sourceMappingURL=menu.js.map