@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
240 lines • 10.4 kB
JavaScript
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