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/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..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": [], @@ -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/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/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/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'; diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index a7b082fe89..561f70f035 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` | `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 `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,6 +226,7 @@ const Dialog = ({ style, theme: themeOverrides, testID, + ...props }: Props) => { const { right, left } = useSafeAreaInsets(); const theme = useInternalTheme(themeOverrides); @@ -104,6 +234,75 @@ const Dialog = ({ const backgroundColor = theme.colors.surfaceContainerHigh; + 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 ( - {React.Children.toArray(children) - .filter((child) => child != null && typeof child !== 'boolean') - .map((child, i) => { - if (i === 0 && React.isValidElement(child)) { - return React.cloneElement(child, { - style: [{ marginTop: 24 }, child.props.style], - }); - } - - return child; - })} + {renderChildren(_children)} ); }; @@ -160,6 +349,14 @@ const styles = StyleSheet.create({ marginVertical: Platform.OS === 'android' ? 44 : 0, elevation: DIALOG_ELEVATION, justifyContent: 'flex-start', + minWidth: 280, + maxWidth: 560, + }, + titleWithIcon: { + textAlign: 'center', + }, + firstChild: { + marginTop: 24, }, }); 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 3ced9e12b0..a016f69995 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 = () => { @@ -40,24 +39,26 @@ export type Props = { * * const hideDialog = () => setVisible(false); * + * // Before * return ( * * * - * This is a title + * This is a title * * This is simple dialog * * * * ); - * }; * - * const styles = StyleSheet.create({ - * title: { - * textAlign: 'center', - * }, - * }) + * // V6 and later + * return ( + * + * + * + * ); + * }; * * export default MyComponent; * ``` @@ -71,7 +72,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/Dialog/DialogScrollArea.tsx b/src/components/Dialog/DialogScrollArea.tsx index a46824be45..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; @@ -52,7 +60,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/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; 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 & { 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', () => {