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。
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 自动置 true) | false |
confirmButtonText | 确认按钮文案 | '确定' |
cancelButtonText | 取消按钮文案 | '取消' |
confirmButtonColor | 确认按钮语义色,destructive 显示为红色 | 'primary' |
confirmButtonDisabled | 禁用确认按钮 | false |
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 };两个按钮都关掉时整个底部操作区不渲染,对话框就只剩标题与正文 —— 此时必须留一条别的出口(closeOnBackdropPress、closeOnBackPress 或 closeDialog()),否则用户关不掉它。
消息对齐
messageAlign 控制正文的水平对齐,默认居中。
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 的内边距 |
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 都会带上当前输入内容。
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(如 maxLength、keyboardType),但 value 与 onChangeText 由 Dialog 接管,传了也会被覆盖 —— 监听输入请用 onInputChange。
键盘避让默认跟随 showInput:带输入框时自动开启 avoidKeyboard(输入框被键盘挡住的对话框等于不能用),纯提示类对话框位于屏幕中部,不必多包一层 KeyboardAvoidingView。需要时可以用 avoidKeyboard 显式覆盖。点击任一按钮时会先 Keyboard.dismiss(),键盘不会在对话框退场时留在屏幕上。
关闭拦截
beforeClose(action, inputValue) 在关闭前拦一道,返回 false 阻止关闭;返回 Promise 时对应按钮进入 loading,resolve(true) 才关闭。
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 |
closeOnBackPress | Android 硬件返回键 | true |
closeDialog() | 代码主动关闭当前对话框 | — |
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 在进出场动画播放完毕后触发,可以用来做埋点或资源清理。
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 外层,或者给该 ScrollView 传 keyboardShouldPersistTaps="handled"。命令式的 showDialog 走 Portal,不受影响。
自定义内容
children 渲染在正文下方、底部操作区上方,title / message 与按钮行为都不受影响。
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 | 确认按钮 |
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 而不是 className:className 落在卡片上,外层容器仍然是 85%,卡片撑不出去。合并顺序是「变体样式 → classNames.root → className」,冲突时 className 优先级最高。
API
Dialog
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| show* | 是否显示对话框 | boolean | - |
| title | 标题,为空时正文会以主信息的字号与颜色渲染 | string | - |
| message | 消息正文 | string | - |
| messageAlign | 正文对齐方式 | 'left' | 'center' | 'right' | 'center' |
| children | 自定义内容,渲染在正文下方、底部操作区上方 | ReactNode | - |
| showConfirmButton | 是否显示确认按钮 | boolean | true |
| showCancelButton | 是否显示取消按钮 | boolean | false |
| confirmButtonText | 确认按钮文本 | string | '确定' |
| cancelButtonText | 取消按钮文本 | string | '取消' |
| confirmButtonColor | 确认按钮颜色语义,destructive 用于删除 / 注销等破坏性操作 | 'primary' | 'destructive' | 'primary' |
| confirmButtonDisabled | 是否禁用确认按钮 | boolean | false |
| theme | 底部按钮主题 | 'default' | 'round-button' | 'default' |
| themeDirection | 按钮排列方向,仅 theme="round-button" 时生效 | 'horizontal' | 'vertical' | 'vertical' |
| showInput | 是否显示输入框 | boolean | false |
| inputValue | 输入框受控值 | string | - |
| defaultInputValue | 输入框默认值(非受控) | string | '' |
| inputPlaceholder | 输入框占位文本 | string | - |
| inputProps | 透传给内部 Input 的额外属性;value / onChangeText 由 Dialog 接管 | Partial<InputProps> | - |
| onInputChange | 输入内容变化回调 | (value: string) => void | - |
| beforeClose | 关闭前的拦截回调,返回 false 阻止关闭;返回 Promise 时对应按钮显示 loading | DialogBeforeClose | - |
| closeOnBackdropPress | 点击遮罩是否关闭,关闭时按取消结算并经过 beforeClose | boolean | false |
| closeOnBackPress | Android 硬件返回键是否关闭,同样按取消处理 | boolean | true |
| avoidKeyboard | 键盘弹出时是否自动避让,缺省时跟随 showInput | boolean | - |
| 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 结算的就是它。
DialogTheme
底部操作区主题:default 为通栏文字按钮,round-button 为胶囊按钮。
DialogThemeDirection
round-button 主题下按钮的排列方向,default 主题恒为横排。
DialogConfirmColor
确认按钮的颜色语义。
DialogSlots
可通过 classNames 覆盖的 slot 名称,popup 为外层弹出容器(宽度、定位),root 为卡片本身。
DialogBeforeClose
关闭拦截器,返回 false 或 resolve(false) 时阻止关闭;showInput 时第二个参数为当前输入值。
DialogLoading
两个按钮的 loading 状态,异步 beforeClose 期间由组件内部维护。
| 字段 | 类型 | 说明 |
|---|---|---|
| confirm* | boolean | 确认按钮是否处于 loading。 |
| cancel* | boolean | 取消按钮是否处于 loading。 |
DialogOptions
命令式调用的入参,等价于 Omit<DialogProps, 'onClosed' | 'onUpdateShow' | 'show'> 再加上 callback。
| 字段 | 类型 | 说明 |
|---|---|---|
| callback | (action: DialogAction, inputValue?: string) => void | 确认或取消后的通用回调,与 Promise 一同结算,且恰好触发一次。 |