Skyroc Native UI

Dialog

居中弹出的对话框,用于提示信息、二次确认与轻量输入

对话框(Dialog)在屏幕中央弹出一张卡片,承载标题、正文与一到两个操作按钮,用于打断当前流程、要求用户确认或输入。组件基于 Popup 封装(底层是 react-native-modal),卡片宽度为 85% 并限制最大 320,避免在平板上被拉成横贯屏幕的长条。

除了声明式的 <Dialog />,还提供命令式的 showDialog() / showConfirmDialog(),返回一个在确认或取消时结算的 Promise。

import { Dialog, closeDialog, showConfirmDialog, showDialog } from '@skyroc/native-ui';

命令式调用会把渲染器挂到 PortalHost 上,应用根节点需要放一个 <PortalHost />,否则对话框不会出现。

基础用法

showDialog(options) 弹出提示框(只有确认按钮),showConfirmDialog(options) 在此基础上补上取消按钮。两者都接受一个字符串作为简写,等价于只传 message

DialogBasic.tsx
Loading…
import { Button, showConfirmDialog, showDialog } from '@skyroc/native-ui';
import { View } from 'react-native';

const DialogBasic = () => {
  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => showDialog({ message: '这是一段需要用户知晓的提示信息。', title: '提示' })}
      >
        提示弹窗
      </Button>
      <Button
        variant="tonal"
        onPress={() => showConfirmDialog({ message: '是否确认继续当前操作?', title: '请确认' })}
      >
        确认弹窗
      </Button>
      <Button
        variant="tonal"
        onPress={() => showDialog('只传一段文案')}
      >
        纯文案
      </Button>
    </View>
  );
};

export { DialogBasic };

两个函数都返回 Promise<DialogAction>'confirm''cancel' 二选一,不会 reject —— 遮罩关闭、返回键关闭、被下一个对话框顶替,都按 'cancel' 结算。同一时刻只显示一个对话框:再次调用会顶替当前这条,被顶替的那条立即以 'cancel' 结算,它的 Promise 不会永远挂起。

何时使用

  • 需要用户明确确认才能继续的操作(删除、退出、提交),用 showConfirmDialog
  • 只是把一条重要信息推到用户面前、点掉即可,用 showDialog
  • 需要一个很短的输入(昵称、验证码、备注),用 showInput,不必为此再开一个页面。
  • 操作项在 2 个以上时用 ActionSheet;内容是表单或长列表时用 Sheet;不需要打断用户的轻提示用 Toast / Notify

按钮配置

底部操作区由 showCancelButton / showConfirmButton 决定,文案、颜色与禁用态各自独立:

属性说明默认值
showConfirmButton是否显示确认按钮true
showCancelButton是否显示取消按钮(showConfirmDialog 自动置 truefalse
confirmButtonText确认按钮文案'确定'
cancelButtonText取消按钮文案'取消'
confirmButtonColor确认按钮语义色,destructive 显示为红色'primary'
confirmButtonDisabled禁用确认按钮false
DialogButtons.tsx
Loading…
import { Button, showConfirmDialog, showDialog } from '@skyroc/native-ui';
import { View } from 'react-native';

const DialogButtons = () => {
  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() =>
          showConfirmDialog({
            cancelButtonText: '返回',
            confirmButtonText: '继续',
            message: '确认与取消按钮都支持自定义文案。',
            title: '按钮文案'
          })
        }
      >
        自定义文案
      </Button>
      <Button
        variant="tonal"
        onPress={() =>
          showDialog({
            message: '确认按钮已禁用,只能通过取消按钮关闭。',
            showCancelButton: true,
            confirmButtonDisabled: true,
            title: '禁用确认'
          })
        }
      >
        禁用确认
      </Button>
      <Button
        variant="tonal"
        onPress={() =>
          showConfirmDialog({
            confirmButtonColor: 'destructive',
            confirmButtonText: '删除',
            message: 'destructive 用于明确标识不可逆操作。',
            title: '破坏性操作'
          })
        }
      >
        危险按钮
      </Button>
      <Button
        variant="tonal"
        onPress={() =>
          showDialog({
            cancelButtonText: '知道了',
            message: 'showConfirmButton=false 可隐藏默认确认按钮。',
            showCancelButton: true,
            showConfirmButton: false,
            title: '仅取消按钮'
          })
        }
      >
        仅取消按钮
      </Button>
    </View>
  );
};

export { DialogButtons };

两个按钮都关掉时整个底部操作区不渲染,对话框就只剩标题与正文 —— 此时必须留一条别的出口(closeOnBackdropPresscloseOnBackPresscloseDialog()),否则用户关不掉它。

消息对齐

messageAlign 控制正文的水平对齐,默认居中。

DialogMessageAlign.tsx
Loading…
import type { DialogProps } from '@skyroc/native-ui';
import { Button, showDialog } from '@skyroc/native-ui';
import { View } from 'react-native';

const ALIGNMENTS: Array<NonNullable<DialogProps['messageAlign']>> = ['left', 'center', 'right'];

const DialogMessageAlign = () => {
  function handleOpen(messageAlign: NonNullable<DialogProps['messageAlign']>) {
    showDialog({
      message: `当前消息使用 ${messageAlign} 对齐。`,
      messageAlign,
      title: `${messageAlign} 对齐`
    });
  }

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      {ALIGNMENTS.map(messageAlign => (
        <Button
          key={messageAlign}
          variant="tonal"
          onPress={() => handleOpen(messageAlign)}
        >
          {messageAlign}
        </Button>
      ))}
    </View>
  );
};

export { DialogMessageAlign };

正文的字号与颜色还会随「有没有标题」变化:有标题时正文是 text-sm 的次要灰字,没有标题时它自己就是主信息,改用 text-base 的前景色。所以不要为了让文字变大而故意省掉标题,反过来也一样。

主题与方向

theme 决定底部操作区的形态:

主题表现
default通栏文字按钮(高 48),与正文之间、两个按钮之间都由分割线隔开
round-button胶囊按钮(高 44),带 gap-3 间距和 px-6 pb-6 的内边距
DialogTheme.tsx
Loading…
import { Button, showConfirmDialog } from '@skyroc/native-ui';
import { View } from 'react-native';

const DialogTheme = () => {
  function handleRound(themeDirection: 'horizontal' | 'vertical') {
    showConfirmDialog({
      message: `round-button 主题 · ${themeDirection}`,
      theme: 'round-button',
      themeDirection,
      title: '圆角按钮'
    });
  }

  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => showConfirmDialog({ message: '默认主题使用通栏文字按钮。', title: '默认主题' })}
      >
        默认主题
      </Button>
      <Button
        variant="tonal"
        onPress={() => handleRound('vertical')}
      >
        圆角竖排
      </Button>
      <Button
        variant="tonal"
        onPress={() => handleRound('horizontal')}
      >
        圆角横排
      </Button>
    </View>
  );
};

export { DialogTheme };

themeDirection 只在 round-button 下生效(默认 vertical):default 主题的通栏按钮永远横排,竖排会退化成两条色带。竖排时确定按钮在上、取消在下 —— 内部用的是 flex-col-reverse,JSX 里的渲染顺序始终是「取消 → 确定」,因此横排时确定自然落在右侧。

default 主题的按钮圆角是被清掉的,底部圆角交给卡片根节点的 overflow-hidden 统一裁切;这样按下时的高亮才能贴着卡片边缘,而不是缩在角落里的一块方角。

输入模式

showInput 在正文下方渲染一个 Input(自动聚焦)。输入值可以非受控(defaultInputValue)也可以受控(inputValue + onInputChange),确认 / 取消回调与 callback 都会带上当前输入内容。

DialogInput.tsx
Loading…
import { Button, Dialog, Portal, Text, showDialog } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DialogInput = () => {
  const [show, setShow] = useState(false);
  const [inputValue, setInputValue] = useState('受控内容');
  const [result, setResult] = useState('尚未确认');

  function handleDefaultInput() {
    showDialog({
      callback: (action, value) => setResult(`${action}:${value || '空值'}`),
      defaultInputValue: '默认内容',
      inputPlaceholder: '请输入昵称',
      inputProps: { maxLength: 12 },
      message: '输入框预置 defaultInputValue,并限制最多 12 个字符。',
      showCancelButton: true,
      showInput: true,
      title: '非受控输入'
    });
  }

  function handleConfirm(value?: string) {
    setResult(`confirm:${value || '空值'}`);
  }

  return (
    <View className="gap-3 bg-background p-4">
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={handleDefaultInput}
        >
          defaultInputValue
        </Button>
        <Button
          variant="outline"
          onPress={() => setShow(true)}
        >
          受控 inputValue
        </Button>
      </View>
      <Text className="text-sm text-muted-foreground">结果:{result}</Text>

      <Portal>
        <Dialog
          inputPlaceholder="请输入内容"
          inputProps={{ maxLength: 12 }}
          inputValue={inputValue}
          message="onInputChange 实时同步外部 inputValue。"
          show={show}
          showCancelButton
          showInput
          title="受控输入"
          onConfirm={handleConfirm}
          onInputChange={setInputValue}
          onUpdateShow={setShow}
        />
      </Portal>
    </View>
  );
};

export { DialogInput };

inputProps 透传给内部的 Input(如 maxLengthkeyboardType),但 valueonChangeText 由 Dialog 接管,传了也会被覆盖 —— 监听输入请用 onInputChange

键盘避让默认跟随 showInput:带输入框时自动开启 avoidKeyboard(输入框被键盘挡住的对话框等于不能用),纯提示类对话框位于屏幕中部,不必多包一层 KeyboardAvoidingView。需要时可以用 avoidKeyboard 显式覆盖。点击任一按钮时会先 Keyboard.dismiss(),键盘不会在对话框退场时留在屏幕上。

关闭拦截

beforeClose(action, inputValue) 在关闭前拦一道,返回 false 阻止关闭;返回 Promise 时对应按钮进入 loading,resolve(true) 才关闭。

DialogBeforeClose.tsx
Loading…
import { Button, Text, showConfirmDialog } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DialogBeforeClose = () => {
  const [result, setResult] = useState('尚未操作');

  function handleSyncGuard() {
    showConfirmDialog({
      beforeClose: action => action === 'confirm',
      callback: action => setResult(`同步拦截最终结果:${action}`),
      message: '点击取消会被阻止,点击确认才会关闭。',
      title: '同步拦截'
    });
  }

  function handleAsyncGuard() {
    showConfirmDialog({
      beforeClose: action => {
        if (action === 'cancel') return true;

        return new Promise<boolean>(resolve => {
          setTimeout(() => resolve(true), 1500);
        });
      },
      callback: action => setResult(`异步拦截最终结果:${action}`),
      message: '点击确认后等待 1.5 秒,期间按钮显示 loading。',
      title: '异步拦截'
    });
  }

  return (
    <View className="gap-3 bg-background p-4">
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={handleSyncGuard}
        >
          同步阻止取消
        </Button>
        <Button
          variant="tonal"
          onPress={handleAsyncGuard}
        >
          异步确认
        </Button>
      </View>
      <Text className="text-sm text-muted-foreground">{result}</Text>
    </View>
  );
};

export { DialogBeforeClose };

几个容易踩的点:

  • 异步等待期间,触发的那个按钮显示 loading,另一个按钮被禁用,避免用户在提交途中点取消。
  • Promise reject 只会结束 loading,不关闭对话框,不会有未捕获异常。
  • 同步返回 false 时不会出现 loading —— 同一批次里的 setState 会被合并掉,写了也渲染不出来。
  • 遮罩点击与 Android 返回键走的是和「取消」完全相同的路径,同样会经过 beforeClose
  • closeDialog() 是直接结算,不经过 beforeClose;声明式下把 show 改成 false 同理。

关闭方式

属性 / 方法关闭路径默认值
closeOnBackdropPress点击遮罩false
closeOnBackPressAndroid 硬件返回键true
closeDialog()代码主动关闭当前对话框
DialogCloseMode.tsx
Loading…
import { Button, Text, closeDialog, showDialog } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DialogCloseMode = () => {
  const [lastAction, setLastAction] = useState<string>('—');

  async function handleBackdrop() {
    const action = await showDialog({
      closeOnBackdropPress: true,
      message: '点击遮罩关闭,同样按取消结算',
      title: '遮罩关闭'
    });

    setLastAction(action);
  }

  async function handleProgrammaticClose() {
    const pending = showDialog({ message: '2 秒后由代码关闭', title: '命令式关闭' });

    setTimeout(closeDialog, 2000);

    setLastAction(await pending);
  }

  return (
    <View className="gap-3 bg-background p-4">
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={handleBackdrop}
        >
          点遮罩关闭
        </Button>
        <Button
          variant="tonal"
          onPress={handleProgrammaticClose}
        >
          代码关闭
        </Button>
        <Button
          variant="tonal"
          onPress={() =>
            showDialog({
              closeOnBackdropPress: false,
              closeOnBackPress: false,
              message: '遮罩点击和 Android 返回键都不会关闭,只能点击确认按钮。',
              title: '禁止外部关闭'
            })
          }
        >
          禁止外部关闭
        </Button>
      </View>
      <Text color="muted">最近一次操作:{lastAction}</Text>
    </View>
  );
};

export { DialogCloseMode };

这三条路径都按 'cancel' 结算:onCancel 会被调用、Promise resolve 'cancel'。注意 closeOnBackdropPress 默认是 false(与 Popup 的默认值相反)—— 对话框通常要求用户做出选择,随手点掉容易误操作。

声明式受控

<Dialog />show + onUpdateShow 控制显示。onOpened / onClosed 在进出场动画播放完毕后触发,可以用来做埋点或资源清理。

DialogDeclarative.tsx
Loading…
import { Button, Dialog, Portal, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DialogDeclarative = () => {
  const [show, setShow] = useState(false);
  const [openedCount, setOpenedCount] = useState(0);
  const [closedCount, setClosedCount] = useState(0);

  return (
    <View className="gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() => setShow(true)}
      >
        打开声明式 Dialog
      </Button>
      <View className="rounded-xl bg-muted p-3">
        <Text className="text-sm text-muted-foreground">
          已打开 {openedCount} 次,已关闭 {closedCount} 次
        </Text>
      </View>

      {/* 声明式 Dialog 不能落在 ScrollView 里面:JS 触摸响应链走的是 React 树,
          键盘弹起时 ScrollView 会在 capture 阶段抢走第一次点击用来收键盘(keyboardShouldPersistTaps 默认 never),
          放在里面的话带输入框的弹窗要点两下才关得掉。这里套 Portal,让它们改挂到 PortalHost 下,
          总览页把本 demo 放进 ScrollView 时也不受影响 */}
      <Portal>
        <Dialog
          message="show 与 onUpdateShow 由外部状态控制。"
          show={show}
          showCancelButton
          title="受控 Dialog"
          onClosed={() => setClosedCount(count => count + 1)}
          onOpened={() => setOpenedCount(count => count + 1)}
          onUpdateShow={setShow}
        />
      </Portal>
    </View>
  );
};

export { DialogDeclarative };

回调顺序是固定的:先 onConfirm / onCancel,再 onUpdateShow(false),最后动画结束时 onClosed。基于 Dialog 再做封装时可以依赖这个顺序区分「确定」与「被关闭」。

不要把声明式的 <Dialog /> 写在 ScrollView 里面:JS 触摸响应链走的是 React 树而不是原生视图树,Modal 渲染到哪儿都改变不了这一点。键盘弹起时 ScrollView 会在 capture 阶段抢走第一次点击用于收键盘(keyboardShouldPersistTaps 默认 never),于是带输入框的对话框要点两下才关得掉。解决办法是套一层 Portal 挂到 PortalHost 下、放到 ScrollView 外层,或者给该 ScrollViewkeyboardShouldPersistTaps="handled"。命令式的 showDialog 走 Portal,不受影响。

自定义内容

children 渲染在正文下方、底部操作区上方,title / message 与按钮行为都不受影响。

DialogCustomContent.tsx
Loading…
import { Button, Divider, Text, showConfirmDialog } from '@skyroc/native-ui';
import { View } from 'react-native';

const DialogCustomContent = () => {
  function handleOpen() {
    showConfirmDialog({
      children: (
        <View className="overflow-hidden rounded-xl border border-border">
          <View className="p-3">
            <Text className="font-medium text-foreground">自定义内容区域</Text>
            <Text className="mt-1 text-sm text-muted-foreground">这里由 children 渲染,可组合组件库内容。</Text>
          </View>
          <Divider />
          <View className="bg-muted p-3">
            <Text className="text-sm text-muted-foreground">边界、留白和层级不会改变 Dialog 行为。</Text>
          </View>
        </View>
      ),
      message: '正文下方追加自定义节点。',
      title: '自定义内容'
    });
  }

  return (
    <View className="bg-background p-4">
      <Button
        variant="tonal"
        onPress={handleOpen}
      >
        打开自定义内容
      </Button>
    </View>
  );
};

export { DialogCustomContent };

样式覆盖

className 覆盖卡片根节点,classNames 按 slot 细粒度覆盖:

slot作用位置
popup外层弹出容器,控制宽度与定位(默认 w-[85%] max-w-[320px]
root卡片本身,圆角、底色与 overflow-hidden 都在这里
header标题容器
title标题文字
body正文容器(正文、输入框、children 都在里面)
message正文文字
footer底部操作区容器
cancelButton取消按钮
confirmButton确认按钮
DialogStyles.tsx
Loading…
import { Button, showConfirmDialog, showDialog } from '@skyroc/native-ui';
import { View } from 'react-native';

const DialogStyles = () => {
  return (
    <View className="flex-row flex-wrap items-center gap-3 bg-background p-4">
      <Button
        variant="tonal"
        onPress={() =>
          showConfirmDialog({
            classNames: {
              confirmButton: 'bg-primary/10',
              header: 'pb-1',
              message: 'text-left',
              popup: 'w-[92%] max-w-[380px]',
              title: 'text-left'
            },
            message: 'popup 控制外层宽度,root 是卡片本身,其余 slot 逐个可覆盖',
            title: 'classNames 覆盖'
          })
        }
      >
        slot 覆盖
      </Button>
      <Button
        variant="tonal"
        onPress={() =>
          showDialog({
            className: 'rounded-3xl bg-carbon',
            classNames: { message: 'text-carbon-foreground', title: 'text-carbon-foreground' },
            message: 'className 覆盖的是卡片根节点',
            title: '深色卡片'
          })
        }
      >
        className 覆盖
      </Button>
    </View>
  );
};

export { DialogStyles };

改宽度要用 classNames.popup 而不是 classNameclassName 落在卡片上,外层容器仍然是 85%,卡片撑不出去。合并顺序是「变体样式 → classNames.rootclassName」,冲突时 className 优先级最高。

API

Dialog

属性说明类型默认值
show*是否显示对话框boolean-
title标题,为空时正文会以主信息的字号与颜色渲染string-
message消息正文string-
messageAlign正文对齐方式'left' | 'center' | 'right''center'
children自定义内容,渲染在正文下方、底部操作区上方ReactNode-
showConfirmButton是否显示确认按钮booleantrue
showCancelButton是否显示取消按钮booleanfalse
confirmButtonText确认按钮文本string'确定'
cancelButtonText取消按钮文本string'取消'
confirmButtonColor确认按钮颜色语义,destructive 用于删除 / 注销等破坏性操作'primary' | 'destructive''primary'
confirmButtonDisabled是否禁用确认按钮booleanfalse
theme底部按钮主题'default' | 'round-button''default'
themeDirection按钮排列方向,仅 theme="round-button" 时生效'horizontal' | 'vertical''vertical'
showInput是否显示输入框booleanfalse
inputValue输入框受控值string-
defaultInputValue输入框默认值(非受控)string''
inputPlaceholder输入框占位文本string-
inputProps透传给内部 Input 的额外属性;value / onChangeText 由 Dialog 接管Partial<InputProps>-
onInputChange输入内容变化回调(value: string) => void-
beforeClose关闭前的拦截回调,返回 false 阻止关闭;返回 Promise 时对应按钮显示 loadingDialogBeforeClose-
closeOnBackdropPress点击遮罩是否关闭,关闭时按取消结算并经过 beforeClosebooleanfalse
closeOnBackPressAndroid 硬件返回键是否关闭,同样按取消处理booleantrue
avoidKeyboard键盘弹出时是否自动避让,缺省时跟随 showInputboolean-
onConfirm确认按钮回调,showInput 时参数为输入值(inputValue?: string) => void-
onCancel取消按钮回调,showInput 时参数为输入值(inputValue?: string) => void-
onUpdateShow显示状态变化回调,始终在 onConfirm / onCancel 之后触发(show: boolean) => void-
onOpened打开动画播放完毕后触发() => void-
onClosed关闭动画播放完毕后触发() => void-
className卡片根节点的类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖SlotClassNames<DialogSlots>-

showDialog

function showDialog(options: DialogOptions | string): Promise<DialogAction>;

命令式显示对话框,默认只有确认按钮。传字符串等价于 { message: options }。Promise 在用户确认时 resolve 'confirm',取消 / 遮罩关闭 / 返回键关闭 / 被下一个对话框顶替时 resolve 'cancel',不会 reject。

showConfirmDialog

function showConfirmDialog(options: DialogOptions | string): Promise<DialogAction>;

showDialog 相同,只是默认补上 showCancelButton: true。显式传入的 showCancelButton 优先级更高。

closeDialog

function closeDialog(): void;

关闭当前对话框,按取消结算:等待中的 Promise resolve 'cancel'onCancel / callback 照常触发,但不经过 beforeClose。没有对话框在显示时调用无副作用。

类型

import type {
  DialogAction,
  DialogBeforeClose,
  DialogConfirmColor,
  DialogLoading,
  DialogOptions,
  DialogProps,
  DialogSlots,
  DialogTheme,
  DialogThemeDirection
} from '@skyroc/native-ui';

DialogAction

用户操作类型,命令式调用的 Promise 与 callback 结算的就是它。

'confirm' | 'cancel'

DialogTheme

底部操作区主题:default 为通栏文字按钮,round-button 为胶囊按钮。

'default' | 'round-button'

DialogThemeDirection

round-button 主题下按钮的排列方向,default 主题恒为横排。

'horizontal' | 'vertical'

DialogConfirmColor

确认按钮的颜色语义。

'primary' | 'destructive'

DialogSlots

可通过 classNames 覆盖的 slot 名称,popup 为外层弹出容器(宽度、定位),root 为卡片本身。

'popup' | 'root' | 'header' | 'title' | 'body' | 'message' | 'footer' | 'cancelButton' | 'confirmButton'

DialogBeforeClose

关闭拦截器,返回 false 或 resolve(false) 时阻止关闭;showInput 时第二个参数为当前输入值。

(action: DialogAction, inputValue?: string) => boolean | Promise<boolean>

DialogLoading

两个按钮的 loading 状态,异步 beforeClose 期间由组件内部维护。

字段类型说明
confirm*boolean确认按钮是否处于 loading。
cancel*boolean取消按钮是否处于 loading。

DialogOptions

命令式调用的入参,等价于 Omit<DialogProps, 'onClosed' | 'onUpdateShow' | 'show'> 再加上 callback。

字段类型说明
callback(action: DialogAction, inputValue?: string) => void确认或取消后的通用回调,与 Promise 一同结算,且恰好触发一次。