@zendeskgarden/container-menu
Version:
Containers relating to Menu in the Garden Design System
970 lines (963 loc) • 31 kB
JavaScript
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/
'use strict';
var React = require('react');
var containerSelection = require('@zendeskgarden/container-selection');
var containerUtilities = require('@zendeskgarden/container-utilities');
var PropTypes = require('prop-types');
const triggerLink = (element, view) => {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view
});
element.dispatchEvent(event);
};
const StateChangeTypes = {
FnInternalUpdate: 'fn:internalUpdate',
FnMenuTransitionFinish: 'fn:menuTransitionFinish',
TriggerClick: 'trigger:click',
TriggerKeyDownEnter: `trigger:keyDown:${containerUtilities.KEYS.ENTER}`,
TriggerKeyDownSpace: `trigger:keyDown:Space`,
TriggerKeyDownArrowDown: `trigger:keyDown:${containerUtilities.KEYS.DOWN}`,
TriggerKeyDownArrowUp: `trigger:keyDown:${containerUtilities.KEYS.UP}`,
MenuKeyDownEscape: `menu:keyDown:${containerUtilities.KEYS.ESCAPE}`,
MenuKeyDownTab: `menu:keyDown:${containerUtilities.KEYS.TAB}`,
MenuBlur: 'menu:blur',
MenuMouseLeave: 'menu:mouseLeave',
MenuItemClick: 'menuItem:click',
MenuItemClickPrevious: `menuItem:click:previous`,
MenuItemClickNext: `menuItem:click:next`,
MenuItemMouseMove: `menuItem:mouseMove`,
MenuItemKeyDown: 'menuItem:keyDown',
MenuItemKeyDownPrevious: `menuItem:keyDown:previous`,
MenuItemKeyDownNext: `menuItem:keyDown:next`,
MenuItemKeyDownEnter: `menuItem:keyDown:${containerUtilities.KEYS.ENTER}`,
MenuItemKeyDownSpace: `menuItem:keyDown:Space`,
MenuItemKeyDownArrowUp: `menuItem:keyDown:${containerUtilities.KEYS.UP}`,
MenuItemKeyDownArrowDown: `menuItem:keyDown:${containerUtilities.KEYS.DOWN}`,
MenuItemKeyDownHome: `menuItem:keyDown:${containerUtilities.KEYS.HOME}`,
MenuItemKeyDownEnd: `menuItem:keyDown:${containerUtilities.KEYS.END}`
};
const isItemGroup = item => Object.hasOwn(item, 'items');
const isValidItem = item => !item.disabled && !item.separator && !isItemGroup(item);
const toMenuItemKeyDownType = type => `MenuItemKeyDown${type === containerUtilities.KEYS.SPACE ? 'Space' : type}`;
const getStateChanges = changes => {
let retVal = null;
for (const change in changes) {
if (changes[change] === undefined) continue;
retVal ||= {};
retVal[change] = changes[change];
}
return retVal;
};
const stateReducer = (state, action) => {
let changes = null;
switch (action.type) {
case StateChangeTypes.MenuBlur:
case StateChangeTypes.MenuKeyDownEscape:
case StateChangeTypes.MenuKeyDownTab:
case StateChangeTypes.TriggerClick:
case StateChangeTypes.TriggerKeyDownEnter:
case StateChangeTypes.TriggerKeyDownSpace:
case StateChangeTypes.TriggerKeyDownArrowDown:
case StateChangeTypes.TriggerKeyDownArrowUp:
{
const {
focusOnOpen,
focusedValue,
isExpanded
} = action.payload;
const stateChanges = getStateChanges({
focusOnOpen,
focusedValue,
isExpanded
});
if (stateChanges) {
changes = {
...state,
...stateChanges
};
}
break;
}
case StateChangeTypes.MenuItemClick:
case StateChangeTypes.MenuItemClickNext:
case StateChangeTypes.MenuItemClickPrevious:
case StateChangeTypes.MenuItemKeyDownSpace:
case StateChangeTypes.MenuItemKeyDownEnter:
{
const {
selectedItems,
isExpanded,
nestedPathIds,
transitionType,
isTransitionNext,
isTransitionPrevious
} = action.payload;
const stateChanges = getStateChanges({
selectedItems,
isExpanded,
nestedPathIds,
transitionType,
isTransitionNext,
isTransitionPrevious
});
if (stateChanges) {
changes = {
...state,
...stateChanges
};
}
break;
}
case StateChangeTypes.MenuItemKeyDownArrowUp:
case StateChangeTypes.MenuItemKeyDownArrowDown:
case StateChangeTypes.MenuItemKeyDownNext:
case StateChangeTypes.MenuItemKeyDownPrevious:
case StateChangeTypes.MenuItemKeyDownHome:
case StateChangeTypes.MenuItemKeyDownEnd:
case StateChangeTypes.MenuItemKeyDown:
case StateChangeTypes.MenuItemMouseMove:
{
const {
focusedValue,
nestedPathIds,
transitionType,
isTransitionNext,
isTransitionPrevious
} = action.payload;
const stateChanges = getStateChanges({
focusedValue,
nestedPathIds,
transitionType,
isTransitionNext,
isTransitionPrevious
});
if (stateChanges) {
changes = {
...state,
...stateChanges
};
}
break;
}
case StateChangeTypes.FnMenuTransitionFinish:
{
const {
focusOnOpen,
focusedValue,
nestedPathIds,
valuesRef
} = action.payload;
const stateChanges = getStateChanges({
focusOnOpen,
focusedValue,
nestedPathIds,
valuesRef
});
if (stateChanges) {
changes = {
...state,
...stateChanges,
transitionType: null,
isTransitionNext: false,
isTransitionPrevious: false
};
}
break;
}
case StateChangeTypes.FnInternalUpdate:
{
const {
...props
} = action.payload;
changes = {
...state,
...props
};
break;
}
default:
{
throw new Error('Error: unexpected menu action provided: ', action.type);
}
}
return changes || state;
};
const useMenu = _ref => {
let {
items: rawItems,
idPrefix,
environment,
menuRef,
triggerRef,
rtl = false,
onChange = () => undefined,
isExpanded,
defaultExpanded = false,
restoreFocus = true,
selectedItems,
focusedValue,
defaultFocusedValue
} = _ref;
const prefix = `${containerUtilities.useId(idPrefix)}-`;
const triggerId = `${prefix}menu-trigger`;
const isExpandedControlled = isExpanded !== undefined;
const isSelectedItemsControlled = selectedItems !== undefined;
const isFocusedValueControlled = focusedValue !== undefined;
const menuItems = React.useMemo(() => rawItems.reduce((items, item) => {
if (isItemGroup(item)) {
const nestedItems = item.items.filter(isValidItem);
items.push(...nestedItems);
} else if (isValidItem(item)) {
items.push(item);
}
return items;
}, []), [rawItems]);
const initialSelectedItems = React.useMemo(() => menuItems.filter(item => {
return !!((item.href || item.type === 'radio' || item.type === 'checkbox') && item.selected);
}), [menuItems]);
const values = React.useMemo(() => menuItems.map(item => item.value), [menuItems]);
const itemRefs = React.useMemo(() => values.reduce((acc, v) => {
acc[v] = React.createRef();
return acc;
}, {}), [values]);
const [menuVisible, setMenuVisible] = React.useState(false);
const [state, dispatch] = React.useReducer(stateReducer, {
focusedValue,
isExpanded: isExpanded || defaultExpanded,
selectedItems: initialSelectedItems,
valuesRef: values,
focusOnOpen: false,
isTransitionNext: false,
isTransitionPrevious: false,
transitionType: null,
nestedPathIds: []
});
const controlledIsExpanded = containerUtilities.getControlledValue(isExpanded, state.isExpanded);
const controlledSelectedItems = containerUtilities.getControlledValue(selectedItems, state.selectedItems);
const uncontrolledFocusedValue = state.focusedValue === null ? undefined : state.focusedValue;
const {
focusedValue: controlledFocusedValue,
getGroupProps,
getElementProps
} = containerSelection.useSelection({
values,
direction: 'vertical',
selectedValue: focusedValue || uncontrolledFocusedValue,
focusedValue: focusedValue || uncontrolledFocusedValue,
allowDefaultOnSelect: true
});
const returnFocusToTrigger = React.useCallback(skip => {
if (!skip && restoreFocus && triggerRef.current) {
triggerRef.current.focus();
}
}, [triggerRef, restoreFocus]);
const closeMenu = React.useCallback(changeType => {
dispatch({
type: changeType,
payload: {
...(!isExpandedControlled && {
isExpanded: false
})
}
});
onChange({
type: changeType,
isExpanded: false
});
}, [onChange, isExpandedControlled]);
const isItemSelected = React.useCallback((value, type, name, href) => {
let isSelected;
if (type === 'checkbox') {
isSelected = !!controlledSelectedItems.find(item => item.value === value);
} else if (type === 'radio') {
const match = controlledSelectedItems.filter(item => item.name === name)[0];
isSelected = match?.value === value;
} else if (href) {
const match = controlledSelectedItems.filter(item => item.value === value)[0];
isSelected = match?.value === value;
}
return isSelected;
}, [controlledSelectedItems]);
const getNextFocusedValue = React.useCallback(_ref2 => {
let {
value,
key,
isAlphanumericChar
} = _ref2;
let nextFocusedValue = value;
if (isAlphanumericChar) {
const firstChars = menuItems.map(item => item.label ? item.label[0].toLowerCase() : String(item.value)[0].toLowerCase());
const index = firstChars.indexOf(key);
const item = menuItems[index];
if (item) {
nextFocusedValue = item.value;
}
} else {
const index = values.indexOf(value);
let nextIndex;
if (key === containerUtilities.KEYS.UP) {
nextIndex = (index === 0 ? values.length : index) - 1;
} else if (key === containerUtilities.KEYS.DOWN) {
nextIndex = (index === values.length - 1 ? -1 : index) + 1;
} else if (key === containerUtilities.KEYS.END) {
nextIndex = values.length - 1;
} else if (key === containerUtilities.KEYS.HOME) {
nextIndex = 0;
}
const item = menuItems[nextIndex];
nextFocusedValue = item.value;
}
return nextFocusedValue;
}, [menuItems, values]);
const getSelectedItems = React.useCallback(_ref3 => {
let {
value,
type,
name,
label,
selected,
href
} = _ref3;
if (href) return controlledSelectedItems;
if (!type) return null;
let changes = [...controlledSelectedItems];
const selectedItem = {
value,
type,
label,
...(name && {
name
})
};
if (type === 'checkbox') {
if (selected) {
changes = changes.filter(item => item.value !== value);
} else {
changes.push(selectedItem);
}
} else if (type === 'radio') {
const index = changes.findIndex(item => item.name === name);
if (index > -1) {
changes.splice(index, 1);
}
changes.push(selectedItem);
}
return changes;
}, [controlledSelectedItems]);
const anchorItemError = _ref4 => {
let {
isNext,
isPrevious,
type,
value
} = _ref4;
let invariantKey;
if (isNext) {
invariantKey = 'isNext';
} else if (isPrevious) {
invariantKey = 'isPrevious';
} else {
invariantKey = type;
}
const invariantType = {
isNext: 'isNext',
isPrevious: 'isPrevious',
radio: 'radio',
checkbox: 'checkbox'
}[invariantKey];
throw new Error(`Error: expected useMenu anchor item '${value}' to not use '${invariantType}'`);
};
const handleTriggerClick = React.useCallback(event => {
event.stopPropagation();
const changeType = StateChangeTypes.TriggerClick;
dispatch({
type: changeType,
payload: {
...(!isFocusedValueControlled && {
focusedValue: null
}),
...(!isExpandedControlled && {
isExpanded: !controlledIsExpanded
})
}
});
returnFocusToTrigger(!controlledIsExpanded);
onChange({
type: changeType,
focusedValue: null,
isExpanded: !controlledIsExpanded
});
}, [isFocusedValueControlled, isExpandedControlled, controlledIsExpanded, returnFocusToTrigger, onChange]);
const handleTriggerKeyDown = React.useCallback(event => {
const {
key
} = event;
const isArrowKey = [containerUtilities.KEYS.DOWN, containerUtilities.KEYS.UP].includes(key);
const isSelectKey = [containerUtilities.KEYS.ENTER, containerUtilities.KEYS.SPACE].includes(key);
let changeType;
let nextFocusedValue;
if (isArrowKey) {
changeType = StateChangeTypes[`TriggerKeyDown${key}`];
nextFocusedValue = containerUtilities.KEYS.UP === key ? values[values.length - 1] : values[0];
} else if (isSelectKey) {
changeType = StateChangeTypes[`TriggerKeyDown${key === containerUtilities.KEYS.SPACE ? 'Space' : key}`];
nextFocusedValue = values[0];
}
if (changeType) {
event.preventDefault();
dispatch({
type: changeType,
payload: {
focusOnOpen: true,
...(!isFocusedValueControlled && {
focusedValue: defaultFocusedValue || nextFocusedValue
}),
...(!isExpandedControlled && {
isExpanded: true
})
}
});
returnFocusToTrigger();
onChange({
type: changeType,
focusedValue: defaultFocusedValue || nextFocusedValue,
isExpanded: true
});
}
}, [values, isFocusedValueControlled, defaultFocusedValue, isExpandedControlled, returnFocusToTrigger, onChange]);
const handleMenuKeyDown = React.useCallback(event => {
const {
key
} = event;
if ([containerUtilities.KEYS.ESCAPE, containerUtilities.KEYS.TAB].includes(key)) {
event.preventDefault();
event.stopPropagation();
const type = StateChangeTypes[key === containerUtilities.KEYS.ESCAPE ? 'MenuKeyDownEscape' : 'MenuKeyDownTab'];
returnFocusToTrigger();
closeMenu(type);
}
}, [closeMenu, returnFocusToTrigger]);
const handleBlur = React.useCallback(event => {
const win = environment || window;
const timeoutId = setTimeout(() => {
const activeElement = win.document.activeElement;
const isMenuOrTriggerFocused = menuRef.current?.contains(activeElement) || triggerRef.current?.contains(activeElement);
if (!isMenuOrTriggerFocused) {
const nextElementIsFocusable = !!event.relatedTarget && event.relatedTarget?.nodeName !== '#document';
const shouldSkipFocusReturn = nextElementIsFocusable || !controlledIsExpanded && !nextElementIsFocusable;
returnFocusToTrigger(shouldSkipFocusReturn);
closeMenu(StateChangeTypes.MenuBlur);
}
return () => {
clearTimeout(timeoutId);
};
});
}, [closeMenu, controlledIsExpanded, environment, menuRef, returnFocusToTrigger, triggerRef]);
const handleMenuMouseLeave = React.useCallback(() => {
onChange({
type: StateChangeTypes.MenuMouseLeave
});
}, [onChange]);
const handleItemClick = React.useCallback((event, item) => {
let changeType = StateChangeTypes.MenuItemClick;
const {
isNext,
isPrevious,
href,
selected
} = item;
const isTransitionItem = isNext || isPrevious;
if (isNext) {
changeType = StateChangeTypes.MenuItemClickNext;
} else if (isPrevious) {
changeType = StateChangeTypes.MenuItemClickPrevious;
} else if (href && selected) {
event.preventDefault();
}
const nextSelection = getSelectedItems(item);
dispatch({
type: changeType,
payload: {
...(isTransitionItem && {
...(isNext && {
nestedPathIds: [...state.nestedPathIds, item.value]
}),
transitionType: changeType,
isTransitionNext: isNext,
isTransitionPrevious: isPrevious
}),
...(!isExpandedControlled && !isTransitionItem && {
isExpanded: false
}),
...(!isTransitionItem && {
nestedPathIds: []
}),
...(!isSelectedItemsControlled && nextSelection && {
selectedItems: nextSelection
})
}
});
returnFocusToTrigger(isTransitionItem);
onChange({
type: changeType,
value: item.value,
...(!isTransitionItem && {
isExpanded: false
}),
...(nextSelection && {
selectedItems: nextSelection
})
});
}, [getSelectedItems, state.nestedPathIds, isExpandedControlled, isSelectedItemsControlled, returnFocusToTrigger, onChange]);
const handleItemKeyDown = React.useCallback((event, item) => {
const {
key
} = event;
const {
isNext,
isPrevious
} = item;
const isJumpKey = [containerUtilities.KEYS.HOME, containerUtilities.KEYS.END].includes(key);
const isSelectKey = [containerUtilities.KEYS.SPACE, containerUtilities.KEYS.ENTER].includes(key);
const isVerticalArrowKeys = [containerUtilities.KEYS.UP, containerUtilities.KEYS.DOWN].includes(key);
const isAlphanumericChar = key.length === 1 && /\S/u.test(key);
const isTransitionItem = isNext || isPrevious;
let changeType;
let payload = {};
let changes = {};
if (isSelectKey) {
changeType = StateChangeTypes[toMenuItemKeyDownType(key)];
const nextSelection = getSelectedItems(item);
if (isNext) {
changeType = StateChangeTypes.MenuItemKeyDownNext;
} else if (isPrevious) {
changeType = StateChangeTypes.MenuItemKeyDownPrevious;
}
payload = {
...(!isExpandedControlled && !isTransitionItem && {
isExpanded: false
}),
...(!isTransitionItem && {
nestedPathIds: []
}),
...(!isSelectedItemsControlled && nextSelection && {
selectedItems: nextSelection
})
};
changes = {
value: item.value,
...(!isTransitionItem && {
isExpanded: false
}),
...(nextSelection && {
selectedItems: nextSelection
})
};
event.preventDefault();
if (item.href) {
triggerLink(event.target, environment || window);
}
returnFocusToTrigger(isTransitionItem);
} else if (key === containerUtilities.KEYS.RIGHT) {
if (rtl && isPrevious) {
changeType = StateChangeTypes.MenuItemKeyDownPrevious;
}
if (!rtl && isNext) {
changeType = StateChangeTypes.MenuItemKeyDownNext;
}
if (changeType) {
event.preventDefault();
changes = {
value: item.value
};
}
} else if (key === containerUtilities.KEYS.LEFT) {
if (rtl && isNext) {
changeType = StateChangeTypes.MenuItemKeyDownNext;
}
if (!rtl && isPrevious) {
changeType = StateChangeTypes.MenuItemKeyDownPrevious;
}
if (changeType) {
event.preventDefault();
changes = {
value: item.value
};
}
} else if (isVerticalArrowKeys || isJumpKey || isAlphanumericChar) {
event.preventDefault();
changeType = isAlphanumericChar ? StateChangeTypes.MenuItemKeyDown : StateChangeTypes[toMenuItemKeyDownType(key)];
const nextFocusedValue = getNextFocusedValue({
value: item.value,
key,
isAlphanumericChar
});
payload = {
...(!isFocusedValueControlled && {
focusedValue: nextFocusedValue
})
};
changes = {
focusedValue: nextFocusedValue
};
}
if (changeType) {
event.stopPropagation();
const transitionNext = changeType.includes('next');
const willTransition = changeType.includes('previous') || transitionNext;
payload = {
...payload,
...(willTransition && {
...(isNext && {
nestedPathIds: [...state.nestedPathIds, item.value]
}),
transitionType: changeType,
isTransitionNext: isNext,
isTransitionPrevious: isPrevious
})
};
dispatch({
type: changeType,
payload
});
onChange({
type: changeType,
...changes
});
}
}, [environment, getNextFocusedValue, getSelectedItems, isExpandedControlled, isFocusedValueControlled, isSelectedItemsControlled, onChange, returnFocusToTrigger, rtl, state.nestedPathIds]);
const handleItemMouseEnter = React.useCallback(value => {
const changeType = StateChangeTypes.MenuItemMouseMove;
dispatch({
type: changeType,
payload: {
...(!isFocusedValueControlled && {
focusedValue: value
})
}
});
onChange({
type: changeType,
focusedValue: value
});
}, [isFocusedValueControlled, onChange]);
React.useEffect(() => {
setMenuVisible(controlledIsExpanded);
}, [controlledIsExpanded]);
React.useEffect(() => {
const win = environment || window;
if (controlledIsExpanded) {
win.document.addEventListener('keydown', handleMenuKeyDown, true);
} else if (!controlledIsExpanded) {
win.document.removeEventListener('keydown', handleMenuKeyDown, true);
}
return () => {
win.document.removeEventListener('keydown', handleMenuKeyDown, true);
};
}, [controlledIsExpanded, handleMenuKeyDown, environment]);
React.useEffect(() => {
if (state.focusOnOpen && menuVisible && controlledFocusedValue && controlledIsExpanded) {
let ref = itemRefs[controlledFocusedValue]?.current;
if (!ref) {
ref = itemRefs[values[0]].current;
}
if (ref) {
ref.focus();
dispatch({
type: StateChangeTypes.FnInternalUpdate,
payload: {
focusOnOpen: false
}
});
}
}
}, [values, menuVisible, itemRefs, controlledFocusedValue, state.focusOnOpen, controlledIsExpanded]);
React.useEffect(() => {
const valuesChanged = JSON.stringify(values) !== JSON.stringify(state.valuesRef);
if (valuesChanged && !state.isTransitionNext && !state.isTransitionPrevious) {
dispatch({
type: StateChangeTypes.FnInternalUpdate,
payload: {
valuesRef: values
}
});
}
if (valuesChanged && (state.isTransitionNext || state.isTransitionPrevious)) {
const nextFocusedValue = state.isTransitionNext ? values[0] : state.nestedPathIds.slice(-1)[0];
dispatch({
type: StateChangeTypes.FnMenuTransitionFinish,
payload: {
valuesRef: values,
focusOnOpen: true,
nestedPathIds: state.isTransitionNext ? state.nestedPathIds : state.nestedPathIds.slice(0, -1),
...(!isFocusedValueControlled && {
focusedValue: nextFocusedValue
})
}
});
onChange({
type: StateChangeTypes.FnMenuTransitionFinish,
focusedValue: nextFocusedValue
});
}
}, [values, isFocusedValueControlled, state.valuesRef, state.transitionType, state.isTransitionNext, state.isTransitionPrevious, state.nestedPathIds, onChange]);
const getTriggerProps = React.useCallback(function (_temp) {
let {
onBlur,
onClick,
onKeyDown,
type = 'button',
role = 'button',
disabled,
...other
} = _temp === void 0 ? {} : _temp;
return {
...other,
'data-garden-container-id': 'containers.menu.trigger',
'data-garden-container-version': '1.0.2',
ref: triggerRef,
id: triggerId,
'aria-expanded': controlledIsExpanded,
'aria-haspopup': true,
disabled,
tabIndex: disabled ? -1 : 0,
type: type === null ? undefined : type,
role: role === null ? undefined : role,
onBlur: containerUtilities.composeEventHandlers(onBlur, handleBlur),
onClick: containerUtilities.composeEventHandlers(onClick, handleTriggerClick),
onKeyDown: containerUtilities.composeEventHandlers(onKeyDown, handleTriggerKeyDown)
};
}, [controlledIsExpanded, handleBlur, handleTriggerClick, handleTriggerKeyDown, triggerId, triggerRef]);
const getMenuProps = React.useCallback(function (_temp2) {
let {
role = 'menu',
onBlur,
onMouseLeave,
...other
} = _temp2 === void 0 ? {} : _temp2;
return {
...other,
...getGroupProps({
onMouseLeave: containerUtilities.composeEventHandlers(onMouseLeave, handleMenuMouseLeave)
}),
'data-garden-container-id': 'containers.menu',
'data-garden-container-version': '1.0.2',
'aria-labelledby': triggerId,
tabIndex: -1,
role: role === null ? undefined : role,
ref: menuRef,
onBlur: containerUtilities.composeEventHandlers(onBlur, handleBlur)
};
}, [getGroupProps, handleBlur, handleMenuMouseLeave, menuRef, triggerId]);
const getSeparatorProps = React.useCallback(function (_temp3) {
let {
role = 'separator',
...other
} = _temp3 === void 0 ? {} : _temp3;
return {
...other,
'data-garden-container-id': 'containers.menu.separator',
'data-garden-container-version': '1.0.2',
role: role === null ? undefined : role
};
}, []);
const getItemGroupProps = React.useCallback(_ref5 => {
let {
role = 'group',
...other
} = _ref5;
return {
...other,
'data-garden-container-id': 'containers.menu.item_group',
'data-garden-container-version': '1.0.2',
role: role === null ? undefined : role
};
}, []);
const getItemProps = React.useCallback(_ref6 => {
let {
role = 'menuitem',
onClick,
onKeyDown,
onMouseEnter,
item,
...other
} = _ref6;
const {
disabled: itemDisabled,
type,
name,
value,
href,
isNext = false,
isPrevious = false,
label = value
} = item;
let itemRole = role;
const baseAttributes = {
'data-garden-container-id': 'containers.menu.item',
'data-garden-container-version': '1.0.2',
onClick,
onKeyDown,
onMouseEnter
};
if (href) {
if (isNext || isPrevious || type) {
anchorItemError(item);
}
return {
...baseAttributes,
role: itemRole === null ? undefined : 'none',
...other
};
}
if (type === 'radio') {
itemRole = 'menuitemradio';
} else if (type === 'checkbox') {
itemRole = 'menuitemcheckbox';
}
const selected = isItemSelected(value, type, name);
const elementProps = {
...baseAttributes,
'aria-selected': undefined,
'aria-disabled': itemRole === 'none' ? undefined : itemDisabled,
'aria-checked': selected,
role: itemRole === null ? undefined : itemRole,
...other
};
if (itemDisabled) {
return elementProps;
}
const itemProps = getElementProps({
value: value,
...elementProps,
onClick: containerUtilities.composeEventHandlers(onClick, e => handleItemClick(e, {
...item,
label,
selected,
isNext,
isPrevious
})),
onKeyDown: containerUtilities.composeEventHandlers(onKeyDown, e => handleItemKeyDown(e, {
...item,
label,
selected,
isNext,
isPrevious
})),
onMouseEnter: containerUtilities.composeEventHandlers(onMouseEnter, () => handleItemMouseEnter(value))
});
if (itemProps.ref !== itemRefs[value]) {
itemRefs[value] = itemProps.ref;
}
return itemProps;
}, [itemRefs, isItemSelected, getElementProps, handleItemClick, handleItemKeyDown, handleItemMouseEnter]);
const getAnchorProps = React.useCallback(_ref7 => {
let {
role = 'menuitem',
onClick,
onKeyDown,
onMouseEnter,
item,
...other
} = _ref7;
const {
disabled: itemDisabled,
value,
href,
external,
label = value
} = item;
if (!href) return undefined;
const elementProps = {
'data-garden-container-id': 'containers.menu.item.link',
'data-garden-container-version': '1.0.2',
'aria-disabled': itemDisabled,
'aria-selected': undefined,
role: role === null ? undefined : role,
onClick,
onKeyDown,
onMouseEnter,
...other
};
const selected = isItemSelected(value, undefined, undefined, href);
if (!itemDisabled) {
elementProps.href = href;
if (external) {
elementProps.target = '_blank';
elementProps.rel = 'noopener noreferrer';
}
if (selected) {
elementProps['aria-current'] = 'page';
}
}
if (itemDisabled) {
return elementProps;
}
const itemProps = getElementProps({
value: value,
...elementProps,
onClick: containerUtilities.composeEventHandlers(onClick, e => handleItemClick(e, {
...item,
label,
selected
})),
onKeyDown: containerUtilities.composeEventHandlers(onKeyDown, e => handleItemKeyDown(e, {
...item,
label,
selected
})),
onMouseEnter: containerUtilities.composeEventHandlers(onMouseEnter, () => handleItemMouseEnter(value))
});
if (itemProps.ref !== itemRefs[value]) {
itemRefs[value] = itemProps.ref;
}
return itemProps;
}, [itemRefs, isItemSelected, getElementProps, handleItemClick, handleItemKeyDown, handleItemMouseEnter]);
return React.useMemo(() => ({
getAnchorProps,
getItemGroupProps,
getItemProps,
getMenuProps,
getSeparatorProps,
getTriggerProps,
isExpanded: controlledIsExpanded,
selection: controlledSelectedItems,
focusedValue: controlledFocusedValue
}), [controlledFocusedValue, controlledIsExpanded, controlledSelectedItems, getAnchorProps, getItemGroupProps, getItemProps, getMenuProps, getSeparatorProps, getTriggerProps]);
};
const MenuContainer = props => {
const {
children,
render = children,
...options
} = props;
return React.createElement(React.Fragment, null, render(useMenu(options)));
};
MenuContainer.propTypes = {
children: PropTypes.func,
render: PropTypes.func,
items: PropTypes.arrayOf(PropTypes.any).isRequired,
triggerRef: PropTypes.any.isRequired,
menuRef: PropTypes.any.isRequired,
idPrefix: PropTypes.string,
environment: PropTypes.any,
onChange: PropTypes.func,
isExpanded: PropTypes.bool,
defaultExpanded: PropTypes.bool,
selectedItems: PropTypes.arrayOf(PropTypes.any),
focusedValue: PropTypes.oneOfType([PropTypes.string]),
defaultFocusedValue: PropTypes.oneOfType([PropTypes.string]),
restoreFocus: PropTypes.bool
};
MenuContainer.defaultProps = {
restoreFocus: true
};
exports.MenuContainer = MenuContainer;
exports.useMenu = useMenu;