Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const DialogInternal: FC<DialogInternalProps> = ({
variant = "default",
type = "default",
container,
fullHeight = false,
}) => {
const [localIsOpen, setLocalIsOpen] = useState(isOpen)

Expand Down Expand Up @@ -86,6 +87,7 @@ export const DialogInternal: FC<DialogInternalProps> = ({
modal={modal}
onOpenChange={setLocalIsOpen}
container={container}
fullHeight={fullHeight}
>
{_memoizedDialogLayout}
</DialogWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const Content = ({
children,
disableContentPadding = false,
}: ContentProps) => {
const { position } = useF0Dialog()
const { position, fullHeight } = useF0Dialog()
const viewportRef = useRef<HTMLDivElement>(null)
const [isAtTop, setIsAtTop] = useState(true)
const [isAtBottom, setIsAtBottom] = useState(true)
Expand Down Expand Up @@ -70,14 +70,14 @@ export const Content = ({
}, [handleScroll])

return (
<div className="relative flex flex-1 flex-col overflow-hidden">
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
<ScrollArea
viewportRef={viewportRef}
className={cn(
"[*[data-state=visible]_div]:bg-f1-background flex flex-1 flex-col",
"[&_.resource-header]:p-0 [&_.resource-header]:pr-1",
!disableContentPadding && "px-4 [&>div]:py-4",
position === "fullscreen" &&
(position === "fullscreen" || fullHeight) &&
"h-full [&>div]:h-full [&>div>div]:h-full"
)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type DialogWrapperContextType = {
onClose: () => void
shownBottomSheet: boolean
position: Position
fullHeight: boolean
/**
* The dialog's content container element.
* Use this as the `portalContainer` prop for components like F0Select
Expand All @@ -23,6 +24,7 @@ export type DialogWrapperProviderProps = {
onClose: () => void
shownBottomSheet?: boolean
position: Position
fullHeight?: boolean
children: ReactNode
portalContainer: HTMLDivElement | null
}
Expand All @@ -31,6 +33,7 @@ export const DialogWrapperContext = createContext<DialogWrapperContextType>({
open: false,
onClose: () => {},
position: "center",
fullHeight: false,
shownBottomSheet: false,
portalContainer: null,
})
Expand All @@ -40,6 +43,7 @@ export const DialogWrapperProvider = ({
onClose,
shownBottomSheet = false,
position,
fullHeight = false,
children,
portalContainer,
}: DialogWrapperProviderProps) => {
Expand All @@ -49,6 +53,7 @@ export const DialogWrapperProvider = ({
open: isOpen,
onClose,
position,
fullHeight,
shownBottomSheet,
portalContainer,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export const DialogWrapper = ({
isOpen={isOpen}
onClose={onClose}
position={position}
fullHeight={fullHeight}
portalContainer={containerElement ?? null}
shownBottomSheet
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { fireEvent, waitFor } from "@testing-library/react"
import { describe, expect, it } from "vitest"

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

import { DialogWrapperProvider } from "../DialogWrapperProvider"
import { Content } from "../Content"

const renderContent = (
children: React.ReactNode,
{ fullHeight = false }: { fullHeight?: boolean } = {}
) =>
render(
<DialogWrapperProvider
isOpen
onClose={() => {}}
position="center"
fullHeight={fullHeight}
portalContainer={null}
>
<Content>{children}</Content>
</DialogWrapperProvider>
)

describe("Content", () => {
it("lets its flex-1 wrapper shrink below content size so the inner ScrollArea can scroll instead of clipping", () => {
const { container } = renderContent(<div>Step content</div>)

const wrapper = container.querySelector(".flex-1.flex-col.overflow-hidden")
expect(wrapper).not.toBeNull()
expect(wrapper).toHaveClass("min-h-0")
})

it("gives the ScrollArea a real height when the dialog is fullHeight, not just when position is fullscreen", () => {
const { container: withoutFullHeight } = renderContent(<div>Content</div>)
const { container: withFullHeight } = renderContent(<div>Content</div>, {
fullHeight: true,
})

const viewportWrapper = (container: HTMLElement) =>
container.querySelector("[data-scroll-container]")?.parentElement

expect(viewportWrapper(withoutFullHeight)).not.toHaveClass("h-full")
expect(viewportWrapper(withFullHeight)).toHaveClass("h-full")
})

it("hides the bottom scroll shadow only once the viewport has actually reached the end of the content", async () => {
const { container } = renderContent(
<div data-testid="last-field">Last field</div>
)

const viewport = container.querySelector<HTMLDivElement>(
"[data-scroll-container]"
)
expect(viewport).not.toBeNull()

// jsdom never lays out real content, so scrollHeight/clientHeight are
// always 0. Fake them to simulate a step tall enough to overflow, with
// "last-field" past the fold — mirrors the it_management form that
// triggered the original clipping bug.
Object.defineProperty(viewport, "scrollHeight", { value: 1000 })
Object.defineProperty(viewport, "clientHeight", { value: 300 })
Object.defineProperty(viewport, "scrollTop", {
value: 0,
writable: true,
})
fireEvent.scroll(viewport!)

const bottomShadow = () => container.querySelector(".bottom-0.h-4")

await waitFor(() => expect(bottomShadow()).not.toBeNull())

viewport!.scrollTop = 700
fireEvent.scroll(viewport!)

await waitFor(() => expect(bottomShadow()).toBeNull())
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ describe("DialogWrapper portal target", () => {
}
)

it("applies h-full to DialogContent when fullHeight is set", () => {
render(<DialogWrapper {...baseProps} position="center" fullHeight />)

expect(dialogContentSpy).toHaveBeenCalledWith(
expect.objectContaining({ className: expect.stringContaining("h-full") })
)
})

it("does not apply h-full to DialogContent when fullHeight is unset", () => {
render(<DialogWrapper {...baseProps} position="center" />)

const { className } = dialogContentSpy.mock.calls[0][0]
expect(className).not.toMatch(/(^|\s)h-full(\s|$)/)
})

it("forwards an explicit container override to DialogContent", () => {
const container = document.createElement("div")

Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/components/dialog-alike/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export type DialogAlikeInternalProps = {
children: ReactNode
// Disable the default padding from the dialog content area
disableContentPadding?: boolean
/**
* Whether the dialog should have a full height.
* @default false
*/
fullHeight?: boolean
/**
* Override the DOM element the dialog is portaled into. By default center
* dialogs portal to the top-level `#f0-overlay-root` (escaping app stacking
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/ui/F0Wizard/F0Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const F0Wizard: FC<F0WizardProps> = ({
primaryAction={primaryAction}
secondaryAction={secondaryAction}
disableContentPadding
fullHeight
>
<WizardProvider
currentStep={navigation.currentStep}
Expand All @@ -118,7 +119,7 @@ export const F0Wizard: FC<F0WizardProps> = ({
steps={steps}
allowStepSkipping={allowStepSkipping}
>
<div className="flex h-[58vh] flex-1 flex-row">
<div className="flex h-full flex-1 flex-row">
<div className="w-1/3 shrink-0 overflow-y-auto border-x-0 border-b-0 border-r border-t-0 border-dashed border-f1-border-secondary p-2">
<WizardSteps />
</div>
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/ui/F0Wizard/__tests__/F0Wizard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,19 @@ describe("F0Wizard", () => {
expect(screen.getByText("Step 2 content")).toBeInTheDocument()
})

it("sizes the step content row to fill the dialog height, not a fixed viewport fraction", () => {
render(
<F0Wizard isOpen={true} onClose={() => {}} steps={makeSteps(2)}>
{() => <div>Content</div>}
</F0Wizard>
)

const stepRow = document.querySelector(".flex.flex-1.flex-row")
expect(stepRow).not.toBeNull()
expect(stepRow).toHaveClass("h-full")
expect(stepRow?.className).not.toMatch(/h-\[/)
})

it("does not skip steps when autoSkipCompletedSteps is disabled", () => {
render(
<F0Wizard
Expand Down
Loading