From 95d9aed0121e1a0cb6ddf8493e7c20f58fb4a460 Mon Sep 17 00:00:00 2001 From: macuzi Date: Thu, 7 May 2026 09:45:08 -0700 Subject: [PATCH 1/5] fix(a11y): render nav dropdowns inline for correct keyboard tab order --- src/components/TopNavClient.tsx | 198 ++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 89 deletions(-) diff --git a/src/components/TopNavClient.tsx b/src/components/TopNavClient.tsx index 68c71629df9548..8f6c012788e6f8 100644 --- a/src/components/TopNavClient.tsx +++ b/src/components/TopNavClient.tsx @@ -11,9 +11,10 @@ import platformSelectorStyles from './platformSelector/style.module.scss'; const mainSections = mainSectionsWithDropdowns; -// Add a helper hook for portal dropdown positioning +// Helper hook for dropdown positioning +// Returns both absolute (for portals) and fixed (for inline) coordinates function useDropdownPosition(triggerRef, open) { - const [position, setPosition] = useState({top: 0, left: 0, width: 0}); + const [position, setPosition] = useState({top: 0, left: 0, width: 0, fixedTop: 0, fixedLeft: 0}); useEffect(() => { function updatePosition() { if (triggerRef.current && open) { @@ -22,6 +23,8 @@ function useDropdownPosition(triggerRef, open) { top: rect.bottom + window.scrollY, left: rect.left + window.scrollX, width: rect.width, + fixedTop: rect.bottom, + fixedLeft: rect.left, }); } } @@ -139,8 +142,29 @@ export default function TopNavClient({platforms}: {platforms: Platform[]}) { } } } + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + if (conceptsDropdownOpen) { + setConceptsDropdownOpen(false); + conceptsBtnRef.current?.focus(); + } + if (moreDropdownOpen) { + setMoreDropdownOpen(false); + moreBtnRef.current?.focus(); + } + if (platformDropdownOpen) { + setPlatformDropdownOpen(false); + setPlatformDropdownByClick(false); + platformBtnRef.current?.focus(); + } + } + } document.addEventListener('mousedown', handleClick); - return () => document.removeEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleKeyDown); + }; }, [ platformDropdownOpen, platformDropdownByClick, @@ -298,6 +322,47 @@ export default function TopNavClient({platforms}: {platforms: Platform[]}) { /> + {conceptsDropdownOpen && ( +
e.stopPropagation()} + onMouseEnter={() => { + clearTimeout(closeTimers.current.concepts); + }} + onMouseLeave={() => { + closeTimers.current.concepts = setTimeout(() => { + setConceptsDropdownOpen(false); + }, 150); + }} + > + + {mainSections + .find(s => s.label === 'Concepts') + ?.dropdown?.map(dropdown => ( + + {dropdown.label} + + ))} +
+ )} ) : section.label === 'Manage' ? (
+ {moreDropdownOpen && ( +
e.stopPropagation()} + onMouseEnter={() => { + clearTimeout(closeTimers.current.more); + }} + onMouseLeave={() => { + closeTimers.current.more = setTimeout(() => { + setMoreDropdownOpen(false); + }, 150); + }} + > + + {mainSections + .find(s => s.label === 'Manage') + ?.dropdown?.map(dropdown => ( + + {dropdown.label} + + ))} +
+ )}
) : section.label === 'SDKs' ? ( , document.body )} - {conceptsDropdownOpen && - ReactDOM.createPortal( -
e.stopPropagation()} - onMouseEnter={() => { - clearTimeout(closeTimers.current.concepts); - }} - onMouseLeave={() => { - closeTimers.current.concepts = setTimeout(() => { - setConceptsDropdownOpen(false); - }, 150); - }} - > - - {mainSections - .find(s => s.label === 'Concepts') - ?.dropdown?.map(dropdown => ( - - {dropdown.label} - - ))} -
, - document.body - )} - {moreDropdownOpen && - ReactDOM.createPortal( -
e.stopPropagation()} - onMouseEnter={() => { - clearTimeout(closeTimers.current.more); - }} - onMouseLeave={() => { - closeTimers.current.more = setTimeout(() => { - setMoreDropdownOpen(false); - }, 150); - }} - > - - {mainSections - .find(s => s.label === 'Manage') - ?.dropdown?.map(dropdown => ( - - {dropdown.label} - - ))} -
, - document.body - )}