From d5b1361121986a72e9a436f46925ba6c21cd5de8 Mon Sep 17 00:00:00 2001 From: Oskar Kaczmarzyk Date: Thu, 20 Aug 2026 09:26:48 +0200 Subject: [PATCH 1/5] refactor: updated Dialog to comply with Material3 guidelines --- .../6.x/docs/components/Dialog/DialogIcon.mdx | 9 +---- docs/src/data/componentDocs6x.json | 4 +-- .../src/Examples/Dialogs/DialogWithIcon.tsx | 13 ++----- src/components/Dialog/Dialog.tsx | 34 +++++++++++++++---- src/components/Dialog/DialogIcon.tsx | 9 +---- src/components/Dialog/DialogScrollArea.tsx | 2 +- src/components/Dialog/utils.ts | 4 +-- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/6.x/docs/components/Dialog/DialogIcon.mdx b/docs/6.x/docs/components/Dialog/DialogIcon.mdx index 34777210ca..a736d9d384 100644 --- a/docs/6.x/docs/components/Dialog/DialogIcon.mdx +++ b/docs/6.x/docs/components/Dialog/DialogIcon.mdx @@ -20,7 +20,6 @@ A component to show an icon in a Dialog. ## Usage ```js import * as React from 'react'; -import { StyleSheet } from 'react-native'; import { Dialog, Portal, Text } from 'react-native-paper'; const MyComponent = () => { @@ -32,7 +31,7 @@ const MyComponent = () => { - This is a title + This is a title This is simple dialog @@ -41,12 +40,6 @@ const MyComponent = () => { ); }; -const styles = StyleSheet.create({ - title: { - textAlign: 'center', - }, -}) - export default MyComponent; ``` diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 300c315b99..374b82a0dc 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -5400,10 +5400,10 @@ "Dialog/DialogIcon": { "filepath": "Dialog/DialogIcon.tsx", "title": "Dialog.Icon", - "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n title: {\n textAlign: 'center',\n },\n})\n\nexport default MyComponent;\n```", + "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", "link": "dialog-icon", "data": { - "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n title: {\n textAlign: 'center',\n },\n})\n\nexport default MyComponent;\n```", + "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", "displayName": "Dialog.Icon", "methods": [], "statics": [], diff --git a/example/src/Examples/Dialogs/DialogWithIcon.tsx b/example/src/Examples/Dialogs/DialogWithIcon.tsx index 6281e9bf6c..9a5b6477c1 100644 --- a/example/src/Examples/Dialogs/DialogWithIcon.tsx +++ b/example/src/Examples/Dialogs/DialogWithIcon.tsx @@ -1,5 +1,3 @@ -import { StyleSheet } from 'react-native'; - import { Button, Portal, Dialog, Palette } from 'react-native-paper'; import { TextComponent } from './DialogTextComponent'; @@ -15,11 +13,11 @@ const DialogWithIcon = ({ - Dialog with Icon + Dialog with Icon - This is a dialog with new component called DialogIcon. When icon is - displayed you should center the header. + This is a dialog with a component called DialogIcon. When the icon + is displayed, the title is centered automatically. @@ -33,9 +31,4 @@ const DialogWithIcon = ({ ); }; -const styles = StyleSheet.create({ - title: { - textAlign: 'center', - }, -}); export default DialogWithIcon; diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index a7b082fe89..dc2745485c 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -103,6 +103,12 @@ const Dialog = ({ const borderRadius = theme.shapes.corner.extraLarge; const backgroundColor = theme.colors.surfaceContainerHigh; + const dialogChildren = React.Children.toArray(children).filter( + (child) => child != null && typeof child !== 'boolean' + ); + const hasIcon = dialogChildren.some( + (child) => React.isValidElement(child) && child.type === DialogIcon + ); return ( - {React.Children.toArray(children) - .filter((child) => child != null && typeof child !== 'boolean') - .map((child, i) => { - if (i === 0 && React.isValidElement(child)) { + {dialogChildren.map((child, i) => { + if (React.isValidElement(child)) { + const topMarginStyle = + i === 0 && child.type !== DialogIcon + ? { marginTop: 24 } + : undefined; + const titleAlignmentStyle = + hasIcon && child.type === DialogTitle + ? styles.titleWithIcon + : undefined; + + if (topMarginStyle || titleAlignmentStyle) { return React.cloneElement(child, { - style: [{ marginTop: 24 }, child.props.style], + style: [topMarginStyle, child.props.style, titleAlignmentStyle], }); } return child; - })} + } + + return child; + })} ); }; @@ -160,6 +177,11 @@ const styles = StyleSheet.create({ marginVertical: Platform.OS === 'android' ? 44 : 0, elevation: DIALOG_ELEVATION, justifyContent: 'flex-start', + minWidth: 280, + maxWidth: 560, + }, + titleWithIcon: { + textAlign: 'center', }, }); diff --git a/src/components/Dialog/DialogIcon.tsx b/src/components/Dialog/DialogIcon.tsx index 3ced9e12b0..5c533f877e 100644 --- a/src/components/Dialog/DialogIcon.tsx +++ b/src/components/Dialog/DialogIcon.tsx @@ -32,7 +32,6 @@ export type Props = { * ## Usage * ```js * import * as React from 'react'; - * import { StyleSheet } from 'react-native'; * import { Dialog, Portal, Text } from 'react-native-paper'; * * const MyComponent = () => { @@ -44,7 +43,7 @@ export type Props = { * * * - * This is a title + * This is a title * * This is simple dialog * @@ -53,12 +52,6 @@ export type Props = { * ); * }; * - * const styles = StyleSheet.create({ - * title: { - * textAlign: 'center', - * }, - * }) - * * export default MyComponent; * ``` */ diff --git a/src/components/Dialog/DialogScrollArea.tsx b/src/components/Dialog/DialogScrollArea.tsx index a46824be45..5418504783 100644 --- a/src/components/Dialog/DialogScrollArea.tsx +++ b/src/components/Dialog/DialogScrollArea.tsx @@ -52,7 +52,7 @@ const DialogScrollArea = (props: Props) => { const theme = useInternalTheme(props.theme); const { colors } = theme; const borderStyles = { - borderColor: colors.surfaceVariant, + borderColor: colors.outline, borderTopWidth: 1, borderBottomWidth: 1, }; diff --git a/src/components/Dialog/utils.ts b/src/components/Dialog/utils.ts index 64ccec77cc..5413d4c612 100644 --- a/src/components/Dialog/utils.ts +++ b/src/components/Dialog/utils.ts @@ -1,7 +1,7 @@ -import type { StyleProp, ViewStyle } from 'react-native'; +import type { StyleProp, TextStyle, ViewStyle } from 'react-native'; export type DialogChildProps = { - style?: StyleProp; + style?: StyleProp; }; export type DialogActionChildProps = DialogChildProps & { From 49442efee47ef512bd8d0f5a3f555e7adb0412ed Mon Sep 17 00:00:00 2001 From: Oskar Kaczmarzyk Date: Tue, 25 Aug 2026 09:54:22 +0200 Subject: [PATCH 2/5] refactor: introduced new Dialog API, updated docs --- docs/6.x/docs/components/Dialog/Dialog.mdx | 59 ++++- docs/src/data/componentDocs6x.json | 4 +- src/components/Dialog/Dialog.tsx | 267 +++++++++++++++++---- 3 files changed, 271 insertions(+), 59 deletions(-) diff --git a/docs/6.x/docs/components/Dialog/Dialog.mdx b/docs/6.x/docs/components/Dialog/Dialog.mdx index e871f7f750..8a9f1381c8 100644 --- a/docs/6.x/docs/components/Dialog/Dialog.mdx +++ b/docs/6.x/docs/components/Dialog/Dialog.mdx @@ -11,6 +11,19 @@ import ExtendedExample from '@docs/components/ExtendedExample.tsx'; Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks. To render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal/Portal) component. +## Recommended props + +| Prop | Type | Description | +| --- | --- | --- | +| `icon` | `IconSource` | Icon rendered through `Dialog.Icon`. | +| `title` | `ReactNode` | Dialog title rendered through `Dialog.Title`. | +| `content` | `ReactNode` | Required dialog content. Non-empty strings use Material 3 supporting-text styles. | +| `actions` | `DialogActionsProps[]` | Action labels, press handlers, and Button props. | +| `scrollable` | `boolean` | Renders content through `Dialog.ScrollArea` and a `ScrollView`. | +| `contentProps` | `DialogContentProps` | Props forwarded to `Dialog.Content`. | +| `scrollAreaProps` | `DialogScrollAreaProps` | Props forwarded to `Dialog.ScrollArea`. | +| `scrollViewProps` | `ScrollViewProps` | Props forwarded to `ScrollView`. | + @@ -21,7 +34,7 @@ To render the `Dialog` above other components, you'll need to wrap it with the [ ```js import * as React from 'react'; import { View } from 'react-native'; -import { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper'; +import { Button, Dialog, Portal, PaperProvider } from 'react-native-paper'; const MyComponent = () => { const [visible, setVisible] = React.useState(false); @@ -35,15 +48,13 @@ const MyComponent = () => { - - Alert - - This is simple dialog - - - - - + @@ -53,6 +64,32 @@ const MyComponent = () => { export default MyComponent; ``` +## Compound composition + +`Dialog.Icon`, `Dialog.Title`, `Dialog.Content`, `Dialog.ScrollArea`, and +`Dialog.Actions` remain available for custom composition within `Dialog`. +Passing them through `children` is deprecated; prefer the props above. + +## Migrating from children + +```js +// Before + + Alert + Something happened. + + + +// After + +``` + ## Props @@ -93,7 +130,7 @@ export default MyComponent;
-### children (required) +### children (depracated, use props instead)
diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 374b82a0dc..f6d7ca856c 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -5206,10 +5206,10 @@ "Dialog/Dialog": { "filepath": "Dialog/Dialog.tsx", "title": "Dialog", - "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n Alert\n \n This is simple dialog\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Recommended props\n\n| Prop | Type | Description |\n| --- | --- | --- |\n| `icon` | `IconSource` | Icon rendered through `Dialog.Icon`. |\n| `title` | `ReactNode` | Dialog title rendered through `Dialog.Title`. |\n| `content` | `ReactNode` | Required dialog content. Non-empty strings use Material 3 supporting-text styles. |\n| `actions` | `DialogActionsProps[]` | Action labels, press handlers, and optional Button props. |\n| `scrollable` | `boolean` | Renders content through `Dialog.ScrollArea` and a `ScrollView`. |\n| `contentProps` | `DialogContentProps` | Props forwarded to `Dialog.Content`. |\n| `scrollAreaProps` | `DialogScrollAreaProps` | Props forwarded to `Dialog.ScrollArea`. |\n| `scrollViewProps` | `ScrollViewProps` | Props forwarded to `ScrollView`. |\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```\n\n## Compound composition\n\n`Dialog.Icon`, `Dialog.Title`, `Dialog.Content`, `Dialog.ScrollArea`, and\n`Dialog.Actions` remain available for custom composition within `Dialog`.\nPassing them through `children` is deprecated; prefer the props above.\n\n## Migrating from children\n\n```js\n// Before\n\n Alert\n Something happened.\n \n\n\n// After\n\n```", "link": "dialog", "data": { - "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n Alert\n \n This is simple dialog\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Recommended props\n\n| Prop | Type | Description |\n| --- | --- | --- |\n| `icon` | `IconSource` | Icon rendered through `Dialog.Icon`. |\n| `title` | `ReactNode` | Dialog title rendered through `Dialog.Title`. |\n| `content` | `ReactNode` | Required dialog content. Non-empty strings use Material 3 supporting-text styles. |\n| `actions` | `DialogActionsProps[]` | Action labels, press handlers, and optional Button props. |\n| `scrollable` | `boolean` | Renders content through `Dialog.ScrollArea` and a `ScrollView`. |\n| `contentProps` | `DialogContentProps` | Props forwarded to `Dialog.Content`. |\n| `scrollAreaProps` | `DialogScrollAreaProps` | Props forwarded to `Dialog.ScrollArea`. |\n| `scrollViewProps` | `ScrollViewProps` | Props forwarded to `ScrollView`. |\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```\n\n## Compound composition\n\n`Dialog.Icon`, `Dialog.Title`, `Dialog.Content`, `Dialog.ScrollArea`, and\n`Dialog.Actions` remain available for custom composition within `Dialog`.\nPassing them through `children` is deprecated; prefer the props above.\n\n## Migrating from children\n\n```js\n// Before\n\n Alert\n Something happened.\n \n\n\n// After\n\n```", "displayName": "Dialog", "methods": [], "statics": [], diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index dc2745485c..cce05e9a47 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -1,20 +1,26 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet } from 'react-native'; -import type { StyleProp, ViewStyle } from 'react-native'; +import { Animated, Platform, ScrollView, StyleSheet } from 'react-native'; +import type { ScrollViewProps, StyleProp, ViewStyle } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import DialogActions from './DialogActions'; import DialogContent from './DialogContent'; +import type { Props as DialogContentProps } from './DialogContent'; import DialogIcon from './DialogIcon'; import DialogScrollArea from './DialogScrollArea'; +import type { Props as DialogScrollAreaProps } from './DialogScrollArea'; import DialogTitle from './DialogTitle'; +import type { DialogChildProps } from './utils'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; +import Button from '../Button/Button'; +import type { Props as ButtonProps } from '../Button/Button'; +import type { IconSource } from '../Icon'; import Modal from '../Modal'; -import type { DialogChildProps } from './utils'; +import Text from '../Typography/Text'; -export type Props = { +type CommonProps = { /** * Determines whether clicking outside the dialog dismiss it. */ @@ -31,10 +37,6 @@ export type Props = { * Determines Whether the dialog is visible. */ visible: boolean; - /** - * Content of the `Dialog`. - */ - children: React.ReactNode; style?: Animated.WithAnimatedValue>; /** * @optional @@ -46,17 +48,120 @@ export type Props = { testID?: string; }; +type DialogActionsProps = Omit & { label: string }; + +type SlotProps = { + /** + * Icon to display above the dialog title. + */ + icon?: IconSource; + /** + * Title of the dialog. + */ + title?: React.ReactNode; + /** + * Content of the dialog. Non-empty strings are rendered as Material 3 + * supporting text. + */ + content: React.ReactNode; + /** + * Action buttons displayed at the bottom of the dialog. + * Keep their order stable between renders. + */ + actions: DialogActionsProps[]; + /** + * Whether to render the content in a `ScrollView` within the dialog scroll + * area. + */ + scrollable?: boolean; + /** + * Props passed to `Dialog.Content` when `scrollable` is not enabled. + */ + contentProps?: Omit; + /** + * Props passed to `Dialog.ScrollArea` when `scrollable` is enabled. + */ + scrollAreaProps?: Omit; + /** + * Props passed to the `ScrollView` when `scrollable` is enabled. + */ + scrollViewProps?: Omit; + children?: never; +}; + +type LegacyProps = { + /** + * Content of the `Dialog` composed with `Dialog.Icon`, `Dialog.Title`, + * `Dialog.Content`, `Dialog.ScrollArea`, and `Dialog.Actions`. + * + * @deprecated Use the `icon`, `title`, `content`, `actions`, and `scrollable` + * props instead. Compound children remain available for custom composition. + */ + children: React.ReactNode; + icon?: never; + title?: never; + content?: never; + actions?: never; + scrollable?: never; + contentProps?: never; + scrollAreaProps?: never; + scrollViewProps?: never; +}; + +export type Props = CommonProps & (SlotProps | LegacyProps); + const DIALOG_ELEVATION: number = 24; +const renderChildren = (children: React.ReactNode) => { + const dialogChildren = React.Children.toArray(children).filter( + (child) => child != null && typeof child !== 'boolean' + ); + const hasIcon = dialogChildren.some( + (child) => React.isValidElement(child) && child.type === DialogIcon + ); + + return dialogChildren.map((child, i) => { + if (React.isValidElement(child)) { + const topMarginStyle = + i === 0 && child.type !== DialogIcon ? styles.firstChild : undefined; + const titleAlignmentStyle = + hasIcon && child.type === DialogTitle + ? styles.titleWithIcon + : undefined; + + if (topMarginStyle || titleAlignmentStyle) { + return React.cloneElement(child, { + style: [topMarginStyle, child.props.style, titleAlignmentStyle], + }); + } + } + + return child; + }); +}; + /** * Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks. * To render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component. * + * ## Recommended props + * + * | Prop | Type | Description | + * | --- | --- | --- | + * | `icon` | `IconSource` | Icon rendered through `Dialog.Icon`. | + * | `title` | `ReactNode` | Dialog title rendered through `Dialog.Title`. | + * | `content` | `string | ReactNode` | Required dialog content. Non-empty strings use Material 3 supporting-text styles. | + * | `actions` | `DialogActionsProps[]` | Action labels, press handlers, and optional Button props. | + * | `scrollable` | `boolean` | Renders content through `Dialog.ScrollArea` and a `ScrollView`. | + * | `contentProps` | `DialogContentProps` | Props forwarded to `Dialog.Content`. | + * | `scrollAreaProps` | `DialogScrollAreaProps` | Props forwarded to `Dialog.ScrollArea`. | + * | `scrollViewProps` | `ScrollViewProps` | Props forwarded to the generated `ScrollView`. | + * * ## Usage * ```js * import * as React from 'react'; * import { View } from 'react-native'; - * import { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper'; + * import { Button, Dialog, Portal, PaperProvider } from 'react-native-paper'; * * const MyComponent = () => { * const [visible, setVisible] = React.useState(false); @@ -70,15 +175,13 @@ const DIALOG_ELEVATION: number = 24; * * * - * - * Alert - * - * This is simple dialog - * - * - * - * - * + * * * * @@ -87,6 +190,32 @@ const DIALOG_ELEVATION: number = 24; * * export default MyComponent; * ``` + * + * ## Compound composition + * + * `Dialog.Icon`, `Dialog.Title`, `Dialog.Content`, `Dialog.ScrollArea`, and + * `Dialog.Actions` remain available for custom composition within `Dialog`. + * Passing them through `children` is deprecated; prefer the props above. + * + * ## Migrating from children + * + * ```js + * // Before + * + * Alert + * Something happened. + * + * + * + * // After + * + * ``` */ const Dialog = ({ children, @@ -97,18 +226,82 @@ const Dialog = ({ style, theme: themeOverrides, testID, + ...props }: Props) => { const { right, left } = useSafeAreaInsets(); const theme = useInternalTheme(themeOverrides); const borderRadius = theme.shapes.corner.extraLarge; const backgroundColor = theme.colors.surfaceContainerHigh; - const dialogChildren = React.Children.toArray(children).filter( - (child) => child != null && typeof child !== 'boolean' - ); - const hasIcon = dialogChildren.some( - (child) => React.isValidElement(child) && child.type === DialogIcon - ); + + const _children = React.useMemo(() => { + if (children) return children; + + const { + actions, + content, + icon, + scrollable, + contentProps, + scrollAreaProps, + scrollViewProps, + title, + } = props; + + const dialogIcon = icon ? ( + + ) : null; + const dialogTitle = title ? {title} : null; + + const contentNode = + typeof content === 'string' ? ( + + {content} + + ) : ( + content + ); + + const dialogContent = scrollable ? ( + + {contentNode} + + ) : ( + + {contentNode} + + ); + + const dialogActions = actions?.length ? ( + + {actions.map( + ({ label, onPress: onActionPress, ...buttonProps }, index) => ( + + ) + )} + + ) : null; + + return [dialogIcon, dialogTitle, dialogContent, dialogActions]; + }, [children, props, theme.colors.onSurfaceVariant]); return ( - {dialogChildren.map((child, i) => { - if (React.isValidElement(child)) { - const topMarginStyle = - i === 0 && child.type !== DialogIcon - ? { marginTop: 24 } - : undefined; - const titleAlignmentStyle = - hasIcon && child.type === DialogTitle - ? styles.titleWithIcon - : undefined; - - if (topMarginStyle || titleAlignmentStyle) { - return React.cloneElement(child, { - style: [topMarginStyle, child.props.style, titleAlignmentStyle], - }); - } - - return child; - } - - return child; - })} + {renderChildren(_children)} ); }; @@ -183,6 +355,9 @@ const styles = StyleSheet.create({ titleWithIcon: { textAlign: 'center', }, + firstChild: { + marginTop: 24, + }, }); export default Dialog; From ab72a9c2020fe17faf64503aadf9df6104da36b6 Mon Sep 17 00:00:00 2001 From: Oskar Kaczmarzyk Date: Tue, 25 Aug 2026 09:55:03 +0200 Subject: [PATCH 3/5] refactor: added new test cases for new Dialog API --- src/components/Dialog/DialogIcon.tsx | 2 +- src/components/__tests__/Dialog.test.tsx | 47 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/components/Dialog/DialogIcon.tsx b/src/components/Dialog/DialogIcon.tsx index 5c533f877e..d3e5abcfab 100644 --- a/src/components/Dialog/DialogIcon.tsx +++ b/src/components/Dialog/DialogIcon.tsx @@ -64,7 +64,7 @@ const DialogIcon = ({ const theme = useInternalTheme(themeOverrides); const { colors } = theme; - //@ts-ignore + // @ts-ignore const iconColor = color || colors.secondary; return ( diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx index 32477e5fe1..7cfe7af6f6 100644 --- a/src/components/__tests__/Dialog.test.tsx +++ b/src/components/__tests__/Dialog.test.tsx @@ -107,6 +107,53 @@ describe('Dialog', () => { marginTop: 24, }); }); + + it('should render a content', async () => { + await render( + + ); + + expect(screen.getByText('Content')).toBeOnTheScreen(); + }); + + it('should render string content in a scroll area', async () => { + await render( + + ); + + expect(screen.getByTestId('dialog-scroll-area')).toBeOnTheScreen(); + expect(screen.getByTestId('dialog-scroll-view')).toBeOnTheScreen(); + expect(screen.getByText('Scrollable content')).toBeOnTheScreen(); + }); + + it('should render passed action', async () => { + await render( + + ); + + expect(screen.getByTestId('cancel-btn')).toBeOnTheScreen(); + }); }); describe('DialogActions', () => { From 4b92272f46e372fe94f45b4db2330e4063f9dbaf Mon Sep 17 00:00:00 2001 From: Oskar Kaczmarzyk Date: Tue, 25 Aug 2026 09:56:44 +0200 Subject: [PATCH 4/5] refactor: updated Examples, duplicated dialogs using new Dialog API --- example/src/Examples/DialogExample.tsx | 96 ++++++++++++- .../Dialogs/NewDialogWithCustomColors.tsx | 47 +++++++ .../NewDialogWithDismissableBackButton.tsx | 31 +++++ .../Examples/Dialogs/NewDialogWithIcon.tsx | 29 ++++ .../Dialogs/NewDialogWithLoadingIndicator.tsx | 51 +++++++ .../Dialogs/NewDialogWithLongText.tsx | 38 ++++++ .../Dialogs/NewDialogWithRadioBtns.tsx | 126 ++++++++++++++++++ .../Dialogs/NewUndismissableDialog.tsx | 30 +++++ example/src/Examples/Dialogs/index.tsx | 7 + 9 files changed, 452 insertions(+), 3 deletions(-) create mode 100644 example/src/Examples/Dialogs/NewDialogWithCustomColors.tsx create mode 100644 example/src/Examples/Dialogs/NewDialogWithDismissableBackButton.tsx create mode 100644 example/src/Examples/Dialogs/NewDialogWithIcon.tsx create mode 100644 example/src/Examples/Dialogs/NewDialogWithLoadingIndicator.tsx create mode 100644 example/src/Examples/Dialogs/NewDialogWithLongText.tsx create mode 100644 example/src/Examples/Dialogs/NewDialogWithRadioBtns.tsx create mode 100644 example/src/Examples/Dialogs/NewUndismissableDialog.tsx diff --git a/example/src/Examples/DialogExample.tsx b/example/src/Examples/DialogExample.tsx index d926ae6616..1c7e1b6686 100644 --- a/example/src/Examples/DialogExample.tsx +++ b/example/src/Examples/DialogExample.tsx @@ -10,6 +10,13 @@ import { DialogWithLoadingIndicator, DialogWithLongText, DialogWithRadioBtns, + NewDialogWithCustomColors, + NewDialogWithDismissableBackButton, + NewDialogWithIcon, + NewDialogWithLoadingIndicator, + NewDialogWithLongText, + NewDialogWithRadioBtns, + NewUndismissableDialog, UndismissableDialog, } from './Dialogs'; import ScreenWrapper from '../ScreenWrapper'; @@ -79,6 +86,57 @@ const DialogExample = () => { Dismissable back button )} + + + + + + + {Platform.OS === 'android' && ( + + )} { visible={_getVisible('dialog6')} close={_toggleDialog('dialog6')} /> - + )} + + + + + + + {Platform.OS === 'android' && ( + + )} ); }; diff --git a/example/src/Examples/Dialogs/NewDialogWithCustomColors.tsx b/example/src/Examples/Dialogs/NewDialogWithCustomColors.tsx new file mode 100644 index 0000000000..f7b700e43c --- /dev/null +++ b/example/src/Examples/Dialogs/NewDialogWithCustomColors.tsx @@ -0,0 +1,47 @@ +import { StyleSheet } from 'react-native'; + +import { Dialog, Palette, Portal, Text } from 'react-native-paper'; + +const NewDialogWithCustomColors = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => ( + + + Alert + + } + content={ + + This is a dialog with custom colors + + } + actions={[ + { + label: 'Ok', + onPress: close, + textColor: Palette.primary95, + }, + ]} + /> + +); + +const styles = StyleSheet.create({ + dialog: { + backgroundColor: Palette.primary10, + }, + text: { + color: Palette.primary95, + }, +}); + +export default NewDialogWithCustomColors; diff --git a/example/src/Examples/Dialogs/NewDialogWithDismissableBackButton.tsx b/example/src/Examples/Dialogs/NewDialogWithDismissableBackButton.tsx new file mode 100644 index 0000000000..c53c10c0c2 --- /dev/null +++ b/example/src/Examples/Dialogs/NewDialogWithDismissableBackButton.tsx @@ -0,0 +1,31 @@ +import { Dialog, Palette, Portal } from 'react-native-paper'; + +const NewDialogWithDismissableBackButton = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => ( + + + +); + +export default NewDialogWithDismissableBackButton; diff --git a/example/src/Examples/Dialogs/NewDialogWithIcon.tsx b/example/src/Examples/Dialogs/NewDialogWithIcon.tsx new file mode 100644 index 0000000000..640fe244e1 --- /dev/null +++ b/example/src/Examples/Dialogs/NewDialogWithIcon.tsx @@ -0,0 +1,29 @@ +import { Dialog, Palette, Portal } from 'react-native-paper'; + +const NewDialogWithIcon = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => ( + + + +); + +export default NewDialogWithIcon; diff --git a/example/src/Examples/Dialogs/NewDialogWithLoadingIndicator.tsx b/example/src/Examples/Dialogs/NewDialogWithLoadingIndicator.tsx new file mode 100644 index 0000000000..ea7aaa9279 --- /dev/null +++ b/example/src/Examples/Dialogs/NewDialogWithLoadingIndicator.tsx @@ -0,0 +1,51 @@ +import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'; + +import { Dialog, Palette, Portal, Text, useTheme } from 'react-native-paper'; + +const isIOS = Platform.OS === 'ios'; + +const NewDialogWithLoadingIndicator = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => { + const theme = useTheme(); + const textColor = { color: theme.colors.onSurfaceVariant }; + + return ( + + + + + Loading..... + + + } + actions={[]} + /> + + ); +}; + +const styles = StyleSheet.create({ + content: { + flexDirection: 'row', + alignItems: 'center', + }, + indicator: { + marginRight: 16, + }, +}); + +export default NewDialogWithLoadingIndicator; diff --git a/example/src/Examples/Dialogs/NewDialogWithLongText.tsx b/example/src/Examples/Dialogs/NewDialogWithLongText.tsx new file mode 100644 index 0000000000..6a5a76f5f8 --- /dev/null +++ b/example/src/Examples/Dialogs/NewDialogWithLongText.tsx @@ -0,0 +1,38 @@ +import { Dimensions, StyleSheet } from 'react-native'; + +import { Portal, Dialog } from 'react-native-paper'; + +const NewDialogWithLongText = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => ( + + + +); + +const styles = StyleSheet.create({ + scrollArea: { + paddingHorizontal: 0, + }, + scrollViewContent: { + paddingHorizontal: 24, + }, +}); + +export default NewDialogWithLongText; diff --git a/example/src/Examples/Dialogs/NewDialogWithRadioBtns.tsx b/example/src/Examples/Dialogs/NewDialogWithRadioBtns.tsx new file mode 100644 index 0000000000..01e8a35f26 --- /dev/null +++ b/example/src/Examples/Dialogs/NewDialogWithRadioBtns.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; + +import { + Portal, + Dialog, + RadioButton, + Text, + TouchableRipple, + useTheme, +} from 'react-native-paper'; + +type Props = { + visible: boolean; + close: () => void; +}; + +type CheckedState = 'normal' | 'first' | 'second' | 'third' | 'fourth'; + +const NewDialogWithRadioBtns = ({ visible, close }: Props) => { + const [checked, setChecked] = React.useState('normal'); + const theme = useTheme(); + const optionTextColor = { color: theme.colors.onSurfaceVariant }; + + return ( + + + setChecked('normal')}> + + + + + + Option 1 + + + + setChecked('second')}> + + + + + + Option 2 + + + + setChecked('third')}> + + + + + + Option 3 + + + + setChecked('fourth')}> + + + + + + Option 4 + + + + + } + actions={[ + { label: 'Cancel', onPress: close }, + { label: 'Ok', onPress: close }, + ]} + /> + + ); +}; + +export default NewDialogWithRadioBtns; + +const styles = StyleSheet.create({ + container: { + maxHeight: 170, + paddingHorizontal: 0, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 8, + }, + text: { + paddingLeft: 8, + }, +}); diff --git a/example/src/Examples/Dialogs/NewUndismissableDialog.tsx b/example/src/Examples/Dialogs/NewUndismissableDialog.tsx new file mode 100644 index 0000000000..ff0e984cab --- /dev/null +++ b/example/src/Examples/Dialogs/NewUndismissableDialog.tsx @@ -0,0 +1,30 @@ +import { Dialog, Palette, Portal } from 'react-native-paper'; + +const NewUndismissableDialog = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => ( + + + +); + +export default NewUndismissableDialog; diff --git a/example/src/Examples/Dialogs/index.tsx b/example/src/Examples/Dialogs/index.tsx index 7af735d036..9b320a67fc 100644 --- a/example/src/Examples/Dialogs/index.tsx +++ b/example/src/Examples/Dialogs/index.tsx @@ -5,3 +5,10 @@ export { default as DialogWithRadioBtns } from './DialogWithRadioBtns'; export { default as UndismissableDialog } from './UndismissableDialog'; export { default as DialogWithIcon } from './DialogWithIcon'; export { default as DialogWithDismissableBackButton } from './DialogWithDismissableBackButton'; +export { default as NewDialogWithCustomColors } from './NewDialogWithCustomColors'; +export { default as NewDialogWithDismissableBackButton } from './NewDialogWithDismissableBackButton'; +export { default as NewDialogWithIcon } from './NewDialogWithIcon'; +export { default as NewDialogWithLoadingIndicator } from './NewDialogWithLoadingIndicator'; +export { default as NewDialogWithLongText } from './NewDialogWithLongText'; +export { default as NewDialogWithRadioBtns } from './NewDialogWithRadioBtns'; +export { default as NewUndismissableDialog } from './NewUndismissableDialog'; From bc47bf4d54fbffcf0fafabb500ab2a29f49fcbaa Mon Sep 17 00:00:00 2001 From: Oskar Kaczmarzyk Date: Tue, 25 Aug 2026 12:01:09 +0200 Subject: [PATCH 5/5] docs: updated docs for Dialog compound components --- src/components/Dialog/Dialog.tsx | 4 ++-- src/components/Dialog/DialogActions.tsx | 15 +++++++++++++++ src/components/Dialog/DialogContent.tsx | 8 ++++++++ src/components/Dialog/DialogIcon.tsx | 8 ++++++++ src/components/Dialog/DialogScrollArea.tsx | 8 ++++++++ src/components/Dialog/DialogTitle.tsx | 8 ++++++++ 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index cce05e9a47..561f70f035 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -150,12 +150,12 @@ const renderChildren = (children: React.ReactNode) => { * | --- | --- | --- | * | `icon` | `IconSource` | Icon rendered through `Dialog.Icon`. | * | `title` | `ReactNode` | Dialog title rendered through `Dialog.Title`. | - * | `content` | `string | ReactNode` | Required dialog content. Non-empty strings use Material 3 supporting-text styles. | + * | `content` | `ReactNode` | Required dialog content. Non-empty strings use Material 3 supporting-text styles. | * | `actions` | `DialogActionsProps[]` | Action labels, press handlers, and optional Button props. | * | `scrollable` | `boolean` | Renders content through `Dialog.ScrollArea` and a `ScrollView`. | * | `contentProps` | `DialogContentProps` | Props forwarded to `Dialog.Content`. | * | `scrollAreaProps` | `DialogScrollAreaProps` | Props forwarded to `Dialog.ScrollArea`. | - * | `scrollViewProps` | `ScrollViewProps` | Props forwarded to the generated `ScrollView`. | + * | `scrollViewProps` | `ScrollViewProps` | Props forwarded to the `ScrollView`. | * * ## Usage * ```js diff --git a/src/components/Dialog/DialogActions.tsx b/src/components/Dialog/DialogActions.tsx index 7e3799451e..10bf16a69b 100644 --- a/src/components/Dialog/DialogActions.tsx +++ b/src/components/Dialog/DialogActions.tsx @@ -31,6 +31,7 @@ export type Props = ViewProps & { * * const hideDialog = () => setVisible(false); * + * // Before * return ( * * @@ -41,6 +42,20 @@ export type Props = ViewProps & { * * * ); + * + * // V6 and later + * return ( + * + * console.log('Cancel'), label: 'Cancel' }, + * { onPress: () => console.log('Ok'), label: 'Ok' }, + * ]} + * /> + * + * ); * }; * * export default MyComponent; diff --git a/src/components/Dialog/DialogContent.tsx b/src/components/Dialog/DialogContent.tsx index a084188b32..fe953396a7 100644 --- a/src/components/Dialog/DialogContent.tsx +++ b/src/components/Dialog/DialogContent.tsx @@ -23,6 +23,7 @@ export type Props = ViewProps & { * * const hideDialog = () => setVisible(false); * + * // Before * return ( * * @@ -32,6 +33,13 @@ export type Props = ViewProps & { * * * ); + * + * // V6 and later + * return ( + * + * + * + * ); * }; * * export default MyComponent; diff --git a/src/components/Dialog/DialogIcon.tsx b/src/components/Dialog/DialogIcon.tsx index d3e5abcfab..a016f69995 100644 --- a/src/components/Dialog/DialogIcon.tsx +++ b/src/components/Dialog/DialogIcon.tsx @@ -39,6 +39,7 @@ export type Props = { * * const hideDialog = () => setVisible(false); * + * // Before * return ( * * @@ -50,6 +51,13 @@ export type Props = { * * * ); + * + * // V6 and later + * return ( + * + * + * + * ); * }; * * export default MyComponent; diff --git a/src/components/Dialog/DialogScrollArea.tsx b/src/components/Dialog/DialogScrollArea.tsx index 5418504783..2c552b6f32 100644 --- a/src/components/Dialog/DialogScrollArea.tsx +++ b/src/components/Dialog/DialogScrollArea.tsx @@ -32,6 +32,7 @@ export type Props = ViewProps & { * * const hideDialog = () => setVisible(false); * + * // Before * return ( * * @@ -43,6 +44,13 @@ export type Props = ViewProps & { * * * ); + * + * // V6 and later + * return ( + * + * + * + * ); * }; * * export default MyComponent; diff --git a/src/components/Dialog/DialogTitle.tsx b/src/components/Dialog/DialogTitle.tsx index ae553daee2..2a2acb5c1a 100644 --- a/src/components/Dialog/DialogTitle.tsx +++ b/src/components/Dialog/DialogTitle.tsx @@ -31,6 +31,7 @@ export type Props = React.ComponentPropsWithRef & { * * const hideDialog = () => setVisible(false); * + * // Before * return ( * * @@ -41,6 +42,13 @@ export type Props = React.ComponentPropsWithRef & { * * * ); + * + * // V6 and later + * return ( + * + * + * + * ); * }; * * export default MyComponent;