Skip to content

Commit 12ad35e

Browse files
committed
plugin: implement collapsible headers
1 parent 49f2695 commit 12ad35e

File tree

2 files changed

+71
-6
lines changed

2 files changed

+71
-6
lines changed
Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type React from "react";
2+
import { useState } from "react";
23

34
import type { Task } from "@/data/task";
45
import { groupBy } from "@/data/transformations/grouping";
6+
import { ObsidianIcon } from "@/ui/components/obsidian-icon";
57
import { QueryContext } from "@/ui/context";
68
import { ListDisplay } from "@/ui/query/displays/ListDisplay";
79

@@ -12,15 +14,50 @@ type Props = {
1214
export const GroupedDisplay: React.FC<Props> = ({ tasks }) => {
1315
const query = QueryContext.use();
1416
const groups = groupBy(tasks, query.groupBy);
17+
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
18+
19+
const toggleGroup = (groupHeader: string) => {
20+
setCollapsedGroups((prev) => {
21+
const newState = { ...prev };
22+
if (newState[groupHeader]) {
23+
delete newState[groupHeader];
24+
} else {
25+
newState[groupHeader] = true;
26+
}
27+
return newState;
28+
});
29+
};
1530

1631
return (
1732
<>
18-
{groups.map((group) => (
19-
<div className="todoist-group" key={group.header}>
20-
<div className="todoist-group-title">{group.header}</div>
21-
<ListDisplay tasks={group.tasks} />
22-
</div>
23-
))}
33+
{groups.map((group) => {
34+
const isCollapsed = group.header in collapsedGroups;
35+
return (
36+
<div className="todoist-group" key={group.header}>
37+
{/* biome-ignore lint/a11y/useSemanticElements: Keeping as div to preserve CSS styling */}
38+
<div
39+
className={`todoist-group-title ${isCollapsed ? "collapsed" : ""}`}
40+
onClick={() => toggleGroup(group.header)}
41+
onKeyDown={(e) => {
42+
if (e.key === "Enter" || e.key === " ") {
43+
e.preventDefault();
44+
toggleGroup(group.header);
45+
}
46+
}}
47+
role="button"
48+
tabIndex={0}
49+
>
50+
<span>{group.header}</span>
51+
<ObsidianIcon
52+
size="s"
53+
id={isCollapsed ? "chevron-right" : "chevron-down"}
54+
className="todoist-group-collapse-icon"
55+
/>
56+
</div>
57+
{!isCollapsed && <ListDisplay tasks={group.tasks} />}
58+
</div>
59+
);
60+
})}
2461
</>
2562
);
2663
};

plugin/src/ui/query/styles.scss

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,34 @@
188188
.todoist-group-title {
189189
margin: 1em 0;
190190
font-weight: 600;
191+
cursor: pointer;
192+
display: flex;
193+
align-items: center;
194+
gap: 0.5em;
195+
outline: none;
196+
197+
&.collapsed {
198+
border-bottom: 1px solid var(--todoist-task-separator-color);
199+
margin-bottom: 0;
200+
padding-bottom: 0.5em;
201+
}
202+
203+
.todoist-group-collapse-icon {
204+
flex-shrink: 0;
205+
transition: all 0.2s ease;
206+
border-radius: 3px;
207+
padding: 2px;
208+
}
209+
210+
&:focus .todoist-group-collapse-icon {
211+
background-color: var(--interactive-hover);
212+
outline: 2px solid var(--interactive-accent);
213+
outline-offset: 1px;
214+
}
215+
216+
&:hover .todoist-group-collapse-icon {
217+
background-color: var(--interactive-hover);
218+
}
191219
}
192220

193221
.todoist-no-tasks {

0 commit comments

Comments
 (0)