Skip to content

Commit

Permalink
chore #179 - Modal and Wizard components (#180)
Browse files Browse the repository at this point in the history
Closes #179
  • Loading branch information
nancy-dassana authored Jan 6, 2021
1 parent c405303 commit 6d97974
Show file tree
Hide file tree
Showing 22 changed files with 940 additions and 92 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dassana-io/web-components",
"version": "0.7.7",
"version": "0.8.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com/dassana-io"
},
Expand Down
18 changes: 18 additions & 0 deletions src/__snapshots__/storybook.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,24 @@ exports[`Storyshots Link Href 1`] = `
</div>
`;

exports[`Storyshots Modal Default 1`] = `
<div
className="light storyWrapper-0-1-2"
>
<button
className="ant-btn ant-btn-default"
data-test="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span>
Open Modal
</span>
</button>
</div>
`;

exports[`Storyshots Notification Error 1`] = `
<div
className="light storyWrapper-0-1-2"
Expand Down
34 changes: 34 additions & 0 deletions src/components/Modal/Modal.stories.tsx
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
}
}
96 changes: 96 additions & 0 deletions src/components/Modal/Modal.tsx
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
14 changes: 14 additions & 0 deletions src/components/Modal/ModalContext.ts
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 }
119 changes: 119 additions & 0 deletions src/components/Modal/__tests__/Modal.test.tsx
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)
})
Loading

0 comments on commit 6d97974

Please sign in to comment.