Skip to content

Commit d639fe2

Browse files
committed
feat(pass-extension): navigate autofill login suggestions with arrow keys
Wire useDropdownArrowNavigation + useHotkeys (bound to the iframe document) in the login view so the suggestions can be moved through with the arrow keys, selected with Enter, and dismissed with Escape. Highlight the focused item in the injected dropdown styles.
1 parent e2560a4 commit d639fe2

2 files changed

Lines changed: 45 additions & 11 deletions

File tree

applications/pass-extension/src/app/content/services/inline/dropdown/app/views/AutofillLogin.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type FC, useCallback, useEffect, useMemo } from 'react';
1+
import { type FC, useCallback, useEffect, useMemo, useRef } from 'react';
22

33
import type { DropdownAction } from 'proton-pass-extension/app/content/constants.runtime';
44
import { DropdownHeader } from 'proton-pass-extension/app/content/services/inline/dropdown/app/components/DropdownHeader';
@@ -19,6 +19,9 @@ import { c } from 'ttag';
1919

2020
import { CircleLoader } from '@proton/atoms/CircleLoader/CircleLoader';
2121
import 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';
2225
import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider';
2326
import { UpsellRef } from '@proton/pass/constants';
2427
import { 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
};

applications/pass-extension/src/lib/components/Inline/ListItem.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,13 @@ button.pass-injected-dropdown--item {
33
cursor: default;
44
background-color: transparent;
55
}
6+
7+
// Keyboard navigation: highlight the focused suggestion like hover so the active item is
8+
// obvious when navigating the list with the arrow keys. `:focus` (not only `:focus-visible`)
9+
// because navigation focuses items programmatically, which some engines do not treat as
10+
// focus-visible.
11+
&:focus,
12+
&:focus-visible {
13+
background-color: var(--interaction-default-hover);
14+
}
615
}

0 commit comments

Comments
 (0)