-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
284 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
packages/epo-react-lib/src/layout/BasicModal/BasicModal.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
39
packages/epo-react-lib/src/layout/BasicModal/BasicModal.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
43
packages/epo-react-lib/src/layout/BasicModal/BasicModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from "./BasicModal"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
`; |