authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-08-29 03:34:41+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-29 03:34:41+05:30
log0009aacdf146e0532327fa3d9d0109d5adca68b9
tree9d327c813dbd317c160efd5c3e7e78705a0ea6bc
parent0e2905eb783fd2385f358a95f0b25bbf07ff7122
signature Signed by PGP key B5690EEEBB952194

fix(web): keep long task drawers usable on small screens (#8313)


2 files changed, 45 insertions(+), 4 deletions(-)

apps/web/src/components/chat/ComposerTasksBadge.test.tsx+30-1
...@@ -96,9 +96,19 @@ describe("ComposerTasksBadge", () => {...@@ -96,9 +96,19 @@ describe("ComposerTasksBadge", () => {
96 );96 );
9797
98 expect(markup).toContain('data-chat-composer-tasks-drawer="true"');98 expect(markup).toContain('data-chat-composer-tasks-drawer="true"');
99 expect(markup).toContain('data-chat-composer-collapsed-controls="true"');
99 expect(markup).not.toContain("data-variant");100 expect(markup).not.toContain("data-variant");
100 expect(markup).toContain('aria-expanded="true"');101 expect(markup).toContain('aria-expanded="true"');
102 expect(markup).toContain('aria-label="Collapse tasks. 1 of 3 complete."');
103 expect(markup).toContain('aria-label="Task list. 1 of 3 complete."');
101 expect(markup).toContain('role="list"');104 expect(markup).toContain('role="list"');
105 expect(markup).toContain('data-composer-tasks-list="true"');
106 expect(markup).toContain('tabindex="0"');
107 expect(markup).toContain("max-h-[min(24rem,40dvh)]");
108 expect(markup).toContain("overflow-y-auto");
109 expect(markup).toContain("overscroll-contain");
110 expect(markup).toContain("focus-visible:ring-2");
111 expect(markup).toContain("focus-visible:ring-inset");
102 expect(markup).toContain("Inspect the composer");112 expect(markup).toContain("Inspect the composer");
103 expect(markup).toContain('data-composer-task-duration="true"');113 expect(markup).toContain('data-composer-task-duration="true"');
104 expect(markup).toContain("ml-auto w-10");114 expect(markup).toContain("ml-auto w-10");
...@@ -107,13 +117,32 @@ describe("ComposerTasksBadge", () => {...@@ -107,13 +117,32 @@ describe("ComposerTasksBadge", () => {
107 expect(markup).toContain("Attach task progress");117 expect(markup).toContain("Attach task progress");
108 expect(markup).toContain("Verify the result");118 expect(markup).toContain("Verify the result");
109 expect(markup).toContain("lucide-list-todo");119 expect(markup).toContain("lucide-list-todo");
120 expect(markup).toContain("lucide-chevron-down");
110 expect(markup).toContain('aria-label="Dismiss tasks for this turn"');121 expect(markup).toContain('aria-label="Dismiss tasks for this turn"');
111 expect(markup).not.toContain("lucide-chevron");
112 expect(markup).not.toContain("bg-success");122 expect(markup).not.toContain("bg-success");
113 expect(markup).not.toContain("bg-primary");123 expect(markup).not.toContain("bg-primary");
114 expect(markup).not.toContain("bg-muted-foreground/25");124 expect(markup).not.toContain("bg-muted-foreground/25");
115 });125 });
116126
127 it("keeps every long-list task inside the bounded scroll region", () => {
128 const longSteps = Array.from({ length: 20 }, (_, index) => ({
129 step: `Task ${index + 1}`,
130 status: index === 0 ? ("inProgress" as const) : ("pending" as const),
131 }));
132 const markup = renderToStaticMarkup(
133 <ComposerTasksDrawer
134 onCollapse={() => undefined}
135 onDismiss={() => undefined}
136 progress={{ step: "Task 1", completedSteps: 0, totalSteps: longSteps.length }}
137 steps={longSteps}
138 />,
139 );
140
141 const listStart = markup.indexOf('data-composer-tasks-list="true"');
142 expect(listStart).toBeGreaterThan(markup.indexOf('aria-label="Collapse tasks.'));
143 expect(markup.slice(listStart)).toContain("Task 20");
144 });
145
117 it("does not render an empty task count", () => {146 it("does not render an empty task count", () => {
118 const markup = renderToStaticMarkup(147 const markup = renderToStaticMarkup(
119 <ComposerTasksBadge148 <ComposerTasksBadge
apps/web/src/components/chat/ComposerTasksBadge.tsx+15-3
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { ListTodoIcon, XIcon } from "lucide-react";1import { ChevronDownIcon, ListTodoIcon, XIcon } from "lucide-react";
2import { memo } from "react";2import { memo } from "react";
33
4import { formatDuration } from "../../session-logic";4import { formatDuration } from "../../session-logic";
...@@ -174,11 +174,16 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({...@@ -174,11 +174,16 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
174 readonly steps: readonly ComposerTaskStep[];174 readonly steps: readonly ComposerTaskStep[];
175}) {175}) {
176 return (176 return (
177 <div className="chat-composer-top-drawer" data-chat-composer-tasks-drawer="true">177 <div
178 className="chat-composer-top-drawer"
179 data-chat-composer-collapsed-controls="true"
180 data-chat-composer-tasks-drawer="true"
181 >
178 <div className="flex items-center gap-1 px-3 py-1.5 sm:px-4">182 <div className="flex items-center gap-1 px-3 py-1.5 sm:px-4">
179 <button183 <button
180 type="button"184 type="button"
181 aria-expanded="true"185 aria-expanded="true"
186 aria-label={`Collapse tasks. ${progress.completedSteps} of ${progress.totalSteps} complete.`}
182 className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 self-stretch text-left text-xs text-muted-foreground hover:text-foreground"187 className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 self-stretch text-left text-xs text-muted-foreground hover:text-foreground"
183 onClick={onCollapse}188 onClick={onCollapse}
184 onPointerDown={(event) => event.preventDefault()}189 onPointerDown={(event) => event.preventDefault()}
...@@ -188,6 +193,7 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({...@@ -188,6 +193,7 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
188 <span className="tabular-nums">193 <span className="tabular-nums">
189 {progress.completedSteps}/{progress.totalSteps}194 {progress.completedSteps}/{progress.totalSteps}
190 </span>195 </span>
196 <ChevronDownIcon aria-hidden className="ml-auto size-3.5 shrink-0" />
191 </button>197 </button>
192 <Button198 <Button
193 size="icon-micro"199 size="icon-micro"
...@@ -200,7 +206,13 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({...@@ -200,7 +206,13 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
200 <XIcon aria-hidden className="size-3" />206 <XIcon aria-hidden className="size-3" />
201 </Button>207 </Button>
202 </div>208 </div>
203 <div className="space-y-px px-3 pb-4 sm:px-4" role="list">209 <div
210 aria-label={`Task list. ${progress.completedSteps} of ${progress.totalSteps} complete.`}
211 className="max-h-[min(24rem,40dvh)] space-y-px overflow-y-auto overscroll-contain px-3 pb-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70 sm:px-4"
212 data-composer-tasks-list="true"
213 role="list"
214 tabIndex={0}
215 >
204 {keyedTaskSteps(steps).map(({ key, step }) => (216 {keyedTaskSteps(steps).map(({ key, step }) => (
205 <div key={key} className="flex items-baseline gap-2 text-xs leading-5" role="listitem">217 <div key={key} className="flex items-baseline gap-2 text-xs leading-5" role="listitem">
206 <span218 <span