diff --git a/src/core/packages/chrome/navigation/src/components/nested_secondary_menu/index.tsx b/src/core/packages/chrome/navigation/src/components/nested_secondary_menu/index.tsx index b24e1a33aae14..b70406a714dff 100644 --- a/src/core/packages/chrome/navigation/src/components/nested_secondary_menu/index.tsx +++ b/src/core/packages/chrome/navigation/src/components/nested_secondary_menu/index.tsx @@ -38,12 +38,20 @@ export const NestedSecondaryMenu: NestedSecondaryMenuComponent = ({ const [panelStack, setPanelStack] = useState>([]); const [returnFocusId, setReturnFocusId] = useState(); + console.log( + `*** [NestedSecondaryMenu] render - currentPanel="${currentPanel}", panelStackDepth=${panelStack.length}` + ); + const goToPanel = useCallback( (panelId: string, focusId?: string) => { - setPanelStack((prev) => [ - ...prev, - { id: currentPanel, returnFocusId: focusId || returnFocusId }, - ]); + console.log( + `*** [NestedSecondaryMenu] goToPanel() - from="${currentPanel}" to="${panelId}", focusId="${focusId}"` + ); + setPanelStack((prev) => { + const newStack = [...prev, { id: currentPanel, returnFocusId: focusId || returnFocusId }]; + console.log(`*** [NestedSecondaryMenu] goToPanel() - new stack depth=${newStack.length}`); + return newStack; + }); setCurrentPanel(panelId); setReturnFocusId(undefined); }, @@ -51,16 +59,23 @@ export const NestedSecondaryMenu: NestedSecondaryMenuComponent = ({ ); const goBack = useCallback(() => { + console.log(`*** [NestedSecondaryMenu] goBack() - from="${currentPanel}"`); setPanelStack((prev) => { const previousPanel = prev[prev.length - 1]; - if (!previousPanel) return prev; + if (!previousPanel) { + console.log( + `*** [NestedSecondaryMenu] goBack() - no previous panel, staying at "${currentPanel}"` + ); + return prev; + } + console.log(`*** [NestedSecondaryMenu] goBack() - to="${previousPanel.id}"`); setCurrentPanel(previousPanel.id); setReturnFocusId(previousPanel.returnFocusId); return prev.slice(0, -1); }); - }, []); + }, [currentPanel]); const contextValue = { canGoBack: panelStack.length > 0, diff --git a/src/core/packages/chrome/navigation/src/components/popover/index.tsx b/src/core/packages/chrome/navigation/src/components/popover/index.tsx index bc0d204ca339b..d77d8edf9862a 100644 --- a/src/core/packages/chrome/navigation/src/components/popover/index.tsx +++ b/src/core/packages/chrome/navigation/src/components/popover/index.tsx @@ -84,74 +84,146 @@ export const SideNavPopover = ({ const [isOpenedByClick, setIsOpenedByClick] = useState(false); const [isOpen, setIsOpen] = useState(false); const [shouldFocusOnOpen, setShouldFocusOnOpen] = useState(false); + const [wasKeyboardUsed, setWasKeyboardUsed] = useState(false); - const setOpenedByClick = useCallback(() => setIsOpenedByClick(true), []); + const setOpenedByClick = useCallback(() => { + console.log(`*** [Popover:${label}] setOpenedByClick()`); + setIsOpenedByClick(true); + }, [label]); - const clearOpenedByClick = useCallback(() => setIsOpenedByClick(false), []); + const clearOpenedByClick = useCallback(() => { + console.log(`*** [Popover:${label}] clearOpenedByClick()`); + setIsOpenedByClick(false); + }, [label]); const open = useCallback(() => { + console.log(`*** [Popover:${label}] open() - setting isOpen=true, anyPopoverOpen=true`); + console.log( + `*** [Popover:${label}] open() - isSidePanelOpen=${isSidePanelOpen}, previous anyPopoverOpen=${anyPopoverOpen}` + ); setIsOpen(true); anyPopoverOpen = true; - }, []); + }, [label, isSidePanelOpen]); const close = useCallback(() => { + console.log(`*** [Popover:${label}] close() - setting isOpen=false, anyPopoverOpen=false`); + console.log(`*** [Popover:${label}] close() - previous anyPopoverOpen=${anyPopoverOpen}`); setIsOpen(false); clearOpenedByClick(); clearTimeout(); setShouldFocusOnOpen(false); anyPopoverOpen = false; - }, [clearOpenedByClick, clearTimeout]); + }, [clearOpenedByClick, clearTimeout, label]); const handleClose = useCallback(() => { + console.log(`*** [Popover:${label}] handleClose()`); clearTimeout(); close(); - }, [clearTimeout, close]); + }, [clearTimeout, close, label]); const tryOpen = useCallback(() => { + console.log( + `*** [Popover:${label}] tryOpen() - isSidePanelOpen=${isSidePanelOpen}, anyPopoverOpen=${getIsAnyPopoverOpenNow()}` + ); if (!isSidePanelOpen && !getIsAnyPopoverOpenNow()) { + console.log(`*** [Popover:${label}] tryOpen() - conditions met, calling open()`); open(); + } else { + console.log(`*** [Popover:${label}] tryOpen() - conditions NOT met, skipping open`); } - }, [isSidePanelOpen, open]); + }, [isSidePanelOpen, open, label]); const handleMouseEnter = useCallback(() => { + console.log( + `*** [Popover:${label}] handleMouseEnter() - persistent=${persistent}, isOpenedByClick=${isOpenedByClick}` + ); if (!persistent || !isOpenedByClick) { clearTimeout(); if (getIsAnyPopoverOpenNow()) { + console.log( + `*** [Popover:${label}] handleMouseEnter() - another popover open, scheduling tryOpen` + ); setTimeout(tryOpen, POPOVER_HOVER_DELAY); } else if (!isSidePanelOpen) { + console.log(`*** [Popover:${label}] handleMouseEnter() - no popover open, scheduling open`); setTimeout(open, POPOVER_HOVER_DELAY); + } else { + console.log(`*** [Popover:${label}] handleMouseEnter() - side panel open, not opening`); } + } else { + console.log( + `*** [Popover:${label}] handleMouseEnter() - persistent and opened by click, ignoring` + ); } - }, [persistent, isOpenedByClick, isSidePanelOpen, clearTimeout, open, setTimeout, tryOpen]); + }, [ + persistent, + isOpenedByClick, + isSidePanelOpen, + clearTimeout, + open, + setTimeout, + tryOpen, + label, + ]); const handleMouseLeave = useCallback(() => { + console.log( + `*** [Popover:${label}] handleMouseLeave() - persistent=${persistent}, isOpenedByClick=${isOpenedByClick}` + ); if (!persistent || !isOpenedByClick) { + console.log(`*** [Popover:${label}] handleMouseLeave() - scheduling close`); setTimeout(handleClose, POPOVER_HOVER_DELAY); + } else { + console.log( + `*** [Popover:${label}] handleMouseLeave() - persistent and opened by click, not closing` + ); } - }, [persistent, isOpenedByClick, setTimeout, handleClose]); + }, [persistent, isOpenedByClick, setTimeout, handleClose, label]); const scrollStyles = useScroll(true); const handleTriggerClick = useCallback(() => { + console.log( + `*** [Popover:${label}] handleTriggerClick() - persistent=${persistent}, isOpen=${isOpen}, isOpenedByClick=${isOpenedByClick}` + ); if (persistent) { if (isOpen && isOpenedByClick) { + console.log(`*** [Popover:${label}] handleTriggerClick() - closing persistent popover`); handleClose(); } else { + console.log(`*** [Popover:${label}] handleTriggerClick() - opening persistent popover`); clearTimeout(); open(); setOpenedByClick(); } + } else { + console.log(`*** [Popover:${label}] handleTriggerClick() - not persistent, ignoring click`); } - }, [persistent, isOpen, isOpenedByClick, handleClose, clearTimeout, open, setOpenedByClick]); + }, [ + persistent, + isOpen, + isOpenedByClick, + handleClose, + clearTimeout, + open, + setOpenedByClick, + label, + ]); const handleTriggerKeyDown: KeyboardEventHandler = useCallback( (e) => { + console.log( + `*** [Popover:${label}] handleTriggerKeyDown() - key=${e.key}, hasContent=${hasContent}` + ); if (e.key === 'Enter' || e.key === ' ') { trigger.props.onKeyDown?.(e); if (hasContent) { // Required for entering the popover with Enter or Space key // Otherwise the navigation happens immediately + console.log( + `*** [Popover:${label}] handleTriggerKeyDown() - opening with keyboard, will focus` + ); e.preventDefault(); setShouldFocusOnOpen(true); open(); @@ -160,18 +232,21 @@ export const SideNavPopover = ({ trigger.props.onKeyDown?.(e); } }, - [trigger, hasContent, open] + [trigger, hasContent, open, label] ); const handlePopoverKeyDown: KeyboardEventHandler = useCallback( (e) => { + console.log(`*** [Popover:${label}] handlePopoverKeyDown() - key=${e.key}`); if (e.key === 'Escape') { + console.log(`*** [Popover:${label}] handlePopoverKeyDown() - Escape pressed, closing`); handleClose(); triggerRef.current?.focus(); return; } if (e.key === 'Tab') { + console.log(`*** [Popover:${label}] handlePopoverKeyDown() - Tab pressed, closing`); e.preventDefault(); handleClose(); focusAdjacentTrigger(triggerRef, e.shiftKey ? -1 : 1); @@ -180,11 +255,17 @@ export const SideNavPopover = ({ handleRovingIndex(e); }, - [handleClose] + [handleClose, label] ); const handleBlur: FocusEventHandler = useCallback( (e) => { + console.log(`*** [Popover:${label}] handleBlur() - wasKeyboardUsed=${wasKeyboardUsed}`); + if (!wasKeyboardUsed) { + console.log(`*** [Popover:${label}] handleBlur() - keyboard not used, ignoring`); + return; + } + clearTimeout(); const nextFocused = e.relatedTarget; @@ -192,20 +273,49 @@ export const SideNavPopover = ({ nextFocused && (triggerRef.current?.contains(nextFocused) || popoverRef.current?.contains(nextFocused)); + console.log( + `*** [Popover:${label}] handleBlur() - isStayingInComponent=${isStayingInComponent}` + ); if (!isStayingInComponent) { + console.log(`*** [Popover:${label}] handleBlur() - leaving component, closing`); handleClose(); } }, - [clearTimeout, handleClose] + [clearTimeout, handleClose, wasKeyboardUsed, label] ); - // Clean up on unmount useEffect(() => { + console.log(`*** [Popover:${label}] mounted`); return () => { + console.log(`*** [Popover:${label}] unmounting - cleaning up`); clearTimeout(); handleClose(); }; - }, [clearTimeout, handleClose]); + }, [clearTimeout, handleClose, label]); + + // TODO: refactor to use non-portalled popover + // Track if the user has used the keyboard to interact with the popover. + // `wasKeyboardUsed` is used to determine if the popover should be closed when the user blurs the popover. + // If we blur it for mouse users as well, the popover isn't responsive when there are trap focus elements + // on the page. + useEffect(() => { + const handleKeyDown = () => { + console.log(`*** [Popover:${label}] keyboard used, setting wasKeyboardUsed=true`); + setWasKeyboardUsed(true); + }; + const handleMouseDown = () => { + console.log(`*** [Popover:${label}] mouse used, setting wasKeyboardUsed=false`); + setWasKeyboardUsed(false); + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('mousedown', handleMouseDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('mousedown', handleMouseDown); + }; + }, [label]); const enhancedTrigger = useMemo( () => @@ -239,6 +349,11 @@ export const SideNavPopover = ({ z-index: calc(${euiTheme.levels.menu} - 1); `; + const euiPopoverIsOpen = hasContent && !isSidePanelOpen && isOpen; + console.log( + `*** [Popover:${label}] render - EuiPopover isOpen=${euiPopoverIsOpen} (hasContent=${hasContent}, isSidePanelOpen=${isSidePanelOpen}, isOpen=${isOpen})` + ); + return (
{ - popoverRef.current = ref; + ref={(node) => { + popoverRef.current = node; - if (ref) { - const elements = getFocusableElements(ref); + if (node) { + const elements = getFocusableElements(node); updateTabIndices(elements); if (shouldFocusOnOpen) { - focusFirstElement(popoverRef); + focusFirstElement(node); setShouldFocusOnOpen(false); } } diff --git a/src/core/packages/chrome/navigation/src/hooks/use_navigation.ts b/src/core/packages/chrome/navigation/src/hooks/use_navigation.ts index 3923611720249..13402a97d560a 100644 --- a/src/core/packages/chrome/navigation/src/hooks/use_navigation.ts +++ b/src/core/packages/chrome/navigation/src/hooks/use_navigation.ts @@ -38,6 +38,16 @@ export const useNavigation = ( const openerNode = primaryItem; const isSidePanelOpen = !isCollapsed && !!openerNode?.sections; + console.log(`*** [useNavigation] isCollapsed=${isCollapsed}, isSidePanelOpen=${isSidePanelOpen}`); + console.log( + `*** [useNavigation] openerNode=${ + openerNode?.id || 'null' + }, hasSection=${!!openerNode?.sections}` + ); + console.log( + `*** [useNavigation] activeItemId="${activeItemId}", visuallyActivePageId="${visuallyActivePageId}"` + ); + const state: NavigationState = { actualActiveItemId, visuallyActivePageId, diff --git a/src/core/packages/chrome/navigation/src/utils/focus_first_element.ts b/src/core/packages/chrome/navigation/src/utils/focus_first_element.ts index 7b7a85fcbe6c1..c4f09f09cc841 100644 --- a/src/core/packages/chrome/navigation/src/utils/focus_first_element.ts +++ b/src/core/packages/chrome/navigation/src/utils/focus_first_element.ts @@ -7,8 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { RefObject } from 'react'; - import { getFocusableElements } from './get_focusable_elements'; /** @@ -16,11 +14,10 @@ import { getFocusableElements } from './get_focusable_elements'; * * @param ref - The ref to the container element. */ -export const focusFirstElement = (ref: RefObject) => { - const container = ref?.current; - if (!container) return; +export const focusFirstElement = (node: HTMLElement) => { + if (!node) return; - const elements = getFocusableElements(container); + const elements = getFocusableElements(node); if (elements.length > 0) { elements[0].focus();