Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 2fc79c8

Browse files
authored
Make questionnaire cards vertically resizable
Adds a drag handle to the top edge of the question permission card so users can shrink it to read more of the transcript above before answering, then grow it back (capped at 80vh). Inner content scrolls within the chosen height. Implemented as an opt-in `resizable` prop on the shared ActionSelector primitive, enabled by QuestionPermission. Generated-By: PostHog Code Task-Id: 6b270274-048e-4383-b258-62703ac84b9a
1 parent fc33dfb commit 2fc79c8

3 files changed

Lines changed: 101 additions & 2 deletions

File tree

packages/ui/src/features/permissions/QuestionPermission.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export function QuestionPermission({
309309
}
310310
multiSelect={isOnSubmitStep ? false : isMultiSelect}
311311
hideSubmitButton={isOnSubmitStep}
312+
resizable
312313
allowCustomInput={!isOnSubmitStep}
313314
customInputPlaceholder="Type your answer..."
314315
currentStep={activeStep}

packages/ui/src/primitives/action-selector/ActionSelector.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { compactHomePath } from "@posthog/shared";
22
import { Box, Flex, Text } from "@radix-ui/themes";
3-
import { useCallback, useEffect, useRef } from "react";
3+
import { useCallback, useEffect, useRef, useState } from "react";
44
import { isCancelOption, isSubmitOption } from "./constants";
55
import { OptionRow } from "./OptionRow";
66
import { StepTabs } from "./StepTabs";
77
import type { ActionSelectorProps } from "./types";
88
import { useActionSelectorState } from "./useActionSelectorState";
99

10+
// Floor keeps the options and submit row visible even at the smallest size;
11+
// ceiling matches the card's default `max-h-[80vh]` cap.
12+
const MIN_CARD_HEIGHT = 160;
13+
const MAX_CARD_HEIGHT_FRACTION = 0.8;
14+
1015
export function ActionSelector({
1116
title,
1217
pendingAction,
@@ -20,6 +25,7 @@ export function ActionSelector({
2025
initialSelections,
2126
initialCustomInput,
2227
hideSubmitButton = false,
28+
resizable = false,
2329
onSelect,
2430
onMultiSelect,
2531
onCancel,
@@ -76,6 +82,68 @@ export function ActionSelector({
7682
onCancel?.();
7783
}, [onCancel]);
7884

85+
// User-chosen height in px once the card has been dragged; null means the
86+
// card sizes naturally under its `max-h-[80vh]` cap.
87+
const [cardHeight, setCardHeight] = useState<number | null>(null);
88+
const [isResizing, setIsResizing] = useState(false);
89+
const resizeStartRef = useRef({ y: 0, height: 0 });
90+
91+
const handleResizeMouseDown = useCallback(
92+
(e: React.MouseEvent) => {
93+
const container = containerRef.current;
94+
if (!container) return;
95+
e.preventDefault();
96+
resizeStartRef.current = {
97+
y: e.clientY,
98+
height: container.getBoundingClientRect().height,
99+
};
100+
setIsResizing(true);
101+
document.body.style.cursor = "row-resize";
102+
document.body.style.userSelect = "none";
103+
},
104+
[containerRef],
105+
);
106+
107+
useEffect(() => {
108+
if (!isResizing) return;
109+
110+
const handleMouseMove = (e: MouseEvent) => {
111+
const { y, height } = resizeStartRef.current;
112+
// Dragging up (clientY decreases) grows the card; down shrinks it,
113+
// revealing more of the transcript above.
114+
const next = height + (y - e.clientY);
115+
const max = window.innerHeight * MAX_CARD_HEIGHT_FRACTION;
116+
setCardHeight(Math.max(MIN_CARD_HEIGHT, Math.min(max, next)));
117+
};
118+
119+
const handleMouseUp = () => {
120+
setIsResizing(false);
121+
document.body.style.cursor = "";
122+
document.body.style.userSelect = "";
123+
};
124+
125+
document.addEventListener("mousemove", handleMouseMove);
126+
document.addEventListener("mouseup", handleMouseUp);
127+
return () => {
128+
document.removeEventListener("mousemove", handleMouseMove);
129+
document.removeEventListener("mouseup", handleMouseUp);
130+
};
131+
}, [isResizing]);
132+
133+
// If the card unmounts mid-drag no mouseup fires — clear the global cursor
134+
// and text-selection lock so the app isn't left stuck.
135+
const isResizingRef = useRef(isResizing);
136+
isResizingRef.current = isResizing;
137+
useEffect(
138+
() => () => {
139+
if (isResizingRef.current) {
140+
document.body.style.cursor = "";
141+
document.body.style.userSelect = "";
142+
}
143+
},
144+
[],
145+
);
146+
79147
const handlersRef = useRef({
80148
moveUp,
81149
moveDown,
@@ -226,9 +294,32 @@ export function ActionSelector({
226294
}}
227295
style={{
228296
outline: "none",
297+
...(resizable && cardHeight !== null ? { height: cardHeight } : {}),
229298
}}
230-
className="flex max-h-[80vh] flex-col rounded-(--radius-3) border border-(--gray-6) bg-(--gray-1)"
299+
className="relative flex max-h-[80vh] flex-col rounded-(--radius-3) border border-(--gray-6) bg-(--gray-1)"
231300
>
301+
{resizable && (
302+
// Drag handle riding the top edge — the card is anchored to the bottom
303+
// of the chat, so dragging up grows it and dragging down shrinks it.
304+
<Box
305+
aria-hidden
306+
onMouseDown={handleResizeMouseDown}
307+
className="group absolute inset-x-0 top-0 z-10 flex h-2 cursor-row-resize items-start justify-center"
308+
>
309+
<span
310+
className={`mt-0.5 h-1 w-10 rounded-full transition-colors ${
311+
isResizing
312+
? "bg-(--gray-8)"
313+
: "bg-(--gray-6) group-hover:bg-(--gray-8)"
314+
}`}
315+
/>
316+
</Box>
317+
)}
318+
{isResizing && (
319+
// Keeps the row-resize cursor while the pointer crosses content that
320+
// sets its own cursor.
321+
<Box className="fixed inset-0 z-[200] cursor-row-resize" />
322+
)}
232323
<Flex direction="column" gap="2" className="min-h-0 flex-1">
233324
{hasSteps && steps && (
234325
<StepTabs

packages/ui/src/primitives/action-selector/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export interface ActionSelectorProps {
3030
initialSelections?: string[];
3131
initialCustomInput?: string;
3232
hideSubmitButton?: boolean;
33+
/**
34+
* When true, the card can be resized vertically via a drag handle at its top
35+
* edge: dragging down shrinks it (revealing more of the transcript above),
36+
* dragging up grows it back (capped at 80vh). Inner content scrolls within
37+
* the chosen height.
38+
*/
39+
resizable?: boolean;
3340
onSelect: (optionId: string, customInput?: string) => void;
3441
onMultiSelect?: (optionIds: string[], customInput?: string) => void;
3542
onCancel?: () => void;

0 commit comments

Comments
 (0)