1- import { type FC , useCallback , useEffect , useMemo } from 'react' ;
1+ import { type FC , useCallback , useEffect , useMemo , useRef } from 'react' ;
22
33import type { DropdownAction } from 'proton-pass-extension/app/content/constants.runtime' ;
44import { DropdownHeader } from 'proton-pass-extension/app/content/services/inline/dropdown/app/components/DropdownHeader' ;
@@ -19,6 +19,9 @@ import { c } from 'ttag';
1919
2020import { CircleLoader } from '@proton/atoms/CircleLoader/CircleLoader' ;
2121import Marks from '@proton/components/components/text/Marks' ;
22+ import useDropdownArrowNavigation from '@proton/components/hooks/useDropdownArrowNavigation' ;
23+ import type { HotkeyTuple } from '@proton/components/hooks/useHotkeys' ;
24+ import { useHotkeys } from '@proton/components/hooks/useHotkeys' ;
2225import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider' ;
2326import { UpsellRef } from '@proton/pass/constants' ;
2427import { useMountedState } from '@proton/pass/hooks/useEnsureMounted' ;
@@ -148,6 +151,26 @@ export const AutofillLogin: FC<Props> = ({ startsWith, action, ...payload }) =>
148151 [ state , filter ]
149152 ) ;
150153
154+ /** Keyboard navigation over the suggestions. When the dropdown is opened via the autofill
155+ * shortcut, focus is moved into the iframe, so the listener is bound to the iframe `document`
156+ * (focus lands on the body, outside `rootRef`). Arrow keys move focus across the rows inside
157+ * `rootRef` (the header is excluded; the upgrade and empty-state rows stay reachable); Enter
158+ * activates the focused item natively (each is a `<button>`); Escape dismisses the dropdown. */
159+ const rootRef = useRef < HTMLDivElement > ( null ) ;
160+ const documentRef = useRef < Document > ( document ) ;
161+ const { shortcutHandlers } = useDropdownArrowNavigation ( { rootRef } ) ;
162+ const hotkeys : HotkeyTuple [ ] = [
163+ ...shortcutHandlers ,
164+ [
165+ 'Escape' ,
166+ ( e ) => {
167+ e . preventDefault ( ) ;
168+ controller . close ( { userAction : true } ) ;
169+ } ,
170+ ] ,
171+ ] ;
172+ useHotkeys ( documentRef , hotkeys ) ;
173+
151174 if ( loading ) return < CircleLoader className = "absolute inset-center m-auto" /> ;
152175
153176 return (
@@ -163,16 +186,18 @@ export const AutofillLogin: FC<Props> = ({ startsWith, action, ...payload }) =>
163186 />
164187 }
165188 />
166- { dropdownItems . length > 0 ? (
167- < ScrollableItemsList > { dropdownItems } </ ScrollableItemsList >
168- ) : (
169- < ListItem
170- icon = { { type : 'status' , icon : PassIconStatus . ACTIVE } }
171- onClick = { controller . close }
172- title = { PASS_APP_NAME }
173- subTitle = { c ( 'Info' ) . t `No login found` }
174- />
175- ) }
189+ < div ref = { rootRef } >
190+ { dropdownItems . length > 0 ? (
191+ < ScrollableItemsList > { dropdownItems } </ ScrollableItemsList >
192+ ) : (
193+ < ListItem
194+ icon = { { type : 'status' , icon : PassIconStatus . ACTIVE } }
195+ onClick = { controller . close }
196+ title = { PASS_APP_NAME }
197+ subTitle = { c ( 'Info' ) . t `No login found` }
198+ />
199+ ) }
200+ </ div >
176201 </ >
177202 ) ;
178203} ;
0 commit comments