Skip to content

Commit

Permalink
change: [M3-8916] - Implement Dialogs/Drawers loading patterns (#11273)
Browse files Browse the repository at this point in the history
* initial commit: drawer + dialog

* post rebase fix

* fix units

* consolidate confirm dialogs

* closeAfterTransition={false}

* Cleanup

* Added changeset: Implement Dialogs/Drawers loading patterns

* Improved coverage

* better approach for content dueing closing transition

* revert test changes since not needed anymore
  • Loading branch information
abailly-akamai authored Nov 21, 2024
1 parent dba4bfa commit c81667d
Show file tree
Hide file tree
Showing 25 changed files with 474 additions and 272 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11273-changed-1731988661274.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Implement Dialogs/Drawers loading patterns ([#11273](https://github.com/linode/manager/pull/11273))
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import { styled } from '@mui/material/styles';
import { Stack } from '@linode/ui';
import * as React from 'react';

import { DialogTitle } from 'src/components/DialogTitle/DialogTitle';
import { Dialog } from 'src/components/Dialog/Dialog';

import type { DialogProps } from '@mui/material/Dialog';
import type { DialogProps } from 'src/components/Dialog/Dialog';

export interface ConfirmationDialogProps extends DialogProps {
actions?: ((props: any) => JSX.Element) | JSX.Element;
error?: JSX.Element | string;
onClose: () => void;
onExited?: () => void;
title: string;
/**
* The actions to be displayed in the dialog.
*/
actions?: ((props: DialogProps) => JSX.Element) | JSX.Element;
}

/**
Expand All @@ -27,75 +22,21 @@ export interface ConfirmationDialogProps extends DialogProps {
*
*/
export const ConfirmationDialog = (props: ConfirmationDialogProps) => {
const {
actions,
children,
error,
onClose,
onExited,
title,
...dialogProps
} = props;
const { actions, children, ...dialogProps } = props;

return (
<StyledDialog
{...dialogProps}
TransitionProps={{
...dialogProps.TransitionProps,
onExited,
}}
onClose={(_, reason) => {
if (reason !== 'backdropClick') {
onClose();
}
}}
PaperProps={{ role: undefined }}
data-qa-dialog
data-qa-drawer
data-testid="drawer"
role="dialog"
>
<DialogTitle onClose={onClose} title={title} />
<StyledDialogContent data-qa-dialog-content>
{children}
{error && <StyledErrorText>{error}</StyledErrorText>}
</StyledDialogContent>
<StyledDialogActions>
<Dialog {...dialogProps} PaperProps={{ role: undefined }}>
{children}
<Stack
direction="row"
justifyContent="flex-end"
spacing={2}
sx={{ mt: 4 }}
>
{actions && typeof actions === 'function'
? actions(dialogProps)
: actions}
</StyledDialogActions>
</StyledDialog>
</Stack>
</Dialog>
);
};

const StyledDialog = styled(Dialog, {
label: 'StyledDialog',
})({
'& .MuiDialogTitle-root': {
marginBottom: '10px',
},
});

const StyledDialogActions = styled(DialogActions, {
label: 'StyledDialogActions',
})({
'& button': {
marginBottom: 0,
},
justifyContent: 'flex-end',
});

const StyledDialogContent = styled(DialogContent, {
label: 'StyledDialogContent',
})({
display: 'flex',
flexDirection: 'column',
});

const StyledErrorText = styled(DialogContentText, {
label: 'StyledErrorText',
})(({ theme }) => ({
color: theme.palette.error.dark,
marginTop: theme.spacing(2),
}));
65 changes: 60 additions & 5 deletions packages/manager/src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Button } from '@linode/ui';
import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/preview-api';
import React from 'react';

import { Typography } from '../Typography';
import { Dialog } from './Dialog';

import type { Meta, StoryObj } from '@storybook/react';
Expand Down Expand Up @@ -35,18 +38,17 @@ const meta: Meta<typeof Dialog> = {
title: { description: 'Title that appears in the heading of the dialog.' },
},
args: {
disableAutoFocus: true,
disableEnforceFocus: true,
disablePortal: true,
disableScrollLock: true,
fullHeight: false,
fullWidth: false,
maxWidth: 'md',
onClose: action('onClose'),
open: true,
style: { position: 'unset' },
title: 'This is a Dialog',
titleBottomBorder: false,
disableAutoFocus: true,
disableEnforceFocus: true,
disablePortal: true,
disableScrollLock: true,
},
component: Dialog,
title: 'Components/Dialog',
Expand All @@ -63,3 +65,56 @@ export const Default: Story = {
</Dialog>
),
};

export const Fetching: Story = {
args: {
isFetching: true,
},
render: (args) => {
const DrawerExampleWrapper = () => {
const [{ isFetching, open }, updateArgs] = useArgs();

React.useEffect(() => {
if (open) {
setTimeout(() => {
updateArgs({ isFetching: false, onClose: action('onClose') });
}, 1500);
} else {
setTimeout(() => {
updateArgs({ isFetching: true, onClose: action('onClose') });
}, 300);
}
}, [isFetching, open, updateArgs]);

return (
<>
<Button
buttonType="primary"
onClick={() => updateArgs({ open: true })}
sx={{ m: 4 }}
>
Click to open Dialog
</Button>
<Dialog
{...args}
isFetching={isFetching}
onClose={() => updateArgs({ open: false })}
open={open}
>
<Typography sx={{ mb: 2 }}>
A most sober dialog, with a title and a description.
</Typography>
<Button
buttonType="primary"
onClick={() => updateArgs({ open: false })}
>
Close This Thing
</Button>
</Dialog>
</>
);
};

return DrawerExampleWrapper();
},
};
83 changes: 60 additions & 23 deletions packages/manager/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Notice, omittedProps } from '@linode/ui';
import { Box, CircleProgress, Notice, omittedProps } from '@linode/ui';
import _Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import { styled, useTheme } from '@mui/material/styles';
Expand All @@ -10,12 +10,32 @@ import { convertForAria } from 'src/utilities/stringUtils';
import type { DialogProps as _DialogProps } from '@mui/material/Dialog';

export interface DialogProps extends _DialogProps {
/**
* Additional CSS to be applied to the Dialog.
*/
className?: string;
/**
* Error that will be shown in the dialog.
*/
error?: string;
/**
* Let the Dialog take up the entire height of the viewport.
*/
fullHeight?: boolean;
/**
* Whether the drawer is fetching the entity's data.
*
* If true, the drawer will feature a loading spinner for its content.
*/
isFetching?: boolean;
/**
* Subtitle that will be shown in the dialog.
*/
subtitle?: string;
/**
* Title that will be shown in the dialog.
*/
title: string;
titleBottomBorder?: boolean;
}

/**
Expand All @@ -31,7 +51,7 @@ export interface DialogProps extends _DialogProps {
* - **Confirmation**
* - Users must confirm a choice
* - **Deletion**
* - The user must confirm the deleteion of an entity
* - The user must confirm the deletion of an entity
* - Can require user to type the entity name to confirm deletion
*
* > Clicking off of the modal will not close it.
Expand All @@ -47,26 +67,44 @@ export const Dialog = React.forwardRef(
error,
fullHeight,
fullWidth,
isFetching,
maxWidth = 'md',
onClose,
open,
subtitle,
title,
titleBottomBorder,
...rest
} = props;

const titleID = convertForAria(title);

// Store the last valid children and title in refs
// This is to prevent flashes of content during the drawer's closing transition,
// and its content becomes potentially undefined
const lastChildrenRef = React.useRef(children);
const lastTitleRef = React.useRef(title);
// Update refs when the drawer is open and content is matched
if (open && children) {
lastChildrenRef.current = children;
lastTitleRef.current = title;
}

return (
<StyledDialog
onClose={(_, reason) => {
if (onClose && reason !== 'backdropClick') {
onClose({}, 'escapeKeyDown');
}
}}
aria-labelledby={titleID}
closeAfterTransition={false}
data-qa-dialog
data-qa-drawer
data-testid="drawer"
fullHeight={fullHeight}
fullWidth={fullWidth}
maxWidth={(fullWidth && maxWidth) ?? undefined}
onClose={onClose}
open={open}
ref={ref}
role="dialog"
title={title}
Expand All @@ -79,20 +117,28 @@ export const Dialog = React.forwardRef(
>
<DialogTitle
id={titleID}
onClose={() => onClose && onClose({}, 'backdropClick')}
isFetching={isFetching}
onClose={() => onClose?.({}, 'escapeKeyDown')}
subtitle={subtitle}
title={title}
title={lastTitleRef.current}
/>
{titleBottomBorder && <StyledHr />}
<DialogContent
sx={{
overflowX: 'hidden',
paddingBottom: theme.spacing(3),
}}
className={className}
>
{error && <Notice text={error} variant="error" />}
{children}
{isFetching ? (
<Box display="flex" justifyContent="center" my={4}>
<CircleProgress size="md" />
</Box>
) : (
<>
{error && <Notice text={error} variant="error" />}
{lastChildrenRef.current}
</>
)}
</DialogContent>
</Box>
</StyledDialog>
Expand All @@ -106,19 +152,10 @@ const StyledDialog = styled(_Dialog, {
'& .MuiDialog-paper': {
height: props.fullHeight ? '100vh' : undefined,
maxHeight: '100%',
minWidth: '500px',
padding: 0,
[theme.breakpoints.down('md')]: {
minWidth: '380px',
},
},
'& .MuiDialogActions-root': {
display: 'flex',
justifyContent: 'flex-end',
marginTop: theme.spacing(2),
},
}));

const StyledHr = styled('hr', { label: 'StyledHr' })(({ theme }) => ({
backgroundColor: theme.tokens.color.Neutrals[20],
border: 'none',
height: 1,
margin: '-2em 8px 0px 8px',
width: '100%',
}));
7 changes: 4 additions & 3 deletions packages/manager/src/components/DialogTitle/DialogTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { SxProps, Theme } from '@mui/material';
interface DialogTitleProps {
className?: string;
id?: string;
isFetching?: boolean;
onClose?: () => void;
subtitle?: string;
sx?: SxProps<Theme>;
Expand All @@ -17,7 +18,7 @@ interface DialogTitleProps {

const DialogTitle = (props: DialogTitleProps) => {
const ref = React.useRef<HTMLDivElement>(null);
const { className, id, onClose, subtitle, sx, title } = props;
const { className, id, isFetching, onClose, subtitle, sx, title } = props;

React.useEffect(() => {
if (ref.current === null) {
Expand Down Expand Up @@ -48,8 +49,8 @@ const DialogTitle = (props: DialogTitleProps) => {
data-qa-dialog-title={title}
data-qa-drawer-title={title}
>
{title}
{onClose != null && (
<Box component="span">{!isFetching && title}</Box>
{onClose !== null && (
<IconButton
sx={{
right: '-12px',
Expand Down
Loading

0 comments on commit c81667d

Please sign in to comment.