Skip to content

Latest commit

 

History

History
277 lines (220 loc) · 9.52 KB

File metadata and controls

277 lines (220 loc) · 9.52 KB
title Dialog
description A single <Dialog> for every overlay surface — centered modal, side-sheet, or bottom-sheet.
order 3

Dialog

A single <Dialog> component for every overlay surface — centered modal, side-sheet (left/right drawer), or bottom-sheet — controlled by the placement prop. Same component, same props on every platform.

Your app root **must** be wrapped with `GestureHandlerRootView` from `react-native-gesture-handler` for the bottom-sheet pan gestures to work. See [Getting started](/docs/bloom/main/getting-started).

Declarative (the 90% case)

import { Dialog, useDialogControl, Button } from '@oxyhq/bloom';

function SignOutButton() {
  const control = useDialogControl();
  return (
    <>
      <Button onPress={() => control.open()}>Sign out</Button>

      <Dialog
        control={control}
        title="Sign out?"
        description="You'll need to enter your password to sign in again."
        actions={[
          { label: 'Sign out', color: 'destructive', onPress: doSignOut },
          { label: 'Cancel', color: 'cancel' },
        ]}
      />
    </>
  );
}

Controlled open state

The controlled path fires `onClose` synchronously on `open` flipping to `false` — ahead of the exit animation. If your `onClose` handler unmounts the consumer (navigates away, removes the dialog from the tree), it races the animation still in flight. `useDialogControl()` doesn't have this footgun: it closes imperatively and only resolves once the exit finishes.

Pass open + onClose to drive the dialog from external state instead of useDialogControl:

<Dialog
  placement="center"
  open={isOpen}
  onClose={() => setIsOpen(false)}
  title="Confirm?"
  actions={[
    { label: 'OK', onPress: handleOk },
    { label: 'Cancel', color: 'cancel' },
  ]}
/>

Side-sheet (drawer)

// Fixed left side-sheet
<Dialog placement="left" control={control} title="Filters">
  <FilterPanel />
</Dialog>

// Responsive: bottom-sheet on mobile, left drawer on desktop
<Dialog placement={{ base: 'bottom', md: 'left' }} control={control} title="Filters">
  <FilterPanel />
</Dialog>

Custom content

Provide any JSX as children. Combine with title to keep a consistent header. Set contentPadding={0} when the children own their own insets.

<Dialog control={control} title="Pick a tag" contentPadding={0}>
  <YourCustomBody />
</Dialog>

Pure custom

Drop the declarative props entirely — children owns every pixel.

<Dialog control={control}>
  <YourEntirelyCustomLayout />
</Dialog>

Props

Prop Type Default Description
control? DialogControl From useDialogControl(). Omit when using controlled open.
open? boolean Controlled open state. When provided, wins over control.
placement? DialogPlacement | ResponsiveDialogPlacement 'center' Surface anchor. See Placement.
title? string Header text.
description? string Supporting copy below the title.
actions? DialogAction[] Confirmation buttons. See DialogAction.
children? React.ReactNode Custom content rendered after the description.
onClose? () => void Fires after the dialog finishes closing. In controlled mode, the host flips open to false.
contentPadding? number 20 Inner padding of the dialog body (px). Set 0 for custom children that own their insets.
width? number 460 Side-sheet width (px) on left/right placements.
maxWidth? number 480 Centered-card max width (px).
maxHeightRatio? number 0.9 Bottom-sheet max height as a fraction of the viewport height.
inset? { top?; bottom?; left?; right? } Side-sheet inset (px) from the overlay container edges.
showHandle? boolean true Show drag handle in bottom-sheet mode.
dismissOnBackdrop? boolean true Tap backdrop to dismiss.
panelStyle? StyleProp<ViewStyle> Style for the panel surface.
panelClassName? string NativeWind class for the panel surface.
containerStyle? StyleProp<ViewStyle> Style for the root overlay (e.g. rail offset, theme-var scope).
containerClassName? string NativeWind class for the root overlay.
label? string Accessibility label.
testID? string RN testing ID.

Placement

placement accepts a single value or a responsive map resolved by useWindowDimensions():

type DialogPlacement = 'center' | 'left' | 'right' | 'bottom';

type ResponsiveDialogPlacement =
  | DialogPlacement
  | { base: DialogPlacement; sm?: DialogPlacement; md?: DialogPlacement; lg?: DialogPlacement; xl?: DialogPlacement };

Breakpoints (px): sm 640 / md 768 / lg 1024 / xl 1280. The largest breakpoint whose min-width is <= the viewport wins; falls back to base below all breakpoints.

Per-placement behavior:

  • center — DOM-portal modal (web) / detached floating card (native).
  • left / right — animated side-sheet anchored to the viewport edge.
  • bottom — reuses the cross-platform BottomSheet on BOTH web and native (drag-to-dismiss + internal scroll work everywhere). Custom children on bottom placement are rendered in a NON-scrollable body — children own scrolling.

DialogAction

Prop Type Description
label string Button text.
color? 'default' | 'cancel' | 'destructive' Defaults to 'default'.
onPress? (e) => void Invoked after the dialog finishes closing.
disabled? boolean
shouldCloseOnPress? boolean Defaults to true. Set false while an async action is in flight.
testID? string

Blurring the backdrop on Android

A backdrop blurs its content on iOS and web with no setup. On Android it needs the app content wrapped in a blur target, which BloomProvider composes — so if you mount it, there is nothing to do:

import { BloomProvider } from '@oxyhq/bloom/provider';

<BloomProvider>{children}</BloomProvider>

An app that does not mount BloomProvider keeps the tint-only backdrop. That is a supported state, not a missing step — see below.

Two things are worth knowing rather than discovering:

  • It applies to the bottom-sheet path only. expo-blur on Android blurs a target view, and a blur view that is a DESCENDANT of its own target crashes the app — SIGSEGV, an unbounded render-tree recursion. Only a surface in a separate native window is safe, which means a real RN <Modal>: that is BottomSheet, and Dialog's bottom placement through it. Dialog's centre and side placements portal into the same window and keep the tint-only backdrop. Bloom enforces this structurally — the target is simply not reachable from a surface that has not crossed a window boundary — so there is no wiring you can get wrong.
  • Not mounting it is a supported state, not a broken one. The backdrop still paints its tint at full intensity; you lose the blur and nothing else, with no warning and no crash.

Web setup

Inject the CSS animations into your global styles once:

import { BLOOM_DIALOG_CSS } from '@oxyhq/bloom/dialog';

// In your HTML head or global CSS file:
<style>{BLOOM_DIALOG_CSS}</style>

Examples

Three-action confirmation

<Dialog
  control={control}
  title="Delete project?"
  description="This will permanently delete the project and all of its files."
  actions={[
    { label: 'Delete', color: 'destructive', onPress: handleDelete },
    { label: 'Archive', onPress: handleArchive },
    { label: 'Cancel', color: 'cancel' },
  ]}
/>

Async confirm with loading state

const [busy, setBusy] = useState(false);

<Dialog
  control={control}
  title="Submit?"
  actions={[
    {
      label: busy ? 'Submitting…' : 'Submit',
      disabled: busy,
      shouldCloseOnPress: false, // keep dialog open while we work
      onPress: async () => {
        setBusy(true);
        try {
          await submit();
          control.close();
        } finally {
          setBusy(false);
        }
      },
    },
    { label: 'Cancel', color: 'cancel' },
  ]}
/>

Form inside a dialog

<Dialog control={control} title="Add tag">
  <TextFieldInput label="Tag name" value={name} onChangeText={setName} />
  <Button onPress={() => { save(name); control.close(); }}>Save</Button>
</Dialog>

Responsive drawer→sheet

Bottom-sheet on small screens, left drawer on md and wider:

<Dialog
  placement={{ base: 'bottom', md: 'left' }}
  control={control}
  title="Store settings"
  containerClassName="md:left-[4.75rem]"
>
  <SettingsPanel />
</Dialog>

Migration from CenteredDialog / ResponsiveSheet

CenteredDialog and ResponsiveSheet were removed in 0.16.x.

// Before
<CenteredDialog visible={v} onClose={c}></CenteredDialog>
// After
<Dialog placement="center" open={v} onClose={c}></Dialog>

// Before
<ResponsiveSheet side="left" open={o} onClose={c}></ResponsiveSheet>
// After
<Dialog placement={{ base: 'bottom', md: 'left' }} open={o} onClose={c}></Dialog>

Removed exports: CenteredDialog, CenteredDialogProps, BLOOM_CENTERED_DIALOG_CSS, CENTERED_DIALOG_BACKDROP_TESTID, ResponsiveSheet, @oxyhq/bloom/responsive-sheet subpath.