Skip to content

refactor(dialog): MD3 updates, updated Dialog API - #5072

Open
kachmashk wants to merge 8 commits into
callstack:mainfrom
kachmashk:refactor/dialog-improvements
Open

refactor(dialog): MD3 updates, updated Dialog API#5072
kachmashk wants to merge 8 commits into
callstack:mainfrom
kachmashk:refactor/dialog-improvements

Conversation

@kachmashk

@kachmashk kachmashk commented Aug 25, 2026

Copy link
Copy Markdown

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.ScrollArea divider border color uses outline,
  • Dialog has now restricted minimum and maximum width set to 280dp and 560dp,
  • Fixed an issue when DialogIcon is the first component in the Dialog and it had a total top margin of 48dp instead of 24dp,
  • Dialog now detects if Dialog.Icon and Dialog.Title are present and if so enforcing Dialog.Title to be centered.

Dialog:

  • Dialog component now supports icon, title, content, actions, scrollable props instead of using compound components. Dialog still uses compound components underneath to properly comply with Material Design specs as these components enforce proper styling,
  • Added new test cases for Dialog to ensure the new approach works as intended.

Example:

  • Updated Dialog screen 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

  • lint
  • typecheck
  • test

Reviewers can through new Dialogs added to the Example app. They are duplicated as previous dialogs but they use new approach.

Comment thread src/components/Dialog/Dialog.tsx Outdated

const DIALOG_ELEVATION: number = 24;

const renderChildren = (children: React.ReactNode) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React.Children + cloneElement is the composition problem v6 is meant to remove (#4954), and this adds child.type === sniffing on top. On the prop path you build the array yourself - pass the margin and centring down as props instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed and updated!

Comment thread src/components/Dialog/Dialog.tsx Outdated
children?: never;
};

type LegacyProps = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves v6 with two Dialog APIs - children deprecated but kept, plus seven duplicated NewDialog* example screens. Which one ships? A major shouldn't carry a deprecation shim, but dropping children is a break worth deciding deliberately.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed legacy approach with children. Removed all NewDialog* examples and replaced older examples with new implementation.

const { colors } = theme;
const borderStyles = {
borderColor: colors.surfaceVariant,
borderColor: colors.outline,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-    borderColor: colors.outline,
+    borderColor: colors.outlineVariant,

md.comp.dialog.with-divider.divider.color is deprecated in favour of md.comp.divider.color - outline-variant, which is what Divider already uses.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed!

Comment thread src/components/Dialog/Dialog.tsx Outdated

if (topMarginStyle || titleAlignmentStyle) {
return React.cloneElement(child, {
style: [topMarginStyle, child.props.style, titleAlignmentStyle],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-          style: [topMarginStyle, child.props.style, titleAlignmentStyle],
+          style: [topMarginStyle, titleAlignmentStyle, child.props.style],

As written, <Dialog.Title style={{ textAlign: 'left' }}> stops working once there's an icon.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I wanted to achieve. I can see in the guidelines that Title must be always centered when Icon is visible. Would you like to drop this restriction?

Comment thread src/components/Dialog/Dialog.tsx Outdated
* Content of the dialog. Non-empty strings are rendered as Material 3
* supporting text.
*/
content: React.ReactNode;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content and actions are required, so a title-only dialog won't typecheck and NewDialogWithLoadingIndicator.tsx:35 has to pass actions={[]}. Make both optional.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guidelines say that content and actions are not optional. Should we override it and allow title-only dialogs? DialogWithLoadingIndicator.tsx uses an empty array for actions but in this case it's against the guidelines and shouldn't be used this way. Do we still want content and actions as optional?

Comment thread src/components/Dialog/Dialog.tsx Outdated
testID?: string;
};

type DialogActionsProps = Omit<ButtonProps, 'children'> & { label: string };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why { label }[] rather than actions?: React.ReactNode? An array of Button props can't express a custom action component, and it's a second shape to keep in sync with Button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a proposed example for actions and it looked solid but you're right that it cannot express a custom action component.

I changed actions into React.ReactNode[] type which results in example usage:

actions={[
   <Button key="disagree-btn" onPress={close} textColor={Palette.error50}>
      Disagree
   </Button>,
   <Button key="agree-btn" onPress={close}>
      Agree
   </Button>,
]}

This way we simplify a composition and let developers decide what action component they would like to use.

I could still change it to React.ReactNode instead and we would have these possibilities:

  1. Developers to keep using Dialog.Actions which does not simplify using Dialog component as title or content props.
actions={
   <Dialog.Actions>
      <Button onPress={close} textColor={Palette.error50}>
         Disagree
      </Button>
      <Button onPress={close}>
         Agree
      </Button>
   </Dialog.Actions>
]}
  1. We would accept a React.ReactNode but it would add additional logic to the DialogActions.tsx to unwrap one level of children to properly assign a styling to them. Each child is supposed to have marginRight: 8 expect the last one.
actions={
   <>
      <Button onPress={close} textColor={Palette.error50}>
         Disagree
      </Button>
      <Button onPress={close}>
         Agree
      </Button>
   </>
]}

What do you think?

Comment thread src/components/Dialog/Dialog.tsx Outdated
) : null;

return [dialogIcon, dialogTitle, dialogContent, dialogActions];
}, [children, props, theme.colors.onSurfaceVariant]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

props is a fresh rest object every render, so this useMemo never hits. Drop it, or list the individual props.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed!

@@ -5206,10 +5206,10 @@
"Dialog/Dialog": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run yarn docs generate - componentDocs6x.json and the .mdx pages are generated, and the JSDoc you added to DialogActions, DialogContent, DialogScrollArea and DialogTitle hasn't reached either. The hand-typed ### children (depracated…) heading will be overwritten.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

Comment thread src/components/Dialog/Dialog.tsx Outdated
{actions.map(
({ label, onPress: onActionPress, ...buttonProps }, index) => (
<Button
key={index}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key={index} on a consumer-supplied array - the JSDoc note "Keep their order stable between renders" is working around it. Key on label.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring as actions changed a type.

Comment thread src/components/Dialog/Dialog.tsx Outdated
const dialogIcon = icon ? (
<DialogIcon icon={icon} key="dialogIcon" />
) : null;
const dialogTitle = title ? <DialogTitle>{title}</DialogTitle> : null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new API knows the title, but nothing gives the dialog an accessible name - Modal sets aria-modal with no label. Worth wiring title through while the API is being designed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added accessibilityLabel as a Dialog prop to enable passing custom aria-label. I also added a condition where if title is a string then it should be taken as aria-label by default which is what guidelines suggest. We could also drop it at only pass a provided accessibilityLabel. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants