Repository navigation
Conversation
8562995 to
7be180f
Compare
|
The MD3 corrections here should all land - divider colour, the 280/560 width clamp, the icon margin, the centred headline. What I'd like to reconsider is replacing |
|
Also @kachmashk could you resolve the comments that've been addressed? 🙏 |
e2db9f1 to
a0fc3b7
Compare
|
Waiting for a #5076 to be closed. |
|
#5094 removes docs from the repo, can you remove them from this PR? 🙏 |
|
@kachmashk ☝️ and we've got conflicts |
|
Heads up - #5125 is reworking |
The surface stays a dialog, but the prop lands together with the Dialog wiring in callstack#5072 rather than half-done here.
|
Review by Claude The new WebSteps: open the example app at 1280px width, go to Dialog, press "With icon". iOSSteps: rotate the device to landscape, go to Dialog, press "With icon". AndroidSteps: turn on RTL in the drawer, rotate the device to landscape, go to Dialog, press "With icon". |
satya164
left a comment
There was a problem hiding this comment.
Review by Claude
See the inline comments. Also, no test covers the new icon prop or the centered title when an icon is present. A test that renders icon with title and checks textAlign: 'center' on the title would catch a regression. (via Claude, verify if it makes sense)
| minWidth: 280, | ||
| maxWidth: 560, |
There was a problem hiding this comment.
maxWidth: 560 caps the width, but the Modal wrapper still stretches the surface from the start edge. On screens wider than 612 points (web, tablets, phones in landscape) the dialog sits against the left edge, or the right edge in RTL. At 320 points wide the 280 point dialog is also off center (26 on the left, 14 on the right).
Centering the surface fixes both. I checked alignSelf: 'center' in the browser at 1280 and 320 points. Short dialogs then size to their content between 280 and 560 instead of filling the phone width.
| minWidth: 280, | |
| maxWidth: 560, | |
| alignSelf: 'center', | |
| minWidth: 280, | |
| maxWidth: 560, |
Steps and the Android screenshot are in the PR comment above. (via Claude, verify if it makes sense)
| ]} | ||
| theme={theme} | ||
| testID={testID} | ||
| aria-label={ariaLabel ?? (typeof title === 'string' ? title : undefined)} |
There was a problem hiding this comment.
The dialog only gets an accessible name when title is a string. DialogWithCustomColors.tsx passes a <Text> element as title, and that dialog has no name on any platform: web renders role="dialog" with no aria-label, and the iOS and Android accessibility trees show the surface with only its test ID. Any title that is not a plain string has the same problem.
Main now gives Dialog.Title an id from React.useId() and passes aria-labelledby to Modal when aria-label is not set (#5076). Use the same approach here so an element title still names the dialog on web, and pass aria-label="Alert" in DialogWithCustomColors.tsx so it also has a name on iOS and Android. (via Claude, verify if it makes sense)
| const backgroundColor = theme.colors.surfaceContainerHigh; | ||
|
|
||
| const contentNode = | ||
| typeof content === 'string' ? ( |
There was a problem hiding this comment.
content is a ReactNode, so content={count} type checks. A number skips the Text wrapper and renders directly inside the View of Dialog.Content, which throws "Text strings must be rendered within a component." I set content={42} in UndismissableDialog.tsx and got this error on iOS.
| typeof content === 'string' ? ( | |
| typeof content === 'string' || typeof content === 'number' ? ( |
(via Claude, verify if it makes sense)
| const actions = React.Children.toArray(children).filter((child) => | ||
| React.isValidElement<{ style?: StyleProp<ViewStyle> }>(child) | ||
| ); | ||
|
|
||
| return ( | ||
| <View {...props} style={[styles.v3Container, props.style]}> | ||
| {React.Children.map(props.children, (child, i) => | ||
| React.isValidElement<DialogActionChildProps>(child) | ||
| ? React.cloneElement(child, { | ||
| compact: true, | ||
| uppercase: false, | ||
| style: [ | ||
| { | ||
| marginRight: i + 1 === actionsLength ? 0 : 8, | ||
| }, | ||
| child.props.style, | ||
| ], | ||
| }) | ||
| : child | ||
| )} | ||
| <View {...rest} style={[styles.v3Container, style]}> | ||
| {actions.map((child, index) => ( | ||
| <React.Fragment | ||
| key={ | ||
| React.isValidElement(child) && child.key != null ? child.key : index | ||
| } | ||
| > | ||
| {index > 0 && <View style={styles.spacer} />} | ||
| {child} | ||
| </React.Fragment> | ||
| ))} | ||
| </View> | ||
| ); |
There was a problem hiding this comment.
This adds React.Children.toArray, an isValidElement filter and a child.key read on the actions. Inspecting children like this is fragile. It only sees the top level, so a fragment or a custom component that renders two buttons counts as one action and gets no gap between its buttons. It also hides the data flow, because the spacing depends on how the caller shapes children.
Main already solved the spacing in #5076 with columnGap: 8 on the container and plain {children}, so this can match it:
| const actions = React.Children.toArray(children).filter((child) => | |
| React.isValidElement<{ style?: StyleProp<ViewStyle> }>(child) | |
| ); | |
| return ( | |
| <View {...props} style={[styles.v3Container, props.style]}> | |
| {React.Children.map(props.children, (child, i) => | |
| React.isValidElement<DialogActionChildProps>(child) | |
| ? React.cloneElement(child, { | |
| compact: true, | |
| uppercase: false, | |
| style: [ | |
| { | |
| marginRight: i + 1 === actionsLength ? 0 : 8, | |
| }, | |
| child.props.style, | |
| ], | |
| }) | |
| : child | |
| )} | |
| <View {...rest} style={[styles.v3Container, style]}> | |
| {actions.map((child, index) => ( | |
| <React.Fragment | |
| key={ | |
| React.isValidElement(child) && child.key != null ? child.key : index | |
| } | |
| > | |
| {index > 0 && <View style={styles.spacer} />} | |
| {child} | |
| </React.Fragment> | |
| ))} | |
| </View> | |
| ); | |
| return ( | |
| <View {...rest} style={[styles.v3Container, style]}> | |
| {children} | |
| </View> | |
| ); |
Then add columnGap: 8 to v3Container and remove spacer. The "should apply default styles" test also needs a change, because it counts the spacer views instead of checking the visible gap. (via Claude, verify if it makes sense)
| * <Dialog | ||
| * visible={visible} | ||
| * onDismiss={hideDialog} | ||
| * actions={[ | ||
| * <Button key="disagree-btn" onPress={close} textColor={Palette.error50}> | ||
| * Disagree | ||
| * </Button>, | ||
| * <Button key="agree-btn" onPress={close}> | ||
| * Agree | ||
| * </Button>, | ||
| * ]} | ||
| * /> |
There was a problem hiding this comment.
The usage example calls close and Palette.error50, but neither is defined or imported in it. The example defines hideDialog. These lines also mix tabs into the space indentation. Dialog.tsx lines 117 to 119 have the same tabs.
| * <Dialog | |
| * visible={visible} | |
| * onDismiss={hideDialog} | |
| * actions={[ | |
| * <Button key="disagree-btn" onPress={close} textColor={Palette.error50}> | |
| * Disagree | |
| * </Button>, | |
| * <Button key="agree-btn" onPress={close}> | |
| * Agree | |
| * </Button>, | |
| * ]} | |
| * /> | |
| * <Dialog | |
| * visible={visible} | |
| * onDismiss={hideDialog} | |
| * actions={[ | |
| * <Button key="cancel" onPress={hideDialog}>Cancel</Button>, | |
| * <Button key="ok" onPress={hideDialog}>Ok</Button>, | |
| * ]} | |
| * /> |
(via Claude, verify if it makes sense)
| <Dialog | ||
| visible={visible} | ||
| onDismiss={hideDialog} | ||
| icon="alert" |
There was a problem hiding this comment.
The "Before" snippet has no Dialog.Icon, but the "After" snippet adds icon="alert". Readers can take the icon as part of the migration. Remove it so both snippets show the same dialog.
| icon="alert" |
(via Claude, verify if it makes sense)





Motivation
The goal of this PR is to address Material Design 3 guidelines for the Dialog component based on the official documentation as well as this issue. This PR also provides ability to avoid using previous compound components implementation in favor of props directly passed to the Dialog component.
Changes
Material Design 3:
Dialog.ScrollAreadivider border color usesoutline,Dialoghas now restricted minimum and maximum width set to280dpand560dp,DialogIconis the first component in theDialogand it had a total top margin of48dpinstead of24dp,Dialognow detects ifDialog.IconandDialog.Titleare present and if so enforcingDialog.Titleto be centered.Dialog:
Dialogcomponent now supportsicon,title,content,actions,scrollableprops instead of using compound components.Dialogstill uses compound components underneath to properly comply with Material Design specs as these components enforce proper styling,Dialogto ensure the new approach works as intended.Example:
Dialogscreen within Example app with the same dialogs as before but using new API to make sure they look and work as expected.Related issue
Test plan
Reviewers can through new Dialogs added to the Example app. They are duplicated as previous dialogs but they use new approach.