mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Settle the form sheets on one anatomy
Field labels no longer lose the space before '(optional)' or set the colon apart from a unit, and a question label carries no colon after its question mark. Cancel next to a primary is the outline button in every form, the illness sheet's selects span the field width, and the reminder sheet drops a description that repeated the page subtitle. On a phone a sheet now opens with focus on itself rather than its first field, so a date or name field no longer raises the keyboard over a sheet nobody has read yet.
This commit is contained in:
1 parent
0b881fe5b7
commit
03f374794e
15 files changed
+84
-31
No files matched your search
@@ -114,7 +114,7 @@ export function CustomMetricEntryForm({
|
||||
{onCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -137,7 +137,7 @@ export function CustomMetricForm({
|
||||
{onCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -255,7 +255,7 @@ export function LogDaySheet({
|
||||
description={t("illness.sheet.description")}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={upsert.isPending}>
|
||||
|
||||
@@ -178,7 +178,7 @@ export function NewEpisodeSheet({
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -203,9 +203,9 @@ export function NewEpisodeSheet({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("illness.new.type")}</Label>
|
||||
<Label htmlFor="illness-type">{t("illness.new.type")}</Label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as IllnessType)}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger id="illness-type" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -228,12 +228,14 @@ export function NewEpisodeSheet({
|
||||
/>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("illness.new.lifecycle")}</Label>
|
||||
<Label htmlFor="illness-lifecycle">
|
||||
{t("illness.new.lifecycle")}
|
||||
</Label>
|
||||
<Select
|
||||
value={lifecycle}
|
||||
onValueChange={(v) => setLifecycle(v as IllnessLifecycle)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger id="illness-lifecycle" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -248,9 +250,9 @@ export function NewEpisodeSheet({
|
||||
|
||||
{showParent ? (
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("illness.new.parent")}</Label>
|
||||
<Label htmlFor="illness-parent">{t("illness.new.parent")}</Label>
|
||||
<Select value={parentId} onValueChange={setParentId}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger id="illness-parent" className="w-full">
|
||||
<SelectValue placeholder={t("illness.new.parentNone")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -144,7 +144,7 @@ export function BiomarkerForm({
|
||||
{onCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -348,7 +348,7 @@ export function LabForm({
|
||||
{onCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -480,7 +480,6 @@ export function VorsorgeSection({
|
||||
? "measurementReminders.form.editTitle"
|
||||
: "measurementReminders.form.createTitle",
|
||||
)}
|
||||
description={t("measurementReminders.sectionDescription")}
|
||||
footer={
|
||||
<>
|
||||
<Button type="button" variant="outline" onClick={closeSheet}>
|
||||
|
||||
@@ -300,7 +300,9 @@ function UnitLabel({ htmlFor, label }: { htmlFor: string; label: string }) {
|
||||
const name = match ? match[1] : label;
|
||||
const unit = match ? match[2] : null;
|
||||
return (
|
||||
<Label htmlFor={htmlFor} className="flex items-baseline gap-1">
|
||||
// `after:-ml-1` pulls the label's colon back against the unit: the colon
|
||||
// is a flex item of its own here, and the row gap set it apart.
|
||||
<Label htmlFor={htmlFor} className="flex items-baseline gap-1 after:-ml-1">
|
||||
<span className="truncate">{name}</span>
|
||||
{unit ? (
|
||||
<span className="text-muted-foreground shrink-0 text-xs font-normal">
|
||||
@@ -773,8 +775,8 @@ export function MeasurementForm({
|
||||
htmlFor="notes"
|
||||
label={
|
||||
<>
|
||||
{t("measurements.notes")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{t("measurements.notes")}
|
||||
<span className="text-muted-foreground ml-1 font-normal">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -353,6 +353,7 @@ export function MoodForm({
|
||||
doubled heading. */}
|
||||
<Label
|
||||
id="mood-level-label"
|
||||
noColon
|
||||
className="text-muted-foreground text-sm font-normal"
|
||||
>
|
||||
{t("mood.heroQuestion")}
|
||||
@@ -479,8 +480,8 @@ export function MoodForm({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Label htmlFor="tags">
|
||||
{t("mood.tags")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{t("mood.tags")}
|
||||
<span className="text-muted-foreground ml-1 font-normal">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
</Label>
|
||||
@@ -554,8 +555,8 @@ export function MoodForm({
|
||||
htmlFor="mood-note"
|
||||
label={
|
||||
<>
|
||||
{t("mood.note")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{t("mood.note")}
|
||||
<span className="text-muted-foreground ml-1 font-normal">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -128,7 +128,7 @@ export function AllergyForm({
|
||||
{onCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -114,7 +114,7 @@ export function FamilyHistoryForm({
|
||||
{onCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function EditSymptomEventSheet({
|
||||
title={t("symptoms.section.editEventTitle", { symptom: label })}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={update.isPending}>
|
||||
@@ -128,8 +128,8 @@ export function EditSymptomEventSheet({
|
||||
htmlFor="symptom-event-note"
|
||||
label={
|
||||
<>
|
||||
{t("symptoms.entry.note")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{t("symptoms.entry.note")}
|
||||
<span className="text-muted-foreground ml-1 font-normal">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
</>
|
||||
@@ -194,7 +194,7 @@ export function EditSymptomDefinitionSheet({
|
||||
title={t("symptoms.section.editSymptomTitle")}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={update.isPending}>
|
||||
|
||||
@@ -347,8 +347,8 @@ export function SymptomEntryForm({
|
||||
htmlFor={`${formId}-episode`}
|
||||
label={
|
||||
<>
|
||||
{t("symptoms.entry.during")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{t("symptoms.entry.during")}
|
||||
<span className="text-muted-foreground ml-1 font-normal">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
</>
|
||||
@@ -376,8 +376,8 @@ export function SymptomEntryForm({
|
||||
htmlFor={`${formId}-note`}
|
||||
label={
|
||||
<>
|
||||
{t("symptoms.entry.note")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{t("symptoms.entry.note")}
|
||||
<span className="text-muted-foreground ml-1 font-normal">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -105,7 +105,7 @@ vi.mock("@/components/ui/dialog", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
import { ResponsiveSheet } from "../responsive-sheet";
|
||||
import { keepKeyboardDownOnOpen, ResponsiveSheet } from "../responsive-sheet";
|
||||
|
||||
describe("<ResponsiveSheet>", () => {
|
||||
it("renders the Dialog branch on `md+` viewports", () => {
|
||||
@@ -324,3 +324,34 @@ describe("<ResponsiveSheet>", () => {
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keepKeyboardDownOnOpen (phone branch open focus)", () => {
|
||||
function setup(activeInside: boolean) {
|
||||
const focus = vi.fn();
|
||||
const preventDefault = vi.fn();
|
||||
const active = { id: "active" };
|
||||
const content = {
|
||||
focus,
|
||||
contains: (node: unknown) => activeInside && node === active,
|
||||
};
|
||||
vi.stubGlobal("document", { activeElement: active });
|
||||
keepKeyboardDownOnOpen({
|
||||
preventDefault,
|
||||
currentTarget: content,
|
||||
} as unknown as Event);
|
||||
vi.unstubAllGlobals();
|
||||
return { focus, preventDefault };
|
||||
}
|
||||
|
||||
it("focuses the sheet itself instead of its first field", () => {
|
||||
const { focus, preventDefault } = setup(false);
|
||||
expect(preventDefault).toHaveBeenCalledOnce();
|
||||
expect(focus).toHaveBeenCalledWith({ preventScroll: true });
|
||||
});
|
||||
|
||||
it("leaves a field the form focused on purpose alone", () => {
|
||||
const { focus, preventDefault } = setup(true);
|
||||
expect(preventDefault).toHaveBeenCalledOnce();
|
||||
expect(focus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -131,6 +131,23 @@ const CONTENT_WIDTH_CLASS: Record<
|
||||
* the form's chrome is unchanged because both branches expose the
|
||||
* same header / body / footer slot shape.
|
||||
*/
|
||||
/**
|
||||
* Phone branch only: on open, focus the sheet itself instead of its first
|
||||
* field. Radix's default lands on the first tabbable element, and when that
|
||||
* is a text-like field (a date, a name) the phone raises its keyboard over a
|
||||
* sheet the person has not read yet, and the sheet jumps up under it. Focus
|
||||
* still moves into the sheet (the content is focusable and traps focus), a
|
||||
* field a form focuses on purpose (`autoFocus`, a mount effect) keeps it, and
|
||||
* the desktop dialog keeps the default.
|
||||
*/
|
||||
export function keepKeyboardDownOnOpen(event: Event): void {
|
||||
event.preventDefault();
|
||||
const content = event.currentTarget as HTMLElement | null;
|
||||
if (content && !content.contains(document.activeElement)) {
|
||||
content.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function ResponsiveSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -153,6 +170,7 @@ export function ResponsiveSheet({
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
onCloseAutoFocus={onCloseAutoFocus}
|
||||
onOpenAutoFocus={keepKeyboardDownOnOpen}
|
||||
side="bottom"
|
||||
showCloseButton={showCloseButton}
|
||||
data-slot="responsive-sheet-content"
|
||||
|
||||
Reference in new issue
Block a user