diff --git a/app/components/form/ReadOnlySideModalForm.tsx b/app/components/form/ReadOnlySideModalForm.tsx index 6540adcdf..3a0bebe0c 100644 --- a/app/components/form/ReadOnlySideModalForm.tsx +++ b/app/components/form/ReadOnlySideModalForm.tsx @@ -7,6 +7,7 @@ */ import type { ReactNode } from 'react' +import { useShouldAnimateModal } from '~/hooks/use-should-animate-modal' import { Button } from '~/ui/lib/Button' import { SideModal } from '~/ui/lib/SideModal' @@ -15,11 +16,7 @@ type ReadOnlySideModalFormProps = { subtitle?: ReactNode onDismiss: () => void children: ReactNode - /** - * Whether to animate the modal opening. Defaults to true. Used to prevent - * modal from animating in on a fresh pageload where it should already be - * open. - */ + /** Pass `true` for state-driven modals. Omit for route-driven modals to use nav type. */ animate?: boolean } @@ -34,13 +31,14 @@ export function ReadOnlySideModalForm({ children, animate, }: ReadOnlySideModalFormProps) { + const animateDefault = useShouldAnimateModal() return (
{children}
diff --git a/app/components/form/SideModalForm.tsx b/app/components/form/SideModalForm.tsx index 067ceba7e..0d7e14bbf 100644 --- a/app/components/form/SideModalForm.tsx +++ b/app/components/form/SideModalForm.tsx @@ -8,10 +8,10 @@ import { useEffect, useId, useState, type ReactNode } from 'react' import type { FieldValues, UseFormReturn } from 'react-hook-form' -import { NavigationType, useNavigationType } from 'react-router' import type { ApiError } from '@oxide/api' +import { useShouldAnimateModal } from '~/hooks/use-should-animate-modal' import { Button } from '~/ui/lib/Button' import { Modal } from '~/ui/lib/Modal' import { SideModal } from '~/ui/lib/SideModal' @@ -49,16 +49,6 @@ type SideModalFormProps = { onSubmit?: (values: TFieldValues) => void } & (CreateFormProps | EditFormProps) -/** - * Only animate the modal in when we're navigating by a client-side click. - * Don't animate on a fresh pageload or on back/forward. The latter may be - * slightly awkward but it also makes some sense. I do not believe there is - * any way to distinguish between fresh pageload and back/forward. - */ -function useShouldAnimateModal() { - return useNavigationType() === NavigationType.Push -} - export function SideModalForm({ form, formType, diff --git a/app/forms/idp/edit.tsx b/app/forms/idp/edit.tsx index 4f1edc103..371c36da5 100644 --- a/app/forms/idp/edit.tsx +++ b/app/forms/idp/edit.tsx @@ -6,12 +6,7 @@ * Copyright Oxide Computer Company */ import { useForm } from 'react-hook-form' -import { - NavigationType, - useNavigate, - useNavigationType, - type LoaderFunctionArgs, -} from 'react-router' +import { useNavigate, type LoaderFunctionArgs } from 'react-router' import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api' import { Access16Icon } from '@oxide/design-system/icons/react' @@ -45,7 +40,6 @@ export default function EditIdpSideModalForm() { const navigate = useNavigate() const onDismiss = () => navigate(pb.silo({ silo })) - const animate = useNavigationType() === NavigationType.Push const form = useForm({ defaultValues: idp }) @@ -53,7 +47,6 @@ export default function EditIdpSideModalForm() { {idp.name} diff --git a/app/forms/image-edit.tsx b/app/forms/image-edit.tsx index 0d8844ecb..f23ddeb62 100644 --- a/app/forms/image-edit.tsx +++ b/app/forms/image-edit.tsx @@ -24,12 +24,10 @@ export function EditImageSideModalForm({ image, dismissLink, type, - animate, }: { image: Image dismissLink: string type: 'Project' | 'Silo' - animate?: boolean }) { const navigate = useNavigate() const form = useForm({ defaultValues: image }) @@ -40,7 +38,6 @@ export function EditImageSideModalForm({ {image.name} diff --git a/app/forms/ssh-key-edit.tsx b/app/forms/ssh-key-edit.tsx index 6bb6d7f66..6fee4ecd8 100644 --- a/app/forms/ssh-key-edit.tsx +++ b/app/forms/ssh-key-edit.tsx @@ -6,12 +6,7 @@ * Copyright Oxide Computer Company */ import { useForm } from 'react-hook-form' -import { - NavigationType, - useNavigate, - useNavigationType, - type LoaderFunctionArgs, -} from 'react-router' +import { useNavigate, type LoaderFunctionArgs } from 'react-router' import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api' import { Key16Icon } from '@oxide/design-system/icons/react' @@ -47,13 +42,11 @@ export default function EditSSHKeySideModalForm() { const form = useForm({ defaultValues: data }) const onDismiss = () => navigate(pb.sshKeys()) - const animate = useNavigationType() === NavigationType.Push return ( {data.name} diff --git a/app/hooks/use-should-animate-modal.ts b/app/hooks/use-should-animate-modal.ts new file mode 100644 index 000000000..8e3d73b34 --- /dev/null +++ b/app/hooks/use-should-animate-modal.ts @@ -0,0 +1,19 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { NavigationType, useNavigationType } from 'react-router' + +/** + * Only animate the modal in when we're navigating by a client-side click. + * Don't animate on a fresh pageload or on back/forward. The latter may be + * slightly awkward but it also makes some sense. I do not believe there is + * any way to distinguish between fresh pageload and back/forward. + */ +export function useShouldAnimateModal() { + const navType = useNavigationType() + return navType === NavigationType.Push || navType === NavigationType.Replace +} diff --git a/app/pages/SiloImageEdit.tsx b/app/pages/SiloImageEdit.tsx index 831176e4d..268ab0fea 100644 --- a/app/pages/SiloImageEdit.tsx +++ b/app/pages/SiloImageEdit.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { NavigationType, useNavigationType, type LoaderFunctionArgs } from 'react-router' +import type { LoaderFunctionArgs } from 'react-router' import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api' @@ -28,14 +28,6 @@ export const handle = titleCrumb('Edit Image') export default function SiloImageEdit() { const selector = useSiloImageSelector() const { data } = usePrefetchedQuery(imageView(selector)) - const animate = useNavigationType() === NavigationType.Push - return ( - - ) + return } diff --git a/app/pages/project/disks/DiskDetailSideModal.tsx b/app/pages/project/disks/DiskDetailSideModal.tsx index 01710b305..a1473916a 100644 --- a/app/pages/project/disks/DiskDetailSideModal.tsx +++ b/app/pages/project/disks/DiskDetailSideModal.tsx @@ -5,12 +5,7 @@ * * Copyright Oxide Computer Company */ -import { - NavigationType, - useNavigate, - useNavigationType, - type LoaderFunctionArgs, -} from 'react-router' +import { useNavigate, type LoaderFunctionArgs } from 'react-router' import { api, q, queryClient, usePrefetchedQuery, type Disk } from '@oxide/api' import { Storage16Icon } from '@oxide/design-system/icons/react' @@ -41,14 +36,9 @@ export default function DiskDetailSideModalRoute() { const { project, disk } = useDiskSelector() const navigate = useNavigate() const { data } = usePrefetchedQuery(diskView({ project, disk })) - const animate = useNavigationType() === NavigationType.Push return ( - navigate(pb.disks({ project }))} - animate={animate} - /> + navigate(pb.disks({ project }))} /> ) } @@ -60,14 +50,14 @@ export default function DiskDetailSideModalRoute() { type DiskDetailSideModalProps = { disk: Disk onDismiss: () => void - /** Default true because when used outside a route (e.g., StorageTab), it's always a click action */ + /** Pass `true` for state-driven usage (e.g., StorageTab). Omit for route usage. */ animate?: boolean } export function DiskDetailSideModal({ disk, onDismiss, - animate = true, + animate, }: DiskDetailSideModalProps) { return ( - ) + return } diff --git a/app/pages/project/instances/StorageTab.tsx b/app/pages/project/instances/StorageTab.tsx index 163222927..75ed4233b 100644 --- a/app/pages/project/instances/StorageTab.tsx +++ b/app/pages/project/instances/StorageTab.tsx @@ -420,7 +420,11 @@ export default function StorageTab() { /> )} {selectedDisk && ( - setSelectedDisk(null)} /> + setSelectedDisk(null)} + animate + /> )} ) diff --git a/app/pages/project/snapshots/SnapshotsPage.tsx b/app/pages/project/snapshots/SnapshotsPage.tsx index 32d5e4de3..bcbe98111 100644 --- a/app/pages/project/snapshots/SnapshotsPage.tsx +++ b/app/pages/project/snapshots/SnapshotsPage.tsx @@ -169,7 +169,11 @@ export default function SnapshotsPage() { {table} {selectedDisk && ( - setSelectedDisk(null)} /> + setSelectedDisk(null)} + animate + /> )} ) diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 2365b0dc2..6ee1ed2b7 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -7,13 +7,7 @@ */ import { useQuery } from '@tanstack/react-query' -import { - Link, - NavigationType, - useNavigate, - useNavigationType, - type LoaderFunctionArgs, -} from 'react-router' +import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router' import { Gateway16Icon } from '@oxide/design-system/icons/react' @@ -96,7 +90,6 @@ export default function EditInternetGatewayForm() { const navigate = useNavigate() const { project, vpc, gateway } = useInternetGatewaySelector() const onDismiss = () => navigate(pb.vpcInternetGateways({ project, vpc })) - const animate = useNavigationType() === NavigationType.Push const { data: internetGateway } = usePrefetchedQuery( q(api.internetGatewayView, { query: { project, vpc }, @@ -116,7 +109,6 @@ export default function EditInternetGatewayForm() { {internetGateway.name}