UNPKG

@zendeskgarden/container-menu

Version:

Containers relating to Menu in the Garden Design System

970 lines (963 loc) 31 kB
/** * 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;