Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, test, vi } from "vitest"

import { screen, userEvent, zeroRender as render } from "@/testing/test-utils"

import { F0Dialog } from "../index"

/**
* A footer action that LEAVES the dialog should be a link: "Go to post" is a
* place, and a place you cannot cmd-click is a place the reader has to come
* back from. The ones that act on what the dialog is showing stay handlers.
*/
const dialog = (props: Record<string, unknown>) =>
render(
<F0Dialog isOpen onClose={() => {}} header={{ title: "A post" }} {...props}>
<p>the post</p>
</F0Dialog>
)

describe("footer actions", () => {
test("draws a primary action with a route as a real link", () => {
dialog({ primaryAction: { label: "Go to post", href: "/posts/1" } })

expect(screen.getByRole("link", { name: "Go to post" })).toHaveAttribute(
"href",
"/posts/1"
)
})

test("draws a secondary action with a route as a real link", () => {
dialog({
secondaryAction: { label: "Go to community", href: "/communities/2" },
})

expect(
screen.getByRole("link", { name: "Go to community" })
).toHaveAttribute("href", "/communities/2")
})

test("keeps a handler action a button", async () => {
const onClick = vi.fn()
dialog({ primaryAction: { label: "Mark as read", onClick } })

await userEvent.click(screen.getByRole("button", { name: "Mark as read" }))

expect(onClick).toHaveBeenCalledTimes(1)
})

test("reports the press on a link that asked to hear about it", async () => {
const onClick = vi.fn()
dialog({
primaryAction: { label: "Go to post", href: "/posts/1", onClick },
})

await userEvent.click(screen.getByRole("link", { name: "Go to post" }))

expect(onClick).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export const F0DialogFooter = ({
return (
<F0Button
label={primaryAction.label}
onClick={primaryAction.onClick}
// Spread rather than passed: the button's `href` is one arm of a union,
// so an `undefined` one is not the same as none at all.
{...(primaryAction.href ? { href: primaryAction.href } : {})}
{...(primaryAction.onClick ? { onClick: primaryAction.onClick } : {})}
variant="default"
icon={primaryAction.icon}
iconPosition={primaryAction.iconPosition}
Expand Down Expand Up @@ -88,7 +91,12 @@ export const F0DialogFooter = ({
return (
<F0Button
label={secondaryAction.label}
onClick={secondaryAction.onClick}
// Spread rather than passed: the button's `href` is one arm of a union,
// so an `undefined` one is not the same as none at all.
{...(secondaryAction.href ? { href: secondaryAction.href } : {})}
{...(secondaryAction.onClick
? { onClick: secondaryAction.onClick }
: {})}
variant="outline"
icon={secondaryAction.icon}
iconPosition={secondaryAction.iconPosition}
Expand Down
22 changes: 18 additions & 4 deletions packages/react/src/patterns/F0Dialog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,37 @@ export type DialogPosition = (typeof dialogPositions)[number]
export const dialogWidths = ["sm", "md", "lg", "xl"] as const
export type DialogWidth = (typeof dialogWidths)[number]

/**
* A FOOTER ACTION IS A ROUTE OR A HANDLER.
*
* `href` makes the button a real link — cmd/middle-clickable, openable in a new
* tab, routed through the app's link provider — which is what an action that
* LEAVES the dialog should be: "Go to post", "Open in Contracts". `onClick` is
* for the ones that act on what the dialog is showing and stay put.
*
* Both together is a link that also reports the press (analytics, closing the
* dialog behind it). The same shape `DialogControls`' resource `url` already
* takes, for the same reason.
*/
type F0DialogActionTarget =
| { href: string; onClick?: () => void }
| { href?: never; onClick: () => void }

export type F0DialogPrimaryAction = {
label: string
icon?: IconType
iconPosition?: "left" | "right"
onClick: () => void
disabled?: boolean
loading?: boolean
}
} & F0DialogActionTarget

export type F0DialogSecondaryAction = {
label: string
icon?: IconType
iconPosition?: "left" | "right"
onClick: () => void
disabled?: boolean
loading?: boolean
}
} & F0DialogActionTarget

// Shared base for action items used in multi-action dropdowns.
// Note: disabled/loading on items are reserved for future use —
Expand Down
Loading