-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Closes #179
- Loading branch information
1 parent
c405303
commit 6d97974
Showing
22 changed files
with
940 additions
and
92 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Button } from '../Button' | ||
import { Emitter } from '@dassana-io/web-utils' | ||
import { ModalConfig } from './utils' | ||
import React from 'react' | ||
import { Meta, Story } from '@storybook/react/types-6-0' | ||
import { ModalProvider, useModal } from './index' | ||
|
||
const mockEmitter = new Emitter() | ||
|
||
export default { | ||
decorators: [ | ||
Story => ( | ||
<ModalProvider emitter={mockEmitter}> | ||
<Story /> | ||
</ModalProvider> | ||
) | ||
], | ||
title: 'Modal' | ||
} as Meta | ||
|
||
const Template: Story<ModalConfig> = args => { | ||
const { setModalConfig } = useModal() | ||
|
||
return <Button onClick={() => setModalConfig(args)}>Open Modal</Button> | ||
} | ||
|
||
export const Default = Template.bind({}) | ||
Default.args = { | ||
content: <div>Modal Content</div>, | ||
options: { | ||
disableKeyboardShortcut: false, | ||
hideCloseButton: false | ||
} | ||
} |
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,96 @@ | ||
import cn from 'classnames' | ||
import { createUseStyles } from 'react-jss' | ||
import { ModalConfig } from './utils' | ||
import noop from 'lodash/noop' | ||
import { | ||
Emitter, | ||
EmitterEventTypes, | ||
useShortcut, | ||
useTheme | ||
} from '@dassana-io/web-utils' | ||
import { IconButton, IconSizes } from 'components/IconButton' | ||
import React, { FC, useEffect } from 'react' | ||
import { styleguide, themedStyles, ThemeType } from 'components/assets/styles' | ||
|
||
const { flexCenter, spacing } = styleguide | ||
|
||
const useStyles = createUseStyles({ | ||
closeButton: { | ||
position: 'absolute', | ||
right: spacing.l, | ||
top: spacing.m, | ||
zIndex: 10000 | ||
}, | ||
container: ({ | ||
overlay, | ||
theme | ||
}: { | ||
overlay: boolean | ||
theme: ThemeType | ||
}) => ({ | ||
...flexCenter, | ||
background: themedStyles[theme].base.backgroundColor, | ||
bottom: 0, | ||
color: themedStyles[theme].base.color, | ||
height: '100%', | ||
left: 0, | ||
opacity: overlay ? 0.6 : 1, | ||
position: 'fixed', | ||
right: 0, | ||
top: 0, | ||
width: '100%', | ||
zIndex: 9999 | ||
}) | ||
}) | ||
|
||
interface ModalProps { | ||
emitter: Emitter | ||
modalConfig: ModalConfig | ||
unsetModal: () => void | ||
} | ||
|
||
const Modal: FC<ModalProps> = ({ | ||
emitter, | ||
modalConfig, | ||
unsetModal | ||
}: ModalProps) => { | ||
const { content, options = {} } = modalConfig | ||
const { | ||
classes = [], | ||
disableKeyboardShortcut = false, | ||
hideCloseButton = false, | ||
onClose, | ||
overlay = false | ||
} = options | ||
const theme = useTheme(emitter) | ||
const modalClasses = useStyles({ overlay, theme }) | ||
|
||
const onModalClose = () => (onClose ? onClose() : unsetModal()) | ||
|
||
useShortcut({ | ||
callback: disableKeyboardShortcut ? noop : onModalClose, | ||
key: 'Escape', | ||
keyEvent: 'keydown' | ||
}) | ||
|
||
useEffect(() => { | ||
emitter.on(EmitterEventTypes.loggedOut, unsetModal) | ||
|
||
return () => emitter.off(EmitterEventTypes.loggedOut, unsetModal) | ||
}) | ||
|
||
return ( | ||
<div className={cn({ [modalClasses.container]: true }, classes)}> | ||
{!hideCloseButton && ( | ||
<IconButton | ||
classes={[modalClasses.closeButton]} | ||
onClick={onModalClose} | ||
size={IconSizes.sm} | ||
/> | ||
)} | ||
{content} | ||
</div> | ||
) | ||
} | ||
|
||
export default Modal |
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,14 @@ | ||
import { createContext, useContext } from 'react' | ||
import { ModalConfig, ModalOptions } from './utils' | ||
|
||
export interface ModalContextProps { | ||
options?: ModalOptions | ||
setModalConfig: (config: ModalConfig) => void | ||
unsetModal: () => void | ||
} | ||
|
||
const ModalCtx = createContext<ModalContextProps>({} as ModalContextProps) | ||
|
||
const useModal = (): ModalContextProps => useContext(ModalCtx) | ||
|
||
export { ModalCtx, useModal } |
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,119 @@ | ||
import { act } from 'react-dom/test-utils' | ||
import { IconButton } from '../../IconButton' | ||
import Modal from '../Modal' | ||
import { ModalOptions } from '../utils' | ||
import { ModalProvider } from 'components/Modal' | ||
import React from 'react' | ||
import { mount, ReactWrapper } from 'enzyme' | ||
|
||
let wrapper: ReactWrapper | ||
|
||
const mockDassanaEmitter = { | ||
emit: jest.fn(), | ||
emitNotificationEvent: jest.fn(), | ||
off: jest.fn(), | ||
on: jest.fn() | ||
} as jest.Mocked<any> | ||
|
||
const mockMessage = 'Hello World' | ||
const mockModalContent = <div>{mockMessage}</div> | ||
|
||
const unsetModalSpy = jest.fn() | ||
|
||
const getWrapper = (options: ModalOptions = {}, mountOptions = {}) => | ||
mount( | ||
<ModalProvider emitter={mockDassanaEmitter}> | ||
<Modal | ||
emitter={mockDassanaEmitter} | ||
modalConfig={{ content: mockModalContent, options }} | ||
unsetModal={unsetModalSpy} | ||
/> | ||
</ModalProvider>, | ||
mountOptions | ||
) | ||
|
||
beforeEach(() => { | ||
wrapper = getWrapper() | ||
}) | ||
|
||
afterEach(() => { | ||
wrapper.unmount() | ||
jest.clearAllMocks() | ||
}) | ||
|
||
it('renders', () => { | ||
expect(wrapper).toHaveLength(1) | ||
}) | ||
|
||
it('should render its content', () => { | ||
expect(wrapper.text()).toContain(mockMessage) | ||
}) | ||
|
||
it('should not call unsetModal if disableKeyboardShortcut is set to true', () => { | ||
wrapper.unmount() | ||
wrapper = getWrapper({ disableKeyboardShortcut: true }) | ||
|
||
act(() => { | ||
dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
code: 'Escape', | ||
key: 'Escape' | ||
}) | ||
) | ||
}) | ||
|
||
expect(unsetModalSpy).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should call the onClose function if one is provided', () => { | ||
const mockOnClose = jest.fn() | ||
|
||
wrapper = getWrapper({ | ||
onClose: mockOnClose | ||
}) | ||
|
||
act(() => { | ||
dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
code: 'Escape', | ||
key: 'Escape' | ||
}) | ||
) | ||
}) | ||
|
||
expect(mockOnClose).toHaveBeenCalled() | ||
}) | ||
|
||
it('should close the modal if Esc is pressed', () => { | ||
act(() => { | ||
dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
code: 'Escape', | ||
key: 'Escape' | ||
}) | ||
) | ||
}) | ||
|
||
expect(unsetModalSpy).toHaveBeenCalled() | ||
}) | ||
|
||
it('should close modal if the close button is clicked', () => { | ||
wrapper.find(IconButton).props().onClick!() | ||
|
||
expect(unsetModalSpy).toHaveBeenCalled() | ||
}) | ||
|
||
it('should have an opaque background if overlay is set to true in options', () => { | ||
const div = document.createElement('div') | ||
div.setAttribute('id', 'container') | ||
document.body.appendChild(div) | ||
|
||
wrapper = getWrapper( | ||
{ overlay: true }, | ||
{ attachTo: document.getElementById('container') } | ||
) | ||
|
||
const style = window.getComputedStyle(wrapper.find(Modal).getDOMNode()) | ||
|
||
expect(style.opacity).not.toEqual(1) | ||
}) |
Oops, something went wrong.