Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/pretty-bears-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'storybook-manifest': minor
'@project44-manifest/react': minor
---

Added dialog version 2
2 changes: 1 addition & 1 deletion apps/storybook/.storybook/manager.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addons } from '@storybook/addons';
import { addons } from '@storybook/manager-api';
import theme from './theme';

addons.setConfig({ theme });
37 changes: 37 additions & 0 deletions packages/react/src/components/Dialogv2/dialogv2.styles.ts
Original file line number Diff line number Diff line change
@@ -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: '',
},
});
117 changes: 117 additions & 0 deletions packages/react/src/components/Dialogv2/dialogv2.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<DialogProvider value={context}>
<DialogV2Wrapper
{...mergeProps(dialogProps, other)}
ref={mergedRef}
as={as}
className={className}
data-testid="dialogV2Wrapper"
edgeToEdge={edgeToEdge ? 'noPadding' : undefined}
size={size}
>
<DialogHeader data-testid="dialogV2Header">{title}</DialogHeader>
<DialogContent data-testid="dialogV2Content">{body}</DialogContent>
{footer && <DialogFooter data-testid="dialogV2Footer">{footer}</DialogFooter>}
</DialogV2Wrapper>
</DialogProvider>
);
}) as ForwardRefComponent<DialogElement, DialogV2Props>;

DialogV2Impl.displayName = 'DialogImpl';

export const DialogV2 = React.forwardRef((props, forwardedRef) => {
const {
isDismissable = true,
isKeyboardDismissDisabled = false,
isOpen,
position,
...other
} = props;

const { onClose } = other.headerProps;
return (
<Modal
data-testid="dialogV2Modal"
isDismissable={isDismissable}
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
isOpen={isOpen}
position={position}
onClose={onClose}
>
<DialogV2Impl {...other} isDismissable={isDismissable} />
</Modal>
);
}) as ForwardRefComponent<DialogElement, DialogV2Props>;

DialogV2.displayName = 'DialogV2';
Original file line number Diff line number Diff line change
@@ -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<DialogV2Props> = {
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<DialogV2Props> = (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: (
<>
<Button variant="secondary" onPress={handleClose}>
Close
</Button>
<Button onPress={handleClose}>Confirm</Button>
</>
),
};

return (
<>
<Button onPress={handleOpen}>Open Dialog</Button>
<DialogV2 {...args} {...props} />
</>
);
};

Default.args = {
isDismissable: true,
isKeyboardDismissDisabled: true,
edgeToEdge: false,
size: DialogV2Size.small,
position: ModalPosition.top,
};
68 changes: 68 additions & 0 deletions packages/react/src/components/Dialogv2/tests/dialogV2.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<DialogV2
isOpen
body="Some string"
headerProps={{
onClose,
title: 'some title',
}}
/>,
);

expect(screen.getByTestId('dialogV2Wrapper')).toBeInTheDocument();
});

it('should render footer', () => {
const onClose = jest.fn();
render(
<DialogV2
isOpen
body="Some string"
footer="Some footer text that i need to place"
headerProps={{
onClose,
title: 'some title',
}}
/>,
);

expect(screen.getByTestId('dialogV2Footer')).toBeInTheDocument();
});

it('should react to size', () => {
const onClose = jest.fn();
render(
<DialogV2
isOpen
body="Some string"
footer="Some footer text that i need to place"
headerProps={{
onClose,
title: 'some title',
}}
size={DialogV2Size.small}
/>,
);
expect(screen.getByTestId('dialogV2Wrapper').className).toContain('manifest-dialog-small');
});
17 changes: 17 additions & 0 deletions packages/react/src/components/Modal/Modal.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});

Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +24,7 @@ const ModalImpl = React.forwardRef((props, forwardedRef) => {
isKeyboardDismissDisabled,
isOpen,
onClose,
position = ModalPosition.top,
...other
} = props;

Expand Down Expand Up @@ -51,7 +52,7 @@ const ModalImpl = React.forwardRef((props, forwardedRef) => {
return (
<>
<StyledUnderlay {...underlayProps} className="manifest-underlay" isOpen={isOpen} />
<StyledModalWrapper className="manifest-modal-wrapper" isOpen={isOpen}>
<StyledModalWrapper className="manifest-modal-wrapper" isOpen={isOpen} position={position}>
<StyledModal
{...mergeProps(overlayProps, other)}
ref={mergedRef}
Expand Down
10 changes: 10 additions & 0 deletions packages/react/src/components/Modal/Modal.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { CSS } from '@project44-manifest/react-styles';

export enum ModalPosition {
top = 'top',
center = 'center',
}

export type ModalElement = 'div';

export interface ModalProps {
Expand Down Expand Up @@ -27,4 +32,9 @@ export interface ModalProps {
* Handler that is called when the modal should close.
*/
onClose?: () => void;

/**
* Handles position of modal wrapper
*/
position?: ModalPosition;
}