Skip to content

Commit

Permalink
[F] add BasicModal
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgoff committed Dec 21, 2022
1 parent e4229f0 commit d1661db
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/epo-react-lib/.storybook/decorators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const withTheme: DecoratorFn = (StoryFn) => {
return (
<>
<GlobalStyle />
<StoryFn />
{StoryFn()}
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/epo-react-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@castiron/style-mixins": "^1.0.6",
"@headlessui/react": "^1.7.5",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.5.12",
"@storybook/addon-interactions": "^6.5.12",
Expand Down
2 changes: 2 additions & 0 deletions packages/epo-react-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ export * from "@/atomic/Share";
export { default as Columns } from "@/layout/Columns";
export { default as Container } from "@/layout/Container";
export { default as Grid } from "@/layout/Grid";
export { default as MasonryGrid } from "@/layout/MasonryGrid";
export { default as BasicModal } from "@/layout/BasicModal";
108 changes: 108 additions & 0 deletions packages/epo-react-lib/src/layout/BasicModal/BasicModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
ComponentMeta,
ComponentStoryObj,
ComponentStory,
} from "@storybook/react";
import { objChildren } from "@/storybook/utilities/argTypes";

import BasicModal from ".";
import Button from "@/atomic/Button";
import { useState } from "react";

const meta: ComponentMeta<typeof BasicModal> = {
component: BasicModal,
argTypes: {
children: objChildren,
open: {
type: "boolean",
description: "Determines if the modal is visible or not.",
table: {
type: {
summary: "boolean",
},
defaultValue: {
summary: false,
},
},
},
darkMode: {
type: "boolean",
description: "Sets the dark mode theme.",
table: {
type: {
summary: "boolean",
},
defaultValue: {
summary: false,
},
},
},
title: {
type: "string",
description: "Modal title displayed at the top.",
table: {
type: {
summary: "string",
},
},
},
description: {
type: "string",
description: "Modal description displayed after the title.",
table: {
type: {
summary: "string",
},
},
},
onClose: {
type: "function",
action: "Closed",
description:
"Callback either when user clicks outside the modal or clicks the close button. Close button will attach an event, click outside will not.",
table: {
type: {
summary: "(event?: MouseEvent) => void",
},
},
},
},
};
export default meta;

const Template: ComponentStory<typeof BasicModal> = ({
open,
title,
description,
darkMode,
children,
}) => {
const [isOpen, setIsOpen] = useState(open || false);

return (
<>
<Button onClick={() => setIsOpen(true)}>Click to open modal</Button>
<BasicModal
open={isOpen}
onClose={() => setIsOpen(false)}
{...{ title, description, darkMode }}
>
{children}
</BasicModal>
</>
);
};

export const Primary: ComponentStoryObj<typeof BasicModal> = Template.bind({});
Primary.args = {
title: "Modal Title",
description: "Modal description",
children:
"Cosmic ipsum universe right ascension pole star solstice cosmic rays extragalactic black body NASA cluster muttnik synodic superior planets gravitational constant new moon telescope inferior planets syzygy perturbation falling star quasar red dwarf satellite density day dust vernal equinox zodiac inclination azimuth weightlessness spectrum variable star magnitude flare Mir minor planet transparency cosmology full moon terrestrial quarter moon red shift seeing gravity binary star red giant star space station local group",
};

export const DarkMode: ComponentStoryObj<typeof BasicModal> = Template.bind({});
DarkMode.args = {
...Primary.args,
darkMode: true,
};
39 changes: 39 additions & 0 deletions packages/epo-react-lib/src/layout/BasicModal/BasicModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen, fireEvent } from "@testing-library/react";
import BasicModal from ".";

const props = {
children:
"Cosmic ipsum universe right ascension pole star solstice cosmic rays extragalactic black body NASA cluster muttnik synodic superior planets gravitational constant new moon telescope inferior planets syzygy perturbation falling star quasar red dwarf satellite density day dust vernal equinox zodiac inclination azimuth weightlessness spectrum variable star magnitude flare Mir minor planet transparency cosmology full moon terrestrial quarter moon red shift seeing gravity binary star red giant star space station local group",
open: true,
onClose: jest.fn(),
title: "Title",
description: "Description",
};

beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
window.IntersectionObserver = mockIntersectionObserver;
});

describe("BasicModal", () => {
it("should create a modal dialog", () => {
render(<BasicModal {...props} />);

const dialog = screen.getByRole("dialog");
expect(dialog).toBeDefined();
expect(dialog).toHaveAttribute("aria-modal", "true");
});
it("should fire a callback on close", () => {
render(<BasicModal {...props} />);

const closeButton = screen.getByRole("button");
fireEvent.click(closeButton);
expect(props.onClose).toBeCalled();
});
});
43 changes: 43 additions & 0 deletions packages/epo-react-lib/src/layout/BasicModal/BasicModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import IconComposer from "@/svg/IconComposer";
import * as Styled from "./styles";
import { FunctionComponent, MouseEvent, ReactNode } from "react";

interface BasicModalProps {
children: ReactNode;
open?: boolean;
onClose: (event?: MouseEvent) => void;
darkMode?: boolean;
title?: string;
description?: string;
}

const BasicModal: FunctionComponent<BasicModalProps> = ({
children,
open = false,
onClose,
darkMode = false,
title,
description,
}) => {
return (
<Styled.Dialog open={open} onClose={() => onClose()}>
<Styled.Overlay />
<Styled.Inner $darkMode={darkMode}>
<Styled.CloseButton type="button" aria-label="Close" onClick={onClose}>
<IconComposer icon="close" />
</Styled.CloseButton>
<Styled.Content aria-live="polite">
{title && <Styled.Title>{title}</Styled.Title>}
{description && (
<Styled.Description>{description}</Styled.Description>
)}
{children}
</Styled.Content>
</Styled.Inner>
</Styled.Dialog>
);
};

BasicModal.displayName = "Layout.Modal";

export default BasicModal;
1 change: 1 addition & 0 deletions packages/epo-react-lib/src/layout/BasicModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./BasicModal";
89 changes: 89 additions & 0 deletions packages/epo-react-lib/src/layout/BasicModal/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import styled from "styled-components";
import { fluidScale, zStack } from "@/styles/globalStyles";
import { Dialog as BaseDialog } from "@headlessui/react";
import BaseFormButtons from "@/form/FormButtons";
import { ReactNode } from "react";
import { protoButton } from "@/styles/mixins/appearance";

interface InnerProps {
$darkMode: boolean;
}

interface DialogProps {
open: boolean;
onClose: () => void;
children: ReactNode;
}

export const Overlay = styled(BaseDialog.Overlay)`
background-color: rgba(0, 0, 0, 0.7);
position: fixed;
top: 0;
width: 100%;
height: 100%;
`;

export const Dialog = styled(BaseDialog)<DialogProps>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: ${zStack.dialog};
overflow: auto;
padding: 1rem;
display: flex;
align-items: start;
justify-content: center;
`;

export const Title = BaseDialog.Title;

export const Description = styled(BaseDialog.Description)`
font-size: 0.909em;
line-height: 1.5;
&:not(:first-child) {
margin-block-start: 1.4em;
}
`;

export const Inner = styled.div<InnerProps>`
position: relative;
display: flex;
background: var(--white);
max-width: 100vw;
${({ $darkMode }) =>
$darkMode &&
`
color: #fff;
background-color: #161818;
`}
`;

export const Content = styled.div`
flex: 1 1 auto;
padding: ${fluidScale("60px", "15px")};
padding-block-start: 60px;
width: calc(100vw - 30px);
max-width: 550px;
`;

export const CloseButton = styled.button`
${protoButton()}
position: absolute;
display: flex;
align-items: center;
justify-content: center;
right: ${fluidScale("10px", "15px")};
top: ${fluidScale("10px", "15px")};
width: 42px;
height: 42px;
border-radius: 100%;
background: var(--neutral10);
`;

export const FormButtons = styled(BaseFormButtons)`
margin-block-start: 44px;
`;

0 comments on commit d1661db

Please sign in to comment.