diff --git a/packages/menu/src/MenuContainer.tsx b/packages/menu/src/MenuContainer.tsx index 290e66cbd..80e06407c 100644 --- a/packages/menu/src/MenuContainer.tsx +++ b/packages/menu/src/MenuContainer.tsx @@ -23,7 +23,8 @@ MenuContainer.propTypes = { triggerRef: PropTypes.any.isRequired, menuRef: PropTypes.any.isRequired, idPrefix: PropTypes.string, - environment: PropTypes.any, + document: PropTypes.any, + window: PropTypes.any, onChange: PropTypes.func, isExpanded: PropTypes.bool, defaultExpanded: PropTypes.bool, diff --git a/packages/menu/src/types.ts b/packages/menu/src/types.ts index d3826403d..d1d123c11 100644 --- a/packages/menu/src/types.ts +++ b/packages/menu/src/types.ts @@ -86,8 +86,14 @@ export interface IUseMenuProps { focusedValue?: string | null; selectedItems?: ISelectedItem[]; }) => void; - /** Sets the environment where the menu is rendered */ - environment?: Window; + /** + * Sets the window where the menu is rendered + * */ + window?: Window; + /** + * Sets the document where the menu is rendered + * */ + document?: Document | ShadowRoot; } export interface IUseMenuReturnValue { diff --git a/packages/menu/src/useMenu.ts b/packages/menu/src/useMenu.ts index c52dad233..304c9d5e1 100644 --- a/packages/menu/src/useMenu.ts +++ b/packages/menu/src/useMenu.ts @@ -38,26 +38,46 @@ import { ISelectedItem } from './types'; +/** + * Returns the document object from the window or document prop. To maintain SSR compatibility, use within useEffect hooks to reference correct document object. + * + * @param win + * @param doc + * @returns Document + */ +function getDocument(win?: IUseMenuProps['window'], doc?: IUseMenuProps['document']) { + let _document: IUseMenuProps['document']; + + if (doc) { + _document = doc; + } else { + _document = win ? win.document : window.document; + } + return _document; +} + export const useMenu = ({ - items: rawItems, + defaultExpanded = false, + defaultFocusedValue, + document: documentProp, + focusedValue, idPrefix, - environment, - menuRef, - triggerRef, - rtl = false, - onChange = () => undefined, isExpanded, - defaultExpanded = false, + items: rawItems, + menuRef, restoreFocus = true, + rtl = false, selectedItems, - focusedValue, - defaultFocusedValue + triggerRef, + window: windowProp, + onChange = () => undefined }: IUseMenuProps): IUseMenuReturnValue => { const prefix = `${useId(idPrefix)}-`; const triggerId = `${prefix}menu-trigger`; const isExpandedControlled = isExpanded !== undefined; const isSelectedItemsControlled = selectedItems !== undefined; const isFocusedValueControlled = focusedValue !== undefined; + const menuItems = useMemo( () => rawItems.reduce((items, item: MenuItem) => { @@ -398,11 +418,10 @@ export const useMenu = { - const win = environment || window; - setTimeout(() => { + const _document = getDocument(windowProp, documentProp); // Timeout is required to ensure blur is handled after focus - const activeElement = win.document.activeElement; + const activeElement = _document.activeElement; const isMenuOrTriggerFocused = menuRef.current?.contains(activeElement) || triggerRef.current?.contains(activeElement); @@ -420,7 +439,15 @@ export const useMenu = { @@ -515,7 +542,7 @@ export const useMenu = { - const win = environment || window; + const _document = getDocument(windowProp, documentProp) as Document; if (controlledIsExpanded) { - win.document.addEventListener('keydown', handleMenuKeyDown, true); + _document.addEventListener('keydown', handleMenuKeyDown, true); } else if (!controlledIsExpanded) { - win.document.removeEventListener('keydown', handleMenuKeyDown, true); + _document.removeEventListener('keydown', handleMenuKeyDown, true); } return () => { - win.document.removeEventListener('keydown', handleMenuKeyDown, true); + (_document as Document).removeEventListener('keydown', handleMenuKeyDown, true); }; - }, [controlledIsExpanded, handleMenuKeyDown, environment]); + }, [controlledIsExpanded, documentProp, handleMenuKeyDown, windowProp]); /** * When the menu is opened, this effect sets focus on the current menu item using `focusedValue`