Skip to content

Commit ea4fef6

Browse files
committed
feat: add List component for accessible item navigation
- Introduced a new List component with keyboard navigation support. - Implemented focus management and active item tracking. - Added CSS styles for list and item focus states. refactor: update NestedPopoverMenu to use DropdownSurface - Replaced custom popover handling with DropdownSurface for better management. - Removed unused CSS properties and effects related to popover visibility. feat: enhance CopyConfigDialog and MoveConfigDialog with DropdownSelect - Replaced select elements with DropdownSelect for conflict handling options. - Improved user experience with memoized options for dropdowns. fix: OverlayDialog focus handling improvements - Updated focus management to respect inert elements. - Ensured proper handling of Escape key for dismissible dialogs. style: update dialog styles for consistency - Adjusted border styles for dialog buttons and input fields. - Ensured consistent padding and background colors across dialog components. fix: register additional keybindings for autocomplete - Added Tab key support for accepting autocomplete suggestions. style: improve ExtensionsPanel layout and dropdowns - Refactored extension source and filter selection to use DropdownSelect. - Enhanced layout with consistent spacing and alignment for better usability.
1 parent a3e3fe9 commit ea4fef6

17 files changed

Lines changed: 950 additions & 373 deletions

packages/ui/lib/components/AutocompleteInput/AutocompleteInput.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
.autocomplete-input {
66
width: 100%;
7+
background: var(--bg);
78
}
89

910
.autocomplete-dropdown {

packages/ui/lib/components/AutocompleteInput/AutocompleteInput.tsx

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
} from "@/features/commands/commandIds";
1111
import { useCommandRegistry } from "@/features/commands/commands";
1212
import { useFocusContext, useManagedFocusLayer } from "@/focusContext";
13-
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
13+
import { DropdownSurface } from "@/components/DropdownSurface/DropdownSurface";
14+
import { useEffect, useId, useMemo, useRef, useState } from "react";
1415
import styles from "./AutocompleteInput.module.css";
1516

1617
export interface AutocompleteOption {
@@ -65,7 +66,6 @@ export function AutocompleteInput({
6566
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
6667
const pointerDownRef = useRef(false);
6768
const dropdownRef = useRef<HTMLDivElement>(null);
68-
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties | null>(null);
6969
const generatedId = useId();
7070
const anchorId = id ?? `autocomplete-${generatedId.replace(/:/g, "")}`;
7171

@@ -104,6 +104,7 @@ export function AutocompleteInput({
104104
allowCommandRouting(event) {
105105
if (!dropdownOpenRef.current) return false;
106106
switch (event.key) {
107+
case "Tab":
107108
case "ArrowUp":
108109
case "ArrowDown":
109110
case "PageUp":
@@ -208,23 +209,6 @@ export function AutocompleteInput({
208209
};
209210
}, [commandRegistry, dropdownOpen]);
210211

211-
useEffect(() => {
212-
const dropdown = dropdownRef.current;
213-
if (!dropdown) return;
214-
if (!("showPopover" in dropdown)) return;
215-
const popoverDropdown = dropdown as HTMLDivElement & {
216-
showPopover: () => void;
217-
hidePopover: () => void;
218-
matches: (selector: string) => boolean;
219-
};
220-
const isOpen = popoverDropdown.matches(":popover-open");
221-
if (dropdownOpen) {
222-
if (!isOpen) popoverDropdown.showPopover();
223-
return;
224-
}
225-
if (isOpen) popoverDropdown.hidePopover();
226-
}, [anchorId, dropdownOpen]);
227-
228212
useEffect(() => {
229213
if (!dropdownOpen || selectedIndex === null) return;
230214
const dropdown = dropdownRef.current;
@@ -233,30 +217,6 @@ export function AutocompleteInput({
233217
selectedEl?.scrollIntoView({ block: "nearest" });
234218
}, [dropdownOpen, selectedIndex]);
235219

236-
useLayoutEffect(() => {
237-
if (!dropdownOpen) return;
238-
239-
const updatePosition = () => {
240-
const input = mergedInputRef.current;
241-
if (!input) return;
242-
const rect = input.getBoundingClientRect();
243-
setDropdownStyle({
244-
position: "fixed",
245-
top: rect.bottom + 4,
246-
left: rect.left,
247-
width: rect.width,
248-
});
249-
};
250-
251-
updatePosition();
252-
window.addEventListener("resize", updatePosition);
253-
window.addEventListener("scroll", updatePosition, true);
254-
return () => {
255-
window.removeEventListener("resize", updatePosition);
256-
window.removeEventListener("scroll", updatePosition, true);
257-
};
258-
}, [dropdownOpen, mergedInputRef]);
259-
260220
return (
261221
<div className={className ? `${styles["autocomplete"]} ${className}` : styles["autocomplete"]}>
262222
<input
@@ -273,18 +233,29 @@ export function AutocompleteInput({
273233
setOpen(true);
274234
setSelectedIndex(null);
275235
}}
236+
onKeyDownCapture={(event) => {
237+
if (event.key !== "Escape") return;
238+
if (!dropdownOpenRef.current) return;
239+
event.preventDefault();
240+
event.stopPropagation();
241+
setOpen(false);
242+
setSelectedIndex(null);
243+
}}
276244
onBlur={() => {
277245
if (pointerDownRef.current) return;
278246
setOpen(false);
279247
setSelectedIndex(null);
280248
}}
281249
/>
282250
{flattened.length > 0 && (
283-
<div
284-
ref={dropdownRef}
285-
popover="manual"
251+
<DropdownSurface
252+
open={dropdownOpen}
253+
anchor={{ type: "element", ref: mergedInputRef }}
254+
placement="bottom-start"
255+
offset={4}
256+
matchAnchorWidth
286257
className={styles["autocomplete-dropdown"]}
287-
style={dropdownStyle ?? undefined}
258+
surfaceRef={dropdownRef}
288259
>
289260
{groups.map((group) => {
290261
if (group.options.length === 0) return null;
@@ -325,7 +296,7 @@ export function AutocompleteInput({
325296
</div>
326297
);
327298
})}
328-
</div>
299+
</DropdownSurface>
329300
)}
330301
</div>
331302
);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
.trigger {
2+
appearance: none;
3+
-webkit-appearance: none;
4+
width: 100%;
5+
display: inline-flex;
6+
align-items: center;
7+
justify-content: space-between;
8+
gap: 8px;
9+
min-height: 32px;
10+
padding: 6px 8px;
11+
border: 1px solid var(--input-border, var(--border));
12+
border-radius: 3px;
13+
background: var(--bg);
14+
background-image: none;
15+
color: var(--fg);
16+
font: inherit;
17+
text-align: left;
18+
box-shadow: none;
19+
cursor: pointer;
20+
}
21+
22+
.trigger:hover {
23+
background: var(--bg);
24+
border-color: var(--border-active, var(--input-border, var(--border)));
25+
}
26+
27+
.trigger:active {
28+
background: var(--bg);
29+
}
30+
31+
.trigger:focus-visible {
32+
outline: 2px solid var(--border-active, var(--border));
33+
outline-offset: 0;
34+
border-color: var(--border-active, var(--border));
35+
}
36+
37+
.chevron {
38+
flex: 0 0 auto;
39+
opacity: 0.8;
40+
}
41+
42+
.menu {
43+
min-width: 180px;
44+
max-width: min(320px, calc(100vw - 16px));
45+
max-height: min(320px, calc(100vh - 16px));
46+
overflow: auto;
47+
border: 1px solid var(--border-active, var(--border));
48+
border-radius: 2px;
49+
background: var(--bg);
50+
color: var(--fg);
51+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.28);
52+
padding: 4px;
53+
z-index: 20;
54+
}
55+
56+
.list {
57+
list-style: none;
58+
margin: 0;
59+
padding: 0;
60+
}
61+
62+
.item {
63+
width: 100%;
64+
display: block;
65+
border: none;
66+
background: transparent;
67+
color: var(--fg-secondary);
68+
text-align: left;
69+
padding: 8px 10px;
70+
border-radius: 2px;
71+
font: inherit;
72+
cursor: pointer;
73+
}
74+
75+
.item:hover,
76+
.item:focus,
77+
.itemActive,
78+
.itemSelected {
79+
background: var(--entry-hover);
80+
color: var(--fg);
81+
outline: none;
82+
}

0 commit comments

Comments
 (0)