fix(sheets): put a swiped sheet back when it asks before closing

A swipe past the close point paints the sheet where the finger let go and
asks the owner to close it. A sheet holding unsaved input asks about
discarding first and stays open, so it sat half pushed off the screen
behind the question. One frame after the close, a sheet whose content is
still open now glides back to its resting place. The function's size
baseline rises by the seven lines this takes.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 13:07:54 +01:00
1 parent 5533b6edf9
commit 8f3b640ab6
3 files changed
+37 -2

No files matched your search

@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 336,
"total": 109378,
"total": 109385,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -231,7 +231,7 @@
"components/vaccinations/custom-vaccine-sheet.tsx :: CustomVaccineSheet": 246,
"components/vaccinations/vaccination-form.tsx :: VaccinationForm": 218,
"components/workouts/manual-workout-form.tsx :: ManualWorkoutForm": 321,
"hooks/use-sheet-drag.ts :: createSheetDrag": 312,
"hooks/use-sheet-drag.ts :: createSheetDrag": 319,
"lib/activity/calendar-math.ts :: buildActivityCalendar": 204,
"lib/ai/anthropic-client.ts :: generateCompletion": 294,
"lib/ai/coach/cycle-snapshot.ts :: buildCycleSnapshotBlock": 202,
@@ -23,6 +23,9 @@ interface FakeEl {
parentElement: FakeEl | null;
overflowY: string;
own?: boolean;
/** Radix's `data-state` on the sheet content. */
state?: "open" | "closed";
getAttribute: (name: string) => string | null;
closest: (selector: string) => FakeEl | null;
getAnimations: () => Animation[];
setPointerCapture: (id: number) => void;
@@ -39,6 +42,8 @@ function fake(partial: Partial<FakeEl> = {}): FakeEl {
parentElement: null,
overflowY: "visible",
closest: () => (node.own ? node : null),
getAttribute: (name) =>
name === "data-state" ? (node.state ?? null) : null,
getAnimations: () => [],
setPointerCapture: vi.fn(),
querySelector: () => null,
@@ -173,6 +178,29 @@ describe("createSheetDrag", () => {
expect(s.controller.swallowClick()).toBe(false);
});
it("returns to its rest when a guarded sheet stays open after the swipe", () => {
const s = setup();
// The owner asks about unsaved input instead of closing.
s.content.state = "open";
s.drag(s.header, 140, { stepMs: 40, restMs: 200 });
expect(s.onClose).toHaveBeenCalledTimes(1);
expect(s.content.style.transform).toBe("translate3d(0, 140px, 0)");
s.flushFrames();
expect(s.content.style.transform).toBe("translate3d(0, 0px, 0)");
expect(s.overlay.style.opacity).toBe("1");
s.timers.forEach((run) => run());
expect(s.content.style.transform).toBe("");
});
it("leaves a closing sheet where it was let go", () => {
const s = setup();
s.content.state = "closed";
s.drag(s.header, 140, { stepMs: 40, restMs: 200 });
s.flushFrames();
expect(s.content.style.transform).toBe("translate3d(0, 140px, 0)");
});
it("lets its own close through: the swallowed click comes after it", () => {
const s = setup();
let swallowedDuringClose: boolean | null = null;
+7
View File
@@ -354,6 +354,13 @@ export function createSheetDrag(
// Before the swallow is armed: the close is itself a click.
options.onClose();
swallow = true;
// A sheet holding unsaved input asks first and stays open meanwhile;
// put it back where it rests rather than where it was let go.
frame(() => {
if (options.content()?.getAttribute("data-state") === "open") {
settleTo(g, options.snap());
}
});
return;
}
swallow = true;