Skip to content

Commit

Permalink
feat(dropdown): add dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
estevanmaito committed Jun 26, 2020
1 parent ea7827b commit e4eeba9
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ plugins: [windmillPlugin()]

- [x] Button
- [x] Card
- [ ] Dropdown
- [x] Dropdown
- [x] Form
- [x] Modal
- [ ] Table
Expand Down
59 changes: 59 additions & 0 deletions __tests__/Dropdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react'
import { mount } from 'enzyme'
import Dropdown from '../src/Dropdown'

describe('Dropdown', () => {
it('should render without crashing', () => {
const onClose = jest.fn()
mount(<Dropdown isOpen={true} onClose={onClose} />)
})

it('should render with base styles', () => {
const onClose = jest.fn()
const expected =
'absolute right-0 w-56 p-2 mt-2 text-gray-600 bg-white border border-gray-100 rounded-lg shadow-md min-w-max-content dark:text-gray-300 dark:border-gray-700 dark:bg-gray-700'
const wrapper = mount(<Dropdown isOpen={true} onClose={onClose} />)

expect(wrapper.find('ul').getDOMNode().getAttribute('class')).toContain(expected)
})

it('should call onClose when Esc is pressed', () => {
const map = {}
document.addEventListener = jest.fn((e, cb) => {
map[e] = cb
})
const onClose = jest.fn()
mount(<Dropdown isOpen={true} onClose={onClose} />)

map.keydown({ key: 'Esc' })

expect(onClose).toHaveBeenCalled()
})

it('should not call onClose when other key than Esc is pressed', () => {
const map = {}
document.addEventListener = jest.fn((e, cb) => {
map[e] = cb
})
const onClose = jest.fn()
mount(<Dropdown isOpen={true} onClose={onClose} />)

map.keydown({ key: 'Enter' })

expect(onClose).not.toHaveBeenCalled()
})

it('should remove the event listener on unmount', () => {
const map = {}
const removeListener = jest.fn((e, cb) => {
map[e] = cb
})
document.removeEventListener = removeListener
const onClose = jest.fn()
const wrapper = mount(<Dropdown isOpen={true} onClose={onClose} />)

wrapper.unmount()

expect(removeListener).toHaveBeenCalled()
})
})
37 changes: 37 additions & 0 deletions __tests__/DropdownItem.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import { mount } from 'enzyme'
import DropdownItem from '../src/DropdownItem'
import Button from '../src/Button'

describe('DropdownItem', () => {
it('should render without crashing', () => {
mount(<DropdownItem />)
})

it('should render with base styles', () => {
const expected = 'mb-2 last:mb-0'
const wrapper = mount(<DropdownItem />)

expect(wrapper.find('li').getDOMNode().getAttribute('class')).toContain(expected)
})

it('should contain a Button child', () => {
const wrapper = mount(<DropdownItem />)

expect(wrapper.find(Button)).toBeTruthy()
})

it('should pass className to the inner button', () => {
const expected = 'bg-red-600'
const wrapper = mount(<DropdownItem className="bg-red-600" />)

expect(wrapper.find(Button).getDOMNode().getAttribute('class')).toContain(expected)
})

it('should pass extra props to the inner button', () => {
const expected = 'test'
const wrapper = mount(<DropdownItem tag="a" href="test" />)

expect(wrapper.find('a').getDOMNode().getAttribute('href')).toContain(expected)
})
})
54 changes: 54 additions & 0 deletions src/Dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useEffect, useContext } from 'react'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import { ThemeContext } from './context/ThemeContext'
import defaultTheme from './themes/default'
import Transition from './Transition'
import FocusLock from 'react-focus-lock'

function Dropdown({ children, onClose, isOpen, className, ...other }) {
const { dropdown } = useContext(ThemeContext) || defaultTheme

const baseStyle = dropdown.base

function handleEsc(e) {
if (e.key === 'Esc' || e.key === 'Escape') {
onClose()
}
}

useEffect(() => {
document.addEventListener('keydown', handleEsc)
return () => {
document.removeEventListener('keydown', handleEsc)
}
})

const cls = classNames(baseStyle, className)

return (
<Transition
show={isOpen}
leave="transition ease-out duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div>
<FocusLock returnFocus>
<ul className={cls} aria-label="submenu" {...other}>
{children}
</ul>
</FocusLock>
</div>
</Transition>
)
}

Dropdown.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
onClose: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
}

export default Dropdown
29 changes: 29 additions & 0 deletions src/DropdownItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useContext } from 'react'
import PropTypes from 'prop-types'
import { ThemeContext } from './context/ThemeContext'
import defaultTheme from './themes/default'
import Button from './Button'

const DropdownItem = React.forwardRef(function DropdownItem(props, ref) {
// Note: className is passed to the inner Button
const { className, children, ...other } = props

const { dropdownItem } = useContext(ThemeContext) || defaultTheme

const baseStyle = dropdownItem.base

return (
<li className={baseStyle}>
<Button layout="__dropdownItem" ref={ref} className={className} {...other}>
{children}
</Button>
</li>
)
})

DropdownItem.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
}

export default DropdownItem
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { default as ModalBody } from './ModalBody'
export { default as ModalFooter } from './ModalFooter'
export { default as ModalHeader } from './ModalHeader'
export { default as Avatar } from './Avatar'
export { default as Dropdown } from './Dropdown'
12 changes: 12 additions & 0 deletions src/themes/default.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export default {
// DropdownItem
// this is the <li> that lives inside the Dropdown <ul>
// you're probably looking for the dropdownItem style inside button
dropdownItem: {
base: 'mb-2 last:mb-0',
},
// Dropdown
dropdown: {
base:
'absolute right-0 w-56 p-2 mt-2 text-gray-600 bg-white border border-gray-100 rounded-lg shadow-md min-w-max-content dark:text-gray-300 dark:border-gray-700 dark:bg-gray-700',
},
// Avatar
avatar: {
base: 'relative rounded-full',
Expand Down Expand Up @@ -117,6 +128,7 @@ export default {
active: 'hover:bg-gray-100 focus:shadow-outline-gray',
disabled: 'opacity-50 cursor-not-allowed',
},
// this is the button that lives inside the DropdownItem
dropdownItem: {
base:
'inline-flex items-center cursor-pointer w-full px-2 py-1 text-sm font-medium transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200',
Expand Down

0 comments on commit e4eeba9

Please sign in to comment.