Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dialog display #19

Merged
merged 3 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/components/feedback/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react'
import { render, screen, userClick } from 'testingUtils'
import { Dialog, getTheme } from '../../index'
import { Button } from '@mui/material'

const title = 'Dialog component'
const text =
'Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.'

const theme = getTheme()
const defaultFont = theme.typography.defaultFont

describe('DialogDisplay', () => {
test('X button is shown by default', () => {
render(<Dialog id="dialog" open={true} />)
expect(screen.getByLabelText('Close')).toBeInTheDocument()
})

test('X button is not rendered if showX = false', () => {
render(<Dialog id="dialog" open={true} showX={false} />)
expect(screen.queryByLabelText('Close')).not.toBeInTheDocument()
})

test('font family of the title is taken from default font', () => {
render(<Dialog id="dialog" open={true} title={title} />)
expect(screen.getByText(title)).toHaveStyle(`font-family: ${defaultFont.fontFamily}`)
})

test('font family and font size of the dialog content are taken from the default font', () => {
render(<Dialog id="dialog" open={true} content={text} />)
const content = screen.getByText(text)

expect(content).toHaveStyle(`font-family: ${defaultFont.fontFamily}`)
expect(content).toHaveStyle(`font-size: ${defaultFont.fontSize}px`)
})

test('renders DialogText component when the text prop is provided', () => {
render(<Dialog id="dialog" open={true} textContent={text} />)
expect(screen.getByText(text)).toHaveClass('MuiDialogContentText-root')
})

test('displays dividers when dividers = true', () => {
render(<Dialog id="dialog" open={true} content={text} dividers />)
expect(screen.getByText(text)).toHaveClass('MuiDialogContent-dividers')
})

test('has transparent backdrop when transparentBackdrop = true', () => {
render(<Dialog id="dialog" open={true} content={text} transparentBackdrop={true} />)
const backdrop = screen.getByRole('dialog').parentElement.parentElement.childNodes[0]

expect(backdrop).toHaveStyle('background-color: transparent')
})

test('calls onClose function when clicking the X button', () => {
const mockOnClose = jest.fn(() => {})

render(<Dialog id="dialog" open={true} content={text} onClose={mockOnClose} />)
userClick(screen.getByLabelText('Close'))

expect(mockOnClose).toHaveBeenCalled()
})

test('doesn\'t close on backdrop click when disableBackdropClick = true', () => {
const mockOnClose = jest.fn(() => {})

render(<Dialog id="dialog" open={true} content={text} onClose={mockOnClose} disableBackdropClick={true} />)
userClick(screen.getByRole('dialog').parentElement)

expect(mockOnClose).not.toHaveBeenCalled()
})

test('default value for overflowY is auto', () => {
render(<Dialog id="dialog" open={true} content={text} />)
expect(screen.getByText(text)).toHaveStyle('overflow-y: auto')
})

test('DialogTitle can be overridden through the titleProps', () => {
render(<Dialog id="dialog" open={true} title={title} content={text} titleProps={{ sx: { backgroundColor: 'red' } }} />)
expect(screen.getByText(title)).toHaveStyle('background-color: red')
})

test('DialogContent can be overridden through the contentProps', () => {
render(<Dialog id="dialog" open={true} content={text} contentProps={{ sx: { backgroundColor: 'red' } }} />)
expect(screen.getByText(text)).toHaveStyle('background-color: red')
})

test('DialogContentText can be overridden through the textContentProps', () => {
render(<Dialog id="dialog" open={true} textContent={text} textContentProps={{ sx: { backgroundColor: 'red' } }} />)
expect(screen.getByText(text)).toHaveStyle('background-color: red')
})

test('DialogActions can be overridden through the actionsProps', () => {
const buttonText = 'OK'
render(
<Dialog
id="dialog"
open={true}
textContent={text}
actions={<Button>{buttonText}</Button>}
actionsProps={{ sx: { backgroundColor: 'red' } }}
/>
)
expect(screen.getByText(buttonText).parentElement).toHaveStyle('background-color: red')
})

test('Close button can be overridden through the closeButtonProps', () => {
render(<Dialog id="dialog" open={true} content={text} closeButtonProps={{ sx: { backgroundColor: 'red' } }} />)
expect(screen.getByLabelText('Close')).toHaveStyle('background-color: red')
})
})
201 changes: 201 additions & 0 deletions src/components/feedback/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import React, { MouseEventHandler, useCallback } from 'react'
import PropTypes from 'prop-types'
import MuiDialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContentText from '@mui/material/DialogContentText'
import useMediaQuery from '@mui/material/useMediaQuery'
import { useTheme } from '@mui/material/styles'
import CloseIcon from '@mui/icons-material/Close'
import { TransparentBackdrop, DialogContent, DialogTitle, justifyRight } from './DialogStyles'
import { Button, IconButton } from '../../index'
import { DialogCloseReason, DialogProps } from './types'

/**
* Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.
*
* A Dialog is a type of modal window that appears in front of app content to provide critical information or ask for a decision. Dialogs disable all app functionality when they appear, and remain on screen until confirmed, dismissed, or a required action has been taken.
*
* Dialogs are purposefully interruptive, so they should be used sparingly.
*/
const Dialog: React.FC<DialogProps> = ({
id,
title,
titleProps,
content,
contentProps,
textContent,
textContentProps,
actions,
actionsProps,
defaultActions,
defaultActionsProps = { textDialogYes: 'Yes', textDialogNo: 'No' },
onYes,
onClose,
closeButtonProps,
disableBackdropClick = false,
fullScreen,
showX = true,
transparentBackdrop,
dividers = false,
maxWidth = 'xl',
open = false,
...rest
}) => {
const dialogTitleId = `${id}-dialog-display-title`

const theme = useTheme()
const smallScreen = useMediaQuery(theme.breakpoints.down('md'))

const handleClose = useCallback(
(event: React.MouseEvent, reason: DialogCloseReason) => {
if (disableBackdropClick && reason === 'backdropClick') return

onClose(event, reason ?? 'closeActionClick')
},
[disableBackdropClick, onClose]
)

const { textDialogYes, textDialogNo, ...buttonProps } = defaultActionsProps
const defaultActionsComp = (
<>
<Button right {...buttonProps} onClick={onYes}>
{textDialogYes}
</Button>
<Button right {...buttonProps} onClick={handleClose as MouseEventHandler}>
{textDialogNo}
</Button>
</>
)
return (
<MuiDialog
onClose={handleClose}
aria-labelledby={dialogTitleId}
fullScreen={fullScreen || smallScreen}
BackdropComponent={transparentBackdrop && TransparentBackdrop}
maxWidth={maxWidth}
open={open}
{...rest}
>
<DialogTitle id={dialogTitleId} {...titleProps}>
{title}
{showX && (
<IconButton
sx={justifyRight}
color="default"
variant="text"
size="small"
aria-label="Close"
onClick={handleClose as MouseEventHandler}
{...closeButtonProps}
>
<CloseIcon />
</IconButton>
)}
</DialogTitle>
<DialogContent dividers={dividers} {...contentProps}>
{textContent && <DialogContentText {...textContentProps}>{textContent}</DialogContentText>}
{content}
</DialogContent>
<DialogActions {...actionsProps}>{defaultActions ? defaultActionsComp : actions}</DialogActions>
</MuiDialog>
)
}

Dialog.propTypes = {
/**
* Identifier of the dialog.
*/
id: PropTypes.string.isRequired,
/**
* @default false
* If true, the component is shown.
*/
open: PropTypes.bool.isRequired,
/**
* Callback fired when the component requests to be closed.
*/
onClose: PropTypes.func,
/**
* Title of the dialog.
*/
title: PropTypes.node,
/**
* Content of the dialog.
*/
content: PropTypes.node,
/**
* Text content of the dialog. If received, it will be wrapped by the MUI DialogContentText component.
*/
textContent: PropTypes.node,
/**
* The actions provided below the dialog.
*/
actions: PropTypes.node,
/**
* @default false
* If true, clicking the backdrop will not fire the onClose callback.
*/
disableBackdropClick: PropTypes.bool,
/**
* Props sent to the DialogTitle component.
*/
titleProps: PropTypes.object,
/**
* Props sent to the DialogContent component.
*/
contentProps: PropTypes.object,
/**
* Props sent to the DialogContentText component.
*/
textContentProps: PropTypes.object,
/**
* Props sent to the DialogActions component.
*/
actionsProps: PropTypes.object,
/**
* @default false
* If `true`, it will render `Yes` and `No` button actions by default.
*
* The following properties would be required: @onYes and @onClose properties.
*
* The two default buttons can be configured using @defaultActionsProps property.
*/
defaultActions: PropTypes.bool,
/**
* Props sent to the default action buttons.
*/
defaultActionsProps: PropTypes.object,
/**
* Callback fired when a "click" event is detected.
*/
onYes: PropTypes.func,
/**
* Props sent to the close button.
*/
closeButtonProps: PropTypes.object,
/**
* If true, the backdrop will be transparent.
*/
transparentBackdrop: PropTypes.bool,
/**
* If true, the dialog is full-screen.
*/
fullScreen: PropTypes.bool,
/**
* @default false
* If true, the close button is shown.
*/
showX: PropTypes.bool,
/**
* @default false
* Display dividers at the top and bottom of DialogContent.
*/
dividers: PropTypes.bool,
/**
* @default 'xl'
* Determine the max-width of the dialog. The dialog width grows with the size of the screen. Set to false to disable maxWidth.
*/
maxWidth: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false])
}

export default Dialog
23 changes: 23 additions & 0 deletions src/components/feedback/Dialog/DialogStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import MuiDialogContent from '@mui/material/DialogContent'
import MuiDialogTitle from '@mui/material/DialogTitle'
import Backdrop from '@mui/material/Backdrop'
import { styled } from '@mui/material/styles'

export const justifyRight = {
position: 'absolute',
top: 16,
right: 14
}

export const DialogContent = styled(MuiDialogContent)(({ theme }) => ({
fontFamily: theme.typography.defaultFont.fontFamily,
fontSize: theme.typography.defaultFont.fontSize
}))

export const DialogTitle = styled(MuiDialogTitle)(({ theme }) => ({
fontFamily: theme.typography.defaultFont.fontFamily
}))

export const TransparentBackdrop = styled(Backdrop)(() => ({
backgroundColor: 'transparent'
}))
3 changes: 3 additions & 0 deletions src/components/feedback/Dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Dialog from './Dialog'
export default Dialog
export * from './types'
Loading