feat(ui): use native mobile confirmation sheets (#3621)
This commit is contained in:
@@ -11,6 +11,10 @@ import {
|
||||
DrawerTrigger,
|
||||
} from "@kandev/ui/drawer";
|
||||
import { PresetsSidebar, type PresetsSidebarProps } from "./presets-sidebar";
|
||||
import {
|
||||
MobileConfirmationHost,
|
||||
MobileConfirmationHostBody,
|
||||
} from "@/components/confirmation/mobile-confirmation-host";
|
||||
|
||||
export function MobileViewsPicker({
|
||||
title,
|
||||
@@ -30,51 +34,58 @@ export function MobileViewsPicker({
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 min-w-0 w-full justify-between gap-2 text-sm font-normal"
|
||||
data-testid="github-mobile-menu-button"
|
||||
aria-label={t("github:viewPicker", { view: title })}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
<span className="text-muted-foreground">{t("github:views")}:</span>{" "}
|
||||
<span className="font-medium" data-testid="github-list-toolbar-title">
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
<IconChevronDown className="h-4 w-4 shrink-0" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent
|
||||
className="h-[88dvh] max-h-[88dvh] overflow-hidden pb-[max(0.5rem,env(safe-area-inset-bottom))]"
|
||||
data-testid="github-mobile-sidebar"
|
||||
aria-describedby={undefined}
|
||||
onCloseAutoFocus={() => {
|
||||
if (!saveAfterClose.current) return;
|
||||
saveAfterClose.current = false;
|
||||
saveFrame.current = requestAnimationFrame(onSaveCurrent);
|
||||
}}
|
||||
>
|
||||
<DrawerHeader className="flex-row shrink-0 items-center justify-between px-3 py-2">
|
||||
<DrawerTitle>{t("github:views")}</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="ghost">{t("task:done")}</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<PresetsSidebar
|
||||
{...props}
|
||||
onSelect={(selection) => {
|
||||
onSelect(selection);
|
||||
if (selection.source !== "kind-switch") setOpen(false);
|
||||
}}
|
||||
onSaveCurrent={() => {
|
||||
saveAfterClose.current = true;
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<MobileConfirmationHost open={open} surface="drawer">
|
||||
{({ contentProps }) => (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 min-w-0 w-full justify-between gap-2 text-sm font-normal"
|
||||
data-testid="github-mobile-menu-button"
|
||||
aria-label={t("github:viewPicker", { view: title })}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
<span className="text-muted-foreground">{t("github:views")}:</span>{" "}
|
||||
<span className="font-medium" data-testid="github-list-toolbar-title">
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
<IconChevronDown className="h-4 w-4 shrink-0" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent
|
||||
className="h-[88dvh] max-h-[88dvh] overflow-hidden pb-[max(0.5rem,env(safe-area-inset-bottom))]"
|
||||
data-testid="github-mobile-sidebar"
|
||||
aria-describedby={undefined}
|
||||
{...contentProps}
|
||||
onCloseAutoFocus={() => {
|
||||
if (!saveAfterClose.current) return;
|
||||
saveAfterClose.current = false;
|
||||
saveFrame.current = requestAnimationFrame(onSaveCurrent);
|
||||
}}
|
||||
>
|
||||
<MobileConfirmationHostBody>
|
||||
<DrawerHeader className="flex-row shrink-0 items-center justify-between px-3 py-2">
|
||||
<DrawerTitle>{t("github:views")}</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="ghost">{t("task:done")}</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<PresetsSidebar
|
||||
{...props}
|
||||
onSelect={(selection) => {
|
||||
onSelect(selection);
|
||||
if (selection.source !== "kind-switch") setOpen(false);
|
||||
}}
|
||||
onSaveCurrent={() => {
|
||||
saveAfterClose.current = true;
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</MobileConfirmationHostBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
</MobileConfirmationHost>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SavedPreset } from "./saved-preset-model";
|
||||
import { PresetsSidebar } from "./presets-sidebar";
|
||||
|
||||
afterEach(cleanup);
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1024 });
|
||||
});
|
||||
|
||||
const savedPreset: SavedPreset = {
|
||||
id: "saved-pr",
|
||||
@@ -15,7 +18,8 @@ const savedPreset: SavedPreset = {
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
async function expectDeleteDoesNotSelect() {
|
||||
async function expectDeleteDoesNotSelect(phone = false) {
|
||||
if (phone) Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
|
||||
const onSelect = vi.fn();
|
||||
const onDeleteSaved = vi.fn();
|
||||
render(
|
||||
@@ -33,7 +37,14 @@ async function expectDeleteDoesNotSelect() {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Delete Kandev PRs saved query" }));
|
||||
const trigger = screen.getByRole("button", { name: "Delete Kandev PRs saved query" });
|
||||
fireEvent.click(trigger);
|
||||
if (phone) {
|
||||
expect(trigger.isConnected).toBe(true);
|
||||
expect(
|
||||
screen.getByRole("dialog", { name: "Delete Kandev PRs?" }).getAttribute("data-slot"),
|
||||
).toBe("drawer-content");
|
||||
}
|
||||
|
||||
expect(onDeleteSaved).not.toHaveBeenCalled();
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
@@ -121,5 +132,7 @@ describe("PresetsSidebar saved defaults", () => {
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes a saved query without selecting it", expectDeleteDoesNotSelect);
|
||||
it("deletes a saved query without selecting it", () => expectDeleteDoesNotSelect());
|
||||
it("keeps a phone saved query row mounted during confirmation", () =>
|
||||
expectDeleteDoesNotSelect(true));
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
|
||||
import { PR_PRESETS, ISSUE_PRESETS, type PresetOption, type PresetGroup } from "./search-bar";
|
||||
import type { SavedPreset } from "./saved-preset-model";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { SavedQueryDefaultButton } from "@/components/integrations/saved-query-default-button";
|
||||
import { SavedTaskViewDeleteConfirmation } from "@/components/confirmation/saved-task-view-delete-confirmation";
|
||||
import {
|
||||
@@ -165,6 +166,7 @@ function SavedSection({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const deletion = useSavedTaskViewDeleteConfirmation<HTMLButtonElement>(saved);
|
||||
const { isMobile } = useResponsiveBreakpoint();
|
||||
const defaultMutationPending = defaultMutationPendingId !== null;
|
||||
return (
|
||||
<>
|
||||
@@ -188,6 +190,19 @@ function SavedSection({
|
||||
onToggleDefault={() => void onToggleSavedDefault(preset)}
|
||||
/>
|
||||
))}
|
||||
{isMobile && deletion.target && (
|
||||
<SavedTaskViewDeleteConfirmation
|
||||
target={deletion.target}
|
||||
presentation="inline"
|
||||
open
|
||||
anchorRef={deletion.anchorRef}
|
||||
confirmDisabled={defaultMutationPending}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) deletion.close();
|
||||
}}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -220,7 +235,8 @@ function GitHubSavedPresetEntry({
|
||||
onToggleDefault: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (deletion.target?.id === preset.id) {
|
||||
const { isMobile } = useResponsiveBreakpoint();
|
||||
if (!isMobile && deletion.target?.id === preset.id) {
|
||||
return (
|
||||
<div className="mx-1 min-w-0 p-1">
|
||||
<SavedTaskViewDeleteConfirmation
|
||||
|
||||
Reference in New Issue
Block a user