Skip to content

Commit da1b1e4

Browse files
authored
Merge pull request #1252 from MarechJ/dev/dont-force-expand-menu-tree
enhance to allow search in menu-tree to expand tree as necessary.
2 parents 390c9cd + 572a3d5 commit da1b1e4

File tree

3 files changed

+108
-12
lines changed

3 files changed

+108
-12
lines changed

rcongui/src/components/layout/MenuContent.js

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import Stack from "@mui/material/Stack";
77
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
88
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
99
import { Link } from "react-router-dom";
10-
import { Collapse, TextField, InputAdornment, IconButton } from "@mui/material";
11-
import {useState} from "react";
10+
import { Collapse, TextField, InputAdornment, IconButton, Box } from "@mui/material";
11+
import { useEffect, useMemo, useRef, useState } from "react";
1212
import { useAppStore } from "@/stores/app-state";
1313
import SearchIcon from "@mui/icons-material/Search";
1414
import CloseIcon from "@mui/icons-material/Close";
@@ -25,21 +25,17 @@ const NavigationLink = ({ to, icon, text, onClick }) => {
2525
);
2626
};
2727

28-
const Group = ({ groupName, icon, level = 1, children }) => {
29-
const [open, setOpen] = useState(true);
30-
31-
const handleClick = () => {
32-
setOpen(!open);
33-
};
28+
const Group = ({ groupName, icon, level = 1, open, forceOpen = false, onToggle, children }) => {
29+
const isOpen = forceOpen || open;
3430

3531
return (
3632
<ListItem key={groupName} disablePadding sx={{ display: "block" }}>
37-
<ListItemButton onClick={handleClick}>
33+
<ListItemButton onClick={onToggle}>
3834
{icon && <ListItemIcon>{icon}</ListItemIcon>}
3935
<ListItemText primary={groupName} />
40-
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
36+
{isOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
4137
</ListItemButton>
42-
<Collapse in={open} timeout={"auto"} unmountOnExit>
38+
<Collapse in={isOpen} timeout={"auto"} unmountOnExit>
4339
<List
4440
dense
4541
sx={{ "& .MuiListItemButton-root": { pl: 2 * level } }}
@@ -54,7 +50,46 @@ const Group = ({ groupName, icon, level = 1, children }) => {
5450

5551
export default function MenuContent({ navigationTree, isMobile }) {
5652
const toggleDrawer = useAppStore((state) => state.toggleDrawer);
53+
const groupOpenState = useAppStore((state) => state.groupOpenState);
54+
const toggleMenuGroupOpen = useAppStore((state) => state.toggleMenuGroupOpen);
5755
const [searchTerm, setSearchTerm] = useState("");
56+
const searchInputRef = useRef(null);
57+
58+
const isApplePlatform = useMemo(() => {
59+
if (typeof navigator === "undefined") return false;
60+
const ua = navigator.userAgent || "";
61+
const uaPlatform = navigator.userAgentData?.platform || "";
62+
return /mac|iphone|ipad|ipod/i.test(uaPlatform) || /Mac OS X|Macintosh|iPhone|iPad|iPod/i.test(ua);
63+
}, []);
64+
65+
const keyHintLabel = isApplePlatform ? "⌘K" : "Ctrl+K";
66+
67+
useEffect(() => {
68+
const onKeyDown = (e) => {
69+
const key = (e.key || "").toLowerCase();
70+
if (key !== "k") return;
71+
72+
const comboPressed = isApplePlatform ? e.metaKey : e.ctrlKey;
73+
if (!comboPressed) return;
74+
75+
e.preventDefault();
76+
e.stopPropagation();
77+
78+
const el = searchInputRef.current;
79+
if (!el) return;
80+
el.focus();
81+
if (typeof el.select === "function") el.select();
82+
};
83+
84+
window.addEventListener("keydown", onKeyDown, { capture: true });
85+
return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
86+
}, [isApplePlatform]);
87+
88+
const toggleGroup = (groupName) => {
89+
toggleMenuGroupOpen(groupName);
90+
};
91+
92+
const isGroupOpen = (groupName) => !!groupOpenState[groupName];
5893

5994
const handleLinkClick = () => {
6095
if (isMobile) {
@@ -122,6 +157,8 @@ export default function MenuContent({ navigationTree, isMobile }) {
122157
size="small"
123158
fullWidth
124159
placeholder="Search..."
160+
name="crcon-search"
161+
inputRef={searchInputRef}
125162
value={searchTerm}
126163
onChange={(e) => setSearchTerm(e.target.value)}
127164
sx={{ mb: 1 }}
@@ -132,6 +169,40 @@ export default function MenuContent({ navigationTree, isMobile }) {
132169
<SearchIcon fontSize="small" />
133170
</InputAdornment>
134171
),
172+
endAdornment: (
173+
<InputAdornment position="end">
174+
<Box
175+
aria-hidden
176+
onMouseDown={(e) => {
177+
// Keep focus in the input when clicking the hint.
178+
e.preventDefault();
179+
searchInputRef.current?.focus?.();
180+
}}
181+
sx={{
182+
display: "inline-flex",
183+
alignItems: "center",
184+
gap: 0.75,
185+
px: 0.75,
186+
py: 0.25,
187+
borderRadius: 1,
188+
border: "1px solid",
189+
borderColor: "divider",
190+
bgcolor: (theme) => theme.palette.background.paper,
191+
color: "text.secondary",
192+
fontSize: 12,
193+
lineHeight: 1,
194+
fontFamily:
195+
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
196+
letterSpacing: 0.2,
197+
userSelect: "none",
198+
boxShadow: (theme) =>
199+
`inset 0 1px 0 ${theme.palette.action.hover}, inset 0 -1px 0 ${theme.palette.action.selected}`,
200+
}}
201+
>
202+
{keyHintLabel}
203+
</Box>
204+
</InputAdornment>
205+
),
135206
}
136207
}}
137208
/>
@@ -159,7 +230,14 @@ export default function MenuContent({ navigationTree, isMobile }) {
159230
{filteredTree
160231
.filter((group) => "name" in group)
161232
.map((group) => (
162-
<Group key={group.name} groupName={group.name} icon={group.icon}>
233+
<Group
234+
key={group.name}
235+
groupName={group.name}
236+
icon={group.icon}
237+
open={isGroupOpen(group.name)}
238+
forceOpen={!!searchTerm.trim()}
239+
onToggle={() => toggleGroup(group.name)}
240+
>
163241
{group.links.map((link) => (
164242
<NavigationLink
165243
key={link.to}

rcongui/src/hooks/useAppState.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export const useAppStore = create(
1414
toggleWidthMode: () => set({ widthMode: get().widthMode === "xl" ? false : "xl" }),
1515
openDrawer: true,
1616
toggleDrawer: () => set({ openDrawer: !get().openDrawer }),
17+
// Persist which navigation groups are expanded.
18+
groupOpenState: {},
19+
toggleMenuGroupOpen: (groupName) =>
20+
set((state) => ({
21+
groupOpenState: {
22+
...state.groupOpenState,
23+
[groupName]: !state.groupOpenState?.[groupName],
24+
},
25+
})),
1726
}),
1827
{
1928
name: withPrefix("app"),

rcongui/src/stores/app-state.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export const useAppStore = create(
1414
toggleWidthMode: () => set({ widthMode: get().widthMode === "xl" ? false : "xl" }),
1515
openDrawer: true,
1616
toggleDrawer: () => set({ openDrawer: !get().openDrawer }),
17+
// Persist which navigation groups are expanded.
18+
groupOpenState: {},
19+
toggleMenuGroupOpen: (groupName) =>
20+
set((state) => ({
21+
groupOpenState: {
22+
...state.groupOpenState,
23+
[groupName]: !state.groupOpenState?.[groupName],
24+
},
25+
})),
1726
}),
1827
{
1928
name: withPrefix("app"),

0 commit comments

Comments
 (0)