refactor(forms): migrate mood and measurement row-edit dialogs to ResponsiveSheet

The mood-list and measurement-list edit dialogs were raw <Dialog> mounts, so
on phones the user faced a centred dialog with the keyboard pushing the form
off-screen instead of the bottom-sheet branch the rest of the app uses.

Both row-edit surfaces now mount <ResponsiveSheet>, with the Save / Cancel
row portalled into the footer slot so the bottom-sheet branch can sticky-pin
the action row. The submit button keeps its <form> association via the HTML
form attribute so submit-on-Enter and native validation continue to work.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 21:11:39 +02:00
1 parent 67fddb98e2
commit 0baea139a3
2 files changed
+158 -130

No files matched your search

@@ -23,12 +23,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
DropdownMenuContent,
@@ -59,6 +54,7 @@ import {
MoreHorizontal,
} from "lucide-react";
import { useId, useState } from "react";
import { createPortal } from "react-dom";
import { formatDateTime } from "@/lib/format";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
@@ -155,6 +151,11 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
// failure when the user submits an invalid value or timestamp.
const editErrorId = useId();
const editErrorDescriptor = editError ? editErrorId : undefined;
// v1.4.27 R4 RC2 — Sheet-branch sticky-pinned footer slot.
const editFormId = useId();
const [editFooterEl, setEditFooterEl] = useState<HTMLDivElement | null>(
null,
);
const setTypeFilter = (value: string) => {
setTypeFilterRaw(value);
@@ -604,13 +605,18 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
)}
</div>
<Dialog open={!!editing} onOpenChange={(open) => !open && closeEdit()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("measurements.editMeasurement")}</DialogTitle>
</DialogHeader>
<ResponsiveSheet
open={!!editing}
onOpenChange={(open) => !open && closeEdit()}
title={t("measurements.editMeasurement")}
footer={<div ref={setEditFooterEl} className="flex w-full" />}
>
{editing && (
<form onSubmit={submitEdit} className="space-y-4">
<form
id={editFormId}
onSubmit={submitEdit}
className="space-y-4"
>
<div className="flex items-center gap-1.5">
<Label className="shrink-0">{t("measurements.type")}</Label>
<span className="text-sm leading-none font-medium">
@@ -683,57 +689,66 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
</div>
)}
<div className="flex items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="size-11"
disabled={
updateMutation.isPending || deleteMutation.isPending
}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => setEditDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{editFooterEl
? createPortal(
<div className="flex w-full items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="size-11"
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => setEditDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={closeEdit}
disabled={
updateMutation.isPending || deleteMutation.isPending
}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
disabled={
updateMutation.isPending || deleteMutation.isPending
}
>
{updateMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : null}
{t("common.save")}
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={closeEdit}
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
form={editFormId}
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
>
{updateMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : null}
{t("common.save")}
</Button>
</div>
</div>,
editFooterEl,
)
: null}
<AlertDialog
open={editDeleteDialogOpen}
@@ -767,8 +782,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
</AlertDialog>
</form>
)}
</DialogContent>
</Dialog>
</ResponsiveSheet>
</>
);
}
+80 -66
View File
@@ -23,12 +23,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
DropdownMenuContent,
@@ -59,7 +54,8 @@ import {
ArrowUpDown,
MoreHorizontal,
} from "lucide-react";
import { useState } from "react";
import { useId, useState } from "react";
import { createPortal } from "react-dom";
import { formatDateTime } from "@/lib/format";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import {
@@ -126,6 +122,11 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
const [editMoodLoggedAt, setEditMoodLoggedAt] = useState("");
const [editError, setEditError] = useState<string | null>(null);
const [editDeleteDialogOpen, setEditDeleteDialogOpen] = useState(false);
// v1.4.27 R4 RC2 — Sheet-branch sticky-pinned footer slot.
const editFormId = useId();
const [editFooterEl, setEditFooterEl] = useState<HTMLDivElement | null>(
null,
);
const setMoodFilter = (value: string) => {
setMoodFilterRaw(value);
@@ -517,14 +518,19 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
)}
</div>
{/* Edit dialog */}
<Dialog open={!!editing} onOpenChange={(open) => !open && closeEdit()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("mood.editEntry")}</DialogTitle>
</DialogHeader>
{/* Edit sheet — bottom-sheet on `<md`, centred Dialog on `md+`. */}
<ResponsiveSheet
open={!!editing}
onOpenChange={(open) => !open && closeEdit()}
title={t("mood.editEntry")}
footer={<div ref={setEditFooterEl} className="flex w-full" />}
>
{editing && (
<form onSubmit={submitEdit} className="space-y-4">
<form
id={editFormId}
onSubmit={submitEdit}
className="space-y-4"
>
<div className="space-y-2">
<Label id="edit-mood-level-label">{t("mood.moodLevel")}</Label>
<div
@@ -598,57 +604,66 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
</div>
)}
<div className="flex items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9"
disabled={
updateMutation.isPending || deleteMutation.isPending
}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => setEditDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{editFooterEl
? createPortal(
<div className="flex w-full items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9"
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => setEditDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={closeEdit}
disabled={
updateMutation.isPending || deleteMutation.isPending
}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
disabled={
updateMutation.isPending || deleteMutation.isPending
}
>
{updateMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : null}
{t("common.save")}
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={closeEdit}
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
form={editFormId}
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
>
{updateMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : null}
{t("common.save")}
</Button>
</div>
</div>,
editFooterEl,
)
: null}
<AlertDialog
open={editDeleteDialogOpen}
@@ -682,8 +697,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
</AlertDialog>
</form>
)}
</DialogContent>
</Dialog>
</ResponsiveSheet>
</>
);
}