diff --git a/.changeset/pretty-bears-nail.md b/.changeset/pretty-bears-nail.md new file mode 100644 index 00000000..832400aa --- /dev/null +++ b/.changeset/pretty-bears-nail.md @@ -0,0 +1,6 @@ +--- +'storybook-manifest': minor +'@project44-manifest/react': minor +--- + +Added dialog version 2 diff --git a/apps/storybook/.storybook/manager.js b/apps/storybook/.storybook/manager.js index a3dfaf47..9240bc59 100644 --- a/apps/storybook/.storybook/manager.js +++ b/apps/storybook/.storybook/manager.js @@ -1,4 +1,4 @@ -import { addons } from '@storybook/addons'; +import { addons } from '@storybook/manager-api'; import theme from './theme'; addons.setConfig({ theme }); diff --git a/packages/react/src/components/Dialogv2/dialogv2.styles.ts b/packages/react/src/components/Dialogv2/dialogv2.styles.ts new file mode 100644 index 00000000..56435101 --- /dev/null +++ b/packages/react/src/components/Dialogv2/dialogv2.styles.ts @@ -0,0 +1,37 @@ +import { pxToRem, styled } from '@project44-manifest/react-styles'; + +export const DialogV2Wrapper = styled('div', { + display: 'flex', + backgroundColor: '$background-primary', + borderRadius: '$small', + boxShadow: '$small', + boxSizing: 'border-box', + flexDirection: 'column', + outline: 0, + padding: '$large', + position: 'relative', + maxHeight: 'calc(100vh - 75px) !important', + overflowY: 'auto', + variants: { + size: { + small: { + width: pxToRem(480), + }, + medium: { + width: pxToRem(640), + }, + large: { + width: pxToRem(960), + }, + }, + edgeToEdge: { + noPadding: { + padding: '0px', + }, + }, + }, + defaultVariants: { + size: 'large', + edgeToEdge: '', + }, +}); diff --git a/packages/react/src/components/Dialogv2/dialogv2.tsx b/packages/react/src/components/Dialogv2/dialogv2.tsx new file mode 100644 index 00000000..f6e30c0a --- /dev/null +++ b/packages/react/src/components/Dialogv2/dialogv2.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { useDialog } from '@react-aria/dialog'; +import { mergeProps } from '@react-aria/utils'; +import { cx } from '@project44-manifest/react-styles'; +import type { ForwardRefComponent } from '@project44-manifest/react-types'; +import { useMergedRef } from '../../hooks'; +import { DialogProvider } from '../Dialog/Dialog.context'; +import { DialogElement } from '../Dialog/Dialog.types'; +import { DialogContent } from '../DialogContent'; +import { DialogFooter } from '../DialogFooter'; +import { DialogHeader } from '../DialogHeader'; +import { Modal } from '../Modal'; +import { ModalPosition } from '../Modal/Modal.types'; +import { DialogV2Wrapper } from './dialogv2.styles'; + +export enum DialogV2Size { + small = 'small', + medium = 'medium', + large = 'large', +} + +export interface DialogV2Props { + isOpen?: boolean; + headerProps: { + title: string; + onClose: () => void; + }; + body: React.ReactNode | string; + footer?: React.ReactNode | string; + isDismissable?: boolean; + isKeyboardDismissDisabled?: boolean; + size?: DialogV2Size; + edgeToEdge?: boolean; + position?: ModalPosition; +} + +export const DialogV2Impl = React.forwardRef((props, forwardedRef) => { + const { + as, + children, + className: classNameProp, + isDismissable, + headerProps, + body, + footer, + edgeToEdge, + size = DialogV2Size.medium, + ...other + } = props; + + const { title, onClose } = headerProps; + + const dialogRef = React.useRef(null); + const mergedRef = useMergedRef(dialogRef, forwardedRef); + + const { dialogProps, titleProps } = useDialog({ role: 'dialog' }, dialogRef); + + const context = React.useMemo( + () => ({ + isDismissable, + titleProps, + onClose, + }), + [isDismissable, onClose, titleProps], + ); + + const className = cx('manifest-dialog', classNameProp, { + [`manifest-dialog-${size}`]: size, + 'manifest-dialog-edgeToEdge': edgeToEdge, + }); + + return ( + + + {title} + {body} + {footer && {footer}} + + + ); +}) as ForwardRefComponent; + +DialogV2Impl.displayName = 'DialogImpl'; + +export const DialogV2 = React.forwardRef((props, forwardedRef) => { + const { + isDismissable = true, + isKeyboardDismissDisabled = false, + isOpen, + position, + ...other + } = props; + + const { onClose } = other.headerProps; + return ( + + + + ); +}) as ForwardRefComponent; + +DialogV2.displayName = 'DialogV2'; diff --git a/packages/react/src/components/Dialogv2/story/Dialogv2.stories.tsx b/packages/react/src/components/Dialogv2/story/Dialogv2.stories.tsx new file mode 100644 index 00000000..af105833 --- /dev/null +++ b/packages/react/src/components/Dialogv2/story/Dialogv2.stories.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { Meta, StoryFn } from '@storybook/react'; +import { Button } from '../../..'; +import { ModalPosition } from '../../Modal/Modal.types'; +import { DialogV2, DialogV2Props, DialogV2Size } from '../dialogv2'; + +const meta: Meta = { + component: DialogV2, + title: 'Components/Dialogv2', + argTypes: { + size: { + options: [DialogV2Size.small, DialogV2Size.medium, DialogV2Size.large], + control: { type: 'radio' }, + }, + position: { + options: [ModalPosition.top, ModalPosition.center], + control: { type: 'radio' }, + }, + }, +}; + +export default meta; + +export const Default: StoryFn = (args: DialogV2Props) => { + const [isOpen, setIsOpen] = React.useState(false); + + const handleClose = React.useCallback(() => void setIsOpen(false), []); + const handleOpen = React.useCallback(() => void setIsOpen(true), []); + + const props: DialogV2Props = { + isOpen, + headerProps: { + title: 'Dialog Title', + onClose: handleClose, + }, + body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum.`, + footer: ( + <> + + + + ), + }; + + return ( + <> + + + + ); +}; + +Default.args = { + isDismissable: true, + isKeyboardDismissDisabled: true, + edgeToEdge: false, + size: DialogV2Size.small, + position: ModalPosition.top, +}; diff --git a/packages/react/src/components/Dialogv2/tests/dialogV2.test.tsx b/packages/react/src/components/Dialogv2/tests/dialogV2.test.tsx new file mode 100644 index 00000000..8cb547a7 --- /dev/null +++ b/packages/react/src/components/Dialogv2/tests/dialogV2.test.tsx @@ -0,0 +1,68 @@ +import { act, render, screen } from '@testing-library/react'; +import { DialogV2, DialogV2Size } from '../dialogv2'; + +beforeAll(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.clearAllMocks(); + + act(() => { + jest.runAllTimers(); + }); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +it('should render dialogV2', () => { + const onClose = jest.fn(); + render( + , + ); + + expect(screen.getByTestId('dialogV2Wrapper')).toBeInTheDocument(); +}); + +it('should render footer', () => { + const onClose = jest.fn(); + render( + , + ); + + expect(screen.getByTestId('dialogV2Footer')).toBeInTheDocument(); +}); + +it('should react to size', () => { + const onClose = jest.fn(); + render( + , + ); + expect(screen.getByTestId('dialogV2Wrapper').className).toContain('manifest-dialog-small'); +}); diff --git a/packages/react/src/components/Modal/Modal.styles.ts b/packages/react/src/components/Modal/Modal.styles.ts index 4743dec7..c23102b7 100644 --- a/packages/react/src/components/Modal/Modal.styles.ts +++ b/packages/react/src/components/Modal/Modal.styles.ts @@ -46,6 +46,23 @@ export const StyledModalWrapper = styled('div', { visibility: 'visible', }, }, + position: { + top: { + top: '64px', + inlineSize: 'auto', + left: '0px', + right: '0px', + alignItems: 'flex-start', + }, + center: { + alignItems: 'center', + justifyContent: 'center', + }, + }, + }, + + defaultVariants: { + position: 'top', }, }); diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx index dc6bb4a4..f40ce3e2 100644 --- a/packages/react/src/components/Modal/Modal.tsx +++ b/packages/react/src/components/Modal/Modal.tsx @@ -6,7 +6,7 @@ import { useMergedRef } from '../../hooks'; import { mergeProps } from '../../utils'; import { Overlay } from '../Overlay'; import { StyledModal, StyledModalWrapper, StyledUnderlay } from './Modal.styles'; -import type { ModalElement, ModalProps } from './Modal.types'; +import { ModalElement, ModalPosition, ModalProps } from './Modal.types'; /** * Modal implementation; Need to initialize the overlay component before calling @@ -24,6 +24,7 @@ const ModalImpl = React.forwardRef((props, forwardedRef) => { isKeyboardDismissDisabled, isOpen, onClose, + position = ModalPosition.top, ...other } = props; @@ -51,7 +52,7 @@ const ModalImpl = React.forwardRef((props, forwardedRef) => { return ( <> - + void; + + /** + * Handles position of modal wrapper + */ + position?: ModalPosition; }