/** @module @category UI */ import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { OverflowMenuVertical } from '@carbon/react/icons'; import { ExtensionSlot, useExtensionSlot, useLayoutType, useOnClickOutside } from '@openmrs/esm-react-utils'; import { getCoreTranslation } from '@openmrs/esm-translations'; import customOverflowMenuStyles from '../../custom-overflow-menu/custom-overflow-menu.module.scss'; import styles from './patient-banner-actions-menu.module.scss'; export interface PatientBannerActionsMenuProps { patient: fhir.Patient; patientUuid: string; actionsSlotName: string; /** * Parts of the actions slot extension state that don't really make sense go in this object, * so as to keep the PatientBannerActionsMenu API clean. */ additionalActionsSlotState?: object; } /** * Overflow menu for the patient banner whose items come from an ExtensionSlot * rather than direct React children. Because cloneElement cannot inject props * into extension-rendered components, arrow key navigation is handled at the * container level via onKeyDown instead of delegating to Carbon's OverflowMenuItem. */ export function PatientBannerActionsMenu({ patient, patientUuid, actionsSlotName, additionalActionsSlotState, }: PatientBannerActionsMenuProps) { const [menuIsOpen, setMenuIsOpen] = useState(false); const { extensions: patientActions } = useExtensionSlot(actionsSlotName); const isTablet = useLayoutType() === 'tablet'; const ref = useOnClickOutside(() => setMenuIsOpen(false), menuIsOpen); const triggerRef = useRef(null); const menuRef = useRef(null); const uniqueId = useId(); const triggerId = `patient-actions-menu-trigger-${uniqueId}`; const menuId = `patient-actions-menu-${uniqueId}`; const toggleShowMenu = useCallback(() => setMenuIsOpen((state) => !state), []); const closeMenuAndFocusTrigger = useCallback(() => { setMenuIsOpen(false); triggerRef.current?.focus(); }, []); const handleMenuKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Escape' && menuIsOpen) { e.stopPropagation(); closeMenuAndFocusTrigger(); return; } if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && menuIsOpen) { e.preventDefault(); const enabledItems = menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])'); if (!enabledItems?.length) { return; } const activeItem = (document.activeElement?.closest?.('[role="menuitem"]') as HTMLElement) ?? document.activeElement; const currentPos = Array.from(enabledItems).indexOf(activeItem as HTMLElement); if (currentPos === -1) { enabledItems[e.key === 'ArrowDown' ? 0 : enabledItems.length - 1]?.focus(); return; } const direction = e.key === 'ArrowDown' ? 1 : -1; const nextPos = currentPos + direction; const wrappedPos = nextPos < 0 ? enabledItems.length - 1 : nextPos >= enabledItems.length ? 0 : nextPos; enabledItems[wrappedPos]?.focus(); } }, [closeMenuAndFocusTrigger, menuIsOpen], ); useEffect(() => { if (menuIsOpen && menuRef.current) { const firstItem = menuRef.current.querySelector('[role="menuitem"]:not([disabled])'); firstItem?.focus(); } }, [menuIsOpen]); const patientActionsSlotState = useMemo( () => ({ patientUuid, patient, closeMenu: closeMenuAndFocusTrigger, ...additionalActionsSlotState }), [patientUuid, patient, closeMenuAndFocusTrigger, additionalActionsSlotState], ); if (patientActions.length === 0) { return null; } return (
); }