From a74eec337e6ca8ddf223cbf885943d2bc9f1272f Mon Sep 17 00:00:00 2001 From: Alexandra Capatina <46926675+alexandra-c@users.noreply.github.com> Date: Thu, 18 May 2023 13:03:12 +0300 Subject: [PATCH 1/2] autocomplete stories update --- .../Autocomplete/Autocomplete.stories.tsx | 111 +++++++++++++++++- .../Autocomplete/CustomOptionPreview.tsx | 29 +++++ src/stories/inputs/Autocomplete/_mocks.ts | 10 ++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 src/stories/inputs/Autocomplete/CustomOptionPreview.tsx diff --git a/src/stories/inputs/Autocomplete/Autocomplete.stories.tsx b/src/stories/inputs/Autocomplete/Autocomplete.stories.tsx index bf356446..aa6a3022 100644 --- a/src/stories/inputs/Autocomplete/Autocomplete.stories.tsx +++ b/src/stories/inputs/Autocomplete/Autocomplete.stories.tsx @@ -9,9 +9,10 @@ import { DefaultPreview } from './DefaultPreview' import { OptionTypesPreview } from './OptionTypesPreview' import { MultipleSelectionPreview } from './MultipleSelectionPreview' import { CheckboxesPreview } from './CheckboxesPreview' -import { loadFilteredOptions, options } from './_mocks' +import { customOptions, loadFilteredOptions, options } from './_mocks' import { StylingPreview } from './StylingPreview' import { RequiredPreview } from './RequiredPreview' +import { CustomOptionPreview } from './CustomOptionPreview' const meta: Meta = { title: 'Components/Inputs/Autocomplete', @@ -28,6 +29,21 @@ type Story = StoryObj * The value must be chosen from a predefined set of allowed values. */ export const Default: Story = { + parameters: { + docs: { + source: { + code: ` + + `, + format: true + } + } + }, args: { label: 'Basic Autocomplete', options, @@ -82,6 +98,65 @@ export const OptionTypes: Story = { render: args => } +/** + * To customize the rendering of the object, the user can use the `renderOption` function property. See its definition + * + * ```javascript + renderOption?: ( + props: React.HTMLAttributes, // @param {object} props The props to apply on the li element. + option: T, // @param {T} option The option to render. + state: AutocompleteRenderOptionState, // @param {object} state The state of the component. + ) => React.ReactNode; + ``` +* Assuming the options array looks as follows, check-out the custom rendering function presented in the next example. +``` +const customOptions = [ + { id: 1, name: 'Cat', icon: Pets, description: 'A cat is the best pet' }, + { id: 2, name: 'Dog', icon: EmojiNature, description: 'A dos is a man best friend' }, + { id: 3, name: 'Turtle', icon: BugReport, description: 'Turtles are slow' }, + { id: 4, name: 'Horse', icon: BedroomBaby , description: 'Horses are elegant.'} +] +``` +*/ +export const OptionRendering: Story = { + parameters: { + controls: { hideNoControlsWarning: true }, + docs: { + source: { + code: ` + ( +
  • + + + {option.name} + + {option.description} + + +
  • + )} + /> + `, + format: true + } + } + }, + args: { + label: 'Custom Options', + options: customOptions, + onChange: null, + onClose: null, + onInputChange: null, + onOpen: null + }, + render: args => +} + /** * By setting the `creatable` property to `true`, the Autocomplete is free solo, meaning that the user input is not bound to provided options and can add * his own values. @@ -139,6 +214,23 @@ export const MultipleSelection: Story = { * Multiple selection can also be done using checkboxes by setting the `withCheckboxes` property to `true`. */ export const Checkboxes: Story = { + parameters: { + docs: { + source: { + code: ` + + `, + format: true + } + } + }, args: { label: 'Multiple Selection', options, @@ -158,6 +250,21 @@ export const Checkboxes: Story = { * During the `loading` state, it displays a progress state as long as the network request is pending. */ export const LazyLoading: Story = { + parameters: { + docs: { + source: { + code: ` + + `, + format: true + } + } + }, args: { label: 'Lazy Loading', loadOptions: loadFilteredOptions, @@ -197,7 +304,7 @@ export const Styling: Story = { } /** - * Various configurations using `TextField` inherited props. Check-out the code bellow. + * Various configurations using `TextField` inherited props. Check-out the code bellow. */ export const TextFieldInheritance: Story = { parameters: { diff --git a/src/stories/inputs/Autocomplete/CustomOptionPreview.tsx b/src/stories/inputs/Autocomplete/CustomOptionPreview.tsx new file mode 100644 index 00000000..d86f89d5 --- /dev/null +++ b/src/stories/inputs/Autocomplete/CustomOptionPreview.tsx @@ -0,0 +1,29 @@ +// Copyright (c) TotalSoft. +// This source code is licensed under the MIT license. + +import React, { useState } from 'react' +import { Autocomplete, Typography } from 'components' +import Box from '@mui/material/Box' + +export const CustomOptionPreview = (props: any) => { + const [value, setValue] = useState() + + return ( + ( +
  • + + + {option.name} + + {option.description} + + +
  • + )} + /> + ) +} diff --git a/src/stories/inputs/Autocomplete/_mocks.ts b/src/stories/inputs/Autocomplete/_mocks.ts index 68739b6f..37503a0d 100644 --- a/src/stories/inputs/Autocomplete/_mocks.ts +++ b/src/stories/inputs/Autocomplete/_mocks.ts @@ -1,3 +1,7 @@ +import Pets from '@mui/icons-material/Pets' +import EmojiNature from '@mui/icons-material/EmojiNature' +import BugReport from '@mui/icons-material/BugReport' +import BedroomBaby from '@mui/icons-material/BedroomBaby' import { includes } from 'ramda' export const options = [ @@ -6,6 +10,12 @@ export const options = [ { id: 3, name: 'Turtle' }, { id: 4, name: 'Horse' } ] +export const customOptions = [ + { id: 1, name: 'Cat', icon: Pets, description: 'A cat is the best pet' }, + { id: 2, name: 'Dog', icon: EmojiNature, description: 'A dos is a man best friend' }, + { id: 3, name: 'Turtle', icon: BugReport, description: 'Turtles are slow' }, + { id: 4, name: 'Horse', icon: BedroomBaby , description: 'Horses are elegant.'} +] export const primitiveOptions = ['first option', 'second option', 'third option'] export const numericOptions = [{ period: 1 }, { period: 2 }, { period: 3 }] From 6091760b0cc20322e782a627a465ab9a016a5d08 Mon Sep 17 00:00:00 2001 From: Alexandra Capatina <46926675+alexandra-c@users.noreply.github.com> Date: Thu, 18 May 2023 13:54:33 +0300 Subject: [PATCH 2/2] rewrite dialog --- .../feedback/Dialog/Dialog.test.tsx | 110 ++ src/components/feedback/Dialog/Dialog.tsx | 201 +++ .../feedback/Dialog/DialogStyles.ts | 23 + src/components/feedback/Dialog/index.ts | 3 + src/components/feedback/Dialog/types.ts | 107 ++ src/components/index.ts | 3 + .../feedback/Dialog/ActionsPreview.tsx | 45 + .../feedback/Dialog/DefaultPreview.tsx | 16 + .../feedback/Dialog/Dialog.stories.tsx | 167 +++ src/stories/feedback/Dialog/_mocks.tsx | 19 + yarn.lock | 1309 +++++++++-------- 11 files changed, 1354 insertions(+), 649 deletions(-) create mode 100644 src/components/feedback/Dialog/Dialog.test.tsx create mode 100644 src/components/feedback/Dialog/Dialog.tsx create mode 100644 src/components/feedback/Dialog/DialogStyles.ts create mode 100644 src/components/feedback/Dialog/index.ts create mode 100644 src/components/feedback/Dialog/types.ts create mode 100644 src/stories/feedback/Dialog/ActionsPreview.tsx create mode 100644 src/stories/feedback/Dialog/DefaultPreview.tsx create mode 100644 src/stories/feedback/Dialog/Dialog.stories.tsx create mode 100644 src/stories/feedback/Dialog/_mocks.tsx diff --git a/src/components/feedback/Dialog/Dialog.test.tsx b/src/components/feedback/Dialog/Dialog.test.tsx new file mode 100644 index 00000000..0aaefa9a --- /dev/null +++ b/src/components/feedback/Dialog/Dialog.test.tsx @@ -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() + expect(screen.getByLabelText('Close')).toBeInTheDocument() + }) + + test('X button is not rendered if showX = false', () => { + render() + expect(screen.queryByLabelText('Close')).not.toBeInTheDocument() + }) + + test('font family of the title is taken from default font', () => { + render() + 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() + 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() + expect(screen.getByText(text)).toHaveClass('MuiDialogContentText-root') + }) + + test('displays dividers when dividers = true', () => { + render() + expect(screen.getByText(text)).toHaveClass('MuiDialogContent-dividers') + }) + + test('has transparent backdrop when transparentBackdrop = true', () => { + render() + 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() + userClick(screen.getByLabelText('Close')) + + expect(mockOnClose).toHaveBeenCalled() + }) + + test('doesn\'t close on backdrop click when disableBackdropClick = true', () => { + const mockOnClose = jest.fn(() => {}) + + render() + userClick(screen.getByRole('dialog').parentElement) + + expect(mockOnClose).not.toHaveBeenCalled() + }) + + test('default value for overflowY is auto', () => { + render() + expect(screen.getByText(text)).toHaveStyle('overflow-y: auto') + }) + + test('DialogTitle can be overridden through the titleProps', () => { + render() + expect(screen.getByText(title)).toHaveStyle('background-color: red') + }) + + test('DialogContent can be overridden through the contentProps', () => { + render() + expect(screen.getByText(text)).toHaveStyle('background-color: red') + }) + + test('DialogContentText can be overridden through the textContentProps', () => { + render() + expect(screen.getByText(text)).toHaveStyle('background-color: red') + }) + + test('DialogActions can be overridden through the actionsProps', () => { + const buttonText = 'OK' + render( + {buttonText}} + actionsProps={{ sx: { backgroundColor: 'red' } }} + /> + ) + expect(screen.getByText(buttonText).parentElement).toHaveStyle('background-color: red') + }) + + test('Close button can be overridden through the closeButtonProps', () => { + render() + expect(screen.getByLabelText('Close')).toHaveStyle('background-color: red') + }) +}) diff --git a/src/components/feedback/Dialog/Dialog.tsx b/src/components/feedback/Dialog/Dialog.tsx new file mode 100644 index 00000000..3516b6a5 --- /dev/null +++ b/src/components/feedback/Dialog/Dialog.tsx @@ -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 = ({ + 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 = ( + <> + + + + ) + return ( + + + {title} + {showX && ( + + + + )} + + + {textContent && {textContent}} + {content} + + {defaultActions ? defaultActionsComp : actions} + + ) +} + +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 diff --git a/src/components/feedback/Dialog/DialogStyles.ts b/src/components/feedback/Dialog/DialogStyles.ts new file mode 100644 index 00000000..97897ed6 --- /dev/null +++ b/src/components/feedback/Dialog/DialogStyles.ts @@ -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' +})) diff --git a/src/components/feedback/Dialog/index.ts b/src/components/feedback/Dialog/index.ts new file mode 100644 index 00000000..f9439ba7 --- /dev/null +++ b/src/components/feedback/Dialog/index.ts @@ -0,0 +1,3 @@ +import Dialog from './Dialog' +export default Dialog +export * from './types' diff --git a/src/components/feedback/Dialog/types.ts b/src/components/feedback/Dialog/types.ts new file mode 100644 index 00000000..8a0e747c --- /dev/null +++ b/src/components/feedback/Dialog/types.ts @@ -0,0 +1,107 @@ +import { + DialogProps as MuiDialogProps, + DialogTitleProps as MuiDialogTitleProps, + DialogContentProps as MuiDialogContentProps, + DialogContentTextProps as MuiDialogContentTextProps, + DialogActionsProps as MuiDialogActionsProps +} from '@mui/material' +import { ButtonProps, IconButtonProps } from '../../index' + +export type DialogCloseReason = 'backdropClick' | 'escapeKeyDown' | 'closeActionClick' + +export interface DefaultActionsProps extends ButtonProps { + /** + * @default 'Yes' + * Text content of the first (left) action. + */ + textDialogYes?: string + /** + * @default 'No' + * Text content of the second (right) action. + */ + textDialogNo?: string +} + +export interface DialogProps extends Omit { + /** + * Callback fired when the component requests to be closed. + * The `reason` parameter can optionally be used to control the response to `onClose`. + * + * @param {React.MouseEvent} event The event source of the callback. + * @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"` or 'closeActionClick'. + */ + onClose?: (event: React.MouseEvent, reason: DialogCloseReason) => void + /** + * Props sent to the close button. + */ + closeButtonProps?: IconButtonProps + /** + * Title of the dialog. + */ + title?: React.ReactNode + /** + * Props sent to the DialogTitle component. + */ + titleProps?: MuiDialogTitleProps + /** + * Props sent to the DialogContent component. + */ + contentProps?: MuiDialogContentProps + /** + * Content of the dialog. + */ + content?: React.ReactNode + /** + * Text content of the dialog. If received, it will be wrapped by the MUI DialogContentText component. + */ + textContent?: React.ReactNode + /** + * Props sent to the DialogContentText component. + */ + textContentProps?: MuiDialogContentTextProps + /** + * The actions provided below the dialog. + */ + actions?: React.ReactNode + /** + * Props sent to the DialogActions component. + */ + actionsProps?: MuiDialogActionsProps + /** + * @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?: boolean + /** + * Specific button configurations that will be applied to the default `Yes`/`No` buttons. + */ + defaultActionsProps?: DefaultActionsProps + /** + * Callback fired when a "click" event is detected. + */ + onYes?: () => void + /** + * @default false + * If `true`, clicking the backdrop will not fire the `onClose` callback. + */ + disableBackdropClick?: boolean + /** + * @default false + * If `true`, the backdrop (the outer background) will be transparent. + */ + transparentBackdrop?: boolean + /** + * @default false + * If `true`, the close button is shown. + */ + showX?: boolean + /** + * @default false + * Display dividers at the top and bottom of DialogContent. + */ + dividers?: boolean +} diff --git a/src/components/index.ts b/src/components/index.ts index 5f691777..607a5531 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -45,5 +45,8 @@ export * from './surfaces/StatsCard' export { default as Accordion } from './surfaces/Accordion' export * from './surfaces/Accordion' +export { default as Dialog } from './feedback/Dialog' +export * from './feedback/Dialog' + export { default as getTheme } from './themes/index' export * from './themes/index' diff --git a/src/stories/feedback/Dialog/ActionsPreview.tsx b/src/stories/feedback/Dialog/ActionsPreview.tsx new file mode 100644 index 00000000..c201ed93 --- /dev/null +++ b/src/stories/feedback/Dialog/ActionsPreview.tsx @@ -0,0 +1,45 @@ +import { Grid } from '@mui/material' +import { Button, Dialog, TextField } from 'components' +import React, { useCallback, useState } from 'react' + +const ActionsPreview = ({ button, ...props }: any) => { + const [open, setOpen] = useState(false) + const toggle = useCallback(() => setOpen(current => !current), []) + + return ( + <> + + + + } + /> + + ) +} + +export default ActionsPreview diff --git a/src/stories/feedback/Dialog/DefaultPreview.tsx b/src/stories/feedback/Dialog/DefaultPreview.tsx new file mode 100644 index 00000000..7f690b24 --- /dev/null +++ b/src/stories/feedback/Dialog/DefaultPreview.tsx @@ -0,0 +1,16 @@ +import { Button, Dialog } from 'components' +import React, { useCallback, useState } from 'react' + +const DefaultPreview = ({ button, ...props }: any) => { + const [open, setOpen] = useState(false) + const toggle = useCallback(() => setOpen(current => !current), []) + + return ( + <> + + + + } + /> + `, + format: true + } + } + }, + args: { title, textContent: text }, + render: args => +} + +/** + * We can set default `Yes/No` actions by passing `defaultActions` property to `true.` + * + * If `true`, the following properties would be required: `onYes` and @onClose. + * + * The two default buttons can be configured using `defaultActionsProps` property. + */ +export const DefaultActions: Story = { + parameters: { + docs: { + source: { + code: ` +