Refactor Dropdown component and specs

pull/2903/head
Andrew Watkins 2018-02-28 14:05:24 -07:00
parent 1a809b4c81
commit 7e90245dba
5 changed files with 304 additions and 155 deletions

View File

@ -1,10 +1,10 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
import DropdownMenu, {DropdownMenuEmpty} from 'shared/components/DropdownMenu'
import DropdownInput from 'shared/components/DropdownInput'
import DropdownHead from 'shared/components/DropdownHead'
export class Dropdown extends Component {
constructor(props) {
@ -123,111 +123,33 @@ export class Dropdown extends Component {
})
}
renderMenu() {
const {
actions,
addNew,
items,
menuWidth,
menuLabel,
menuClass,
useAutoComplete,
selected,
} = this.props
const {filteredItems, highlightedItemIndex} = this.state
const menuItems = useAutoComplete ? filteredItems : items
return (
<ul
className={classnames('dropdown-menu', {
'dropdown-menu--no-highlight': useAutoComplete,
[menuClass]: menuClass,
})}
style={{width: menuWidth}}
data-test="dropdown-ul"
>
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
data-test="scrollbar"
>
{menuLabel
? <li className="dropdown-header">
{menuLabel}
</li>
: null}
{menuItems.map((item, i) => {
if (item.text === 'SEPARATOR') {
return <li key={i} className="dropdown-divider" />
}
return (
<li
className={classnames('dropdown-item', {
highlight: i === highlightedItemIndex,
active: item.text === selected,
})}
data-test="dropdown-item"
key={i}
>
<a
href="#"
onClick={this.handleSelection(item)}
onMouseOver={this.handleHighlight(i)}
>
{item.text}
</a>
{actions && actions.length
? <div className="dropdown-actions">
{actions.map(action => {
return (
<button
key={action.text}
className="dropdown-action"
onClick={this.handleAction(action, item)}
>
<span
title={action.text}
className={`icon ${action.icon}`}
/>
</button>
)
})}
</div>
: null}
</li>
)
})}
{addNew
? <li className="multi-select--apply">
<Link className="btn btn-xs btn-default" to={addNew.url}>
{addNew.text}
</Link>
</li>
: null}
</FancyScrollbar>
</ul>
)
}
render() {
const {
items,
addNew,
actions,
selected,
disabled,
iconName,
className,
menuClass,
iconName,
menuWidth,
menuLabel,
buttonSize,
buttonColor,
toggleStyle,
useAutoComplete,
<<<<<<< HEAD
disabled,
tabIndex,
=======
>>>>>>> Refactor Dropdown component and specs
} = this.props
const {isOpen, searchTerm, filteredItems} = this.state
const menuItems = useAutoComplete ? filteredItems : items
const {isOpen, searchTerm, filteredItems, highlightedItemIndex} = this.state
const menuItems = useAutoComplete ? filteredItems : items
const disabledClass = disabled ? 'disabled' : null
return (
<div
onClick={this.handleClick}
@ -237,49 +159,46 @@ export class Dropdown extends Component {
})}
tabIndex={tabIndex}
ref={r => (this.dropdownRef = r)}
data-test="dropdown-button"
data-test="dropdown-toggle"
>
{useAutoComplete && isOpen
? <div
className={`dropdown-autocomplete dropdown-toggle ${buttonSize} ${buttonColor} ${disabledClass}`}
style={toggleStyle}
>
<input
ref="dropdownAutoComplete"
className="dropdown-autocomplete--input"
type="text"
autoFocus={true}
placeholder="Filter items..."
spellCheck={false}
onChange={this.handleFilterChange}
onKeyDown={this.handleFilterKeyPress}
value={searchTerm}
/>
<span className="caret" />
</div>
: <div
className={`btn dropdown-toggle ${buttonSize} ${buttonColor} ${disabledClass}`}
style={toggleStyle}
>
{iconName
? <span className={classnames('icon', {[iconName]: true})} />
: null}
<span className="dropdown-selected">
{selected}
</span>
<span className="caret" />
</div>}
{isOpen && menuItems.length ? this.renderMenu() : null}
{isOpen && !menuItems.length
? <ul
className={classnames('dropdown-menu', {
'dropdown-menu--no-highlight': useAutoComplete,
[menuClass]: menuClass,
})}
>
<li className="dropdown-empty">No matching items</li>
</ul>
: null}
? <DropdownInput
searchTerm={searchTerm}
buttonSize={buttonSize}
buttonColor={buttonColor}
toggleStyle={toggleStyle}
disabledClass={disabledClass}
onFilterChange={this.handleFilterChange}
onFilterKeyPress={this.handleFilterKeyPress}
/>
: <DropdownHead
iconName={iconName}
selected={selected}
searchTerm={searchTerm}
buttonSize={buttonSize}
buttonColor={buttonColor}
toggleStyle={toggleStyle}
disabledClass={disabledClass}
/>}
{isOpen && menuItems.length
? <DropdownMenu
addNew={addNew}
actions={actions}
items={menuItems}
selected={selected}
menuClass={menuClass}
menuWidth={menuWidth}
menuLabel={menuLabel}
onAction={this.handleAction}
useAutoComplete={useAutoComplete}
onSelection={this.handleSelection}
onHighlight={this.handleHighlight}
highlightedItemIndex={highlightedItemIndex}
/>
: <DropdownMenuEmpty
useAutoComplete={useAutoComplete}
menuClass={menuClass}
/>}
</div>
)
}

View File

@ -0,0 +1,35 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const DropdownHead = ({
iconName,
selected,
buttonSize,
toggleStyle,
buttonColor,
disabledClass,
}) =>
<div
className={`btn dropdown-toggle ${buttonSize} ${buttonColor} ${disabledClass}`}
style={toggleStyle}
>
{iconName && <span className={classnames('icon', {[iconName]: true})} />}
<span className="dropdown-selected">
{selected}
</span>
<span className="caret" />
</div>
const {string, shape} = PropTypes
DropdownHead.propTypes = {
iconName: string,
selected: string,
buttonSize: string,
toggleStyle: shape(),
buttonColor: string,
disabledClass: string,
}
export default DropdownHead

View File

@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
const DropdownInput = ({
searchTerm,
buttonSize,
buttonColor,
toggleStyle,
disabledClass,
onFilterChange,
onFilterKeyPress,
}) =>
<div
className={`dropdown-autocomplete dropdown-toggle ${buttonSize} ${buttonColor} ${disabledClass}`}
style={toggleStyle}
>
<input
className="dropdown-autocomplete--input"
type="text"
autoFocus={true}
placeholder="Filter items..."
spellCheck={false}
onChange={onFilterChange}
onKeyDown={onFilterKeyPress}
value={searchTerm}
/>
<span className="caret" />
</div>
export default DropdownInput
const {func, shape, string} = PropTypes
DropdownInput.propTypes = {
searchTerm: string,
buttonSize: string,
buttonColor: string,
toggleStyle: shape({}),
disabledClass: string,
onFilterChange: func,
onFilterKeyPress: func,
}

View File

@ -0,0 +1,156 @@
import React from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import classnames from 'classnames'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
import FancyScrollbar from 'shared/components/FancyScrollbar'
// AddNewResource is an optional parameter that takes the user to another
// route defined by url prop
const AddNewButton = ({url, text}) =>
<li className="multi-select--apply">
<Link className="btn btn-xs btn-default" to={url}>
{text}
</Link>
</li>
const DropdownMenu = ({
items,
addNew,
actions,
selected,
onAction,
menuClass,
menuWidth,
menuLabel,
onSelection,
onHighlight,
useAutoComplete,
highlightedItemIndex,
}) => {
return (
<ul
className={classnames('dropdown-menu', {
'dropdown-menu--no-highlight': useAutoComplete,
[menuClass]: menuClass,
})}
style={{width: menuWidth}}
data-test="dropdown-ul"
>
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
data-test="scrollbar"
>
{menuLabel
? <li className="dropdown-header">
{menuLabel}
</li>
: null}
{items.map((item, i) => {
if (item.text === 'SEPARATOR') {
return <li key={i} className="dropdown-divider" />
}
return (
<li
className={classnames('dropdown-item', {
highlight: i === highlightedItemIndex,
active: item.text === selected,
})}
data-test="dropdown-item"
key={i}
>
<a
href="#"
onClick={onSelection(item)}
onMouseOver={onHighlight(i)}
>
{item.text}
</a>
{actions && actions.length
? <div className="dropdown-actions">
{actions.map(action => {
return (
<button
key={action.text}
className="dropdown-action"
onClick={onAction(action, item)}
>
<span
title={action.text}
className={`icon ${action.icon}`}
/>
</button>
)
})}
</div>
: null}
</li>
)
})}
{addNew && <AddNewButton url={addNew.url} text={addNew.text} />}
</FancyScrollbar>
</ul>
)
}
export const DropdownMenuEmpty = ({useAutoComplete, menuClass}) =>
<ul
className={classnames('dropdown-menu', {
'dropdown-menu--no-highlight': useAutoComplete,
[menuClass]: menuClass,
})}
>
<li className="dropdown-empty">No matching items</li>
</ul>
const {arrayOf, bool, number, shape, string, func} = PropTypes
AddNewButton.propTypes = {
url: string,
text: string,
}
DropdownMenuEmpty.propTypes = {
useAutoComplete: bool,
menuClass: string,
}
DropdownMenu.propTypes = {
onAction: func,
actions: arrayOf(
shape({
icon: string.isRequired,
text: string.isRequired,
handler: func.isRequired,
})
),
items: arrayOf(
shape({
text: string.isRequired,
})
).isRequired,
onClick: func,
addNew: shape({
url: string.isRequired,
text: string.isRequired,
}),
selected: string.isRequired,
iconName: string,
className: string,
buttonColor: string,
menuWidth: string,
menuLabel: string,
menuClass: string,
useAutoComplete: bool,
disabled: bool,
searchTerm: string,
onSelection: func,
onHighlight: func,
highlightedItemIndex: number,
}
export default DropdownMenu

View File

@ -1,6 +1,9 @@
import React from 'react'
import Dropdown from 'shared/components/dropdown'
import Dropdown from 'shared/components/Dropdown'
import DropdownMenu from 'shared/components/DropdownMenu'
import {shallow} from 'enzyme'
const items = [{text: 'foo'}, {text: 'bar'}]
const setup = (override = {}) => {
const props = {
@ -10,10 +13,12 @@ const setup = (override = {}) => {
...override,
}
const wrapper = shallow(<Dropdown {...props} />)
const dropdown = shallow(<Dropdown {...props} />).dive({
'data-test': 'dropdown-button',
})
return {
wrapper,
dropdown,
props,
}
}
@ -21,36 +26,28 @@ const setup = (override = {}) => {
describe('Components.Shared.Dropdown', () => {
describe('rednering', () => {
describe('initial render', () => {
it('renders the dropdown button', () => {
const {wrapper} = setup()
it('renders the dropdown menu button', () => {
const {dropdown} = setup()
const actual = wrapper.dive({'data-test': 'dropdown-button'})
expect(actual.length).toBe(1)
expect(dropdown.exists()).toBe(true)
})
it('does not show the list', () => {
const items = [{text: 'foo'}, {text: 'bar'}]
const {wrapper} = setup({items})
const {dropdown} = setup({items})
const dropdown = wrapper.dive({'data-test': 'dropdown-button'})
const list = dropdown.find({'data-test': 'dropdown-items'})
expect(list.length).toBe(0)
const menu = dropdown.find(DropdownMenu)
expect(menu.exists()).toBe(false)
})
})
describe('user interactions', () => {
it('shows the list when clicked', () => {
const items = [{text: 'foo'}, {text: 'bar'}]
const {wrapper} = setup({items})
it('shows the menu when clicked', () => {
const {dropdown} = setup({items})
const dropdown = wrapper.dive({'data-test': 'dropdown-button'})
dropdown.simulate('click')
const ul = dropdown.find({'data-test': 'dropdown-ul'})
expect(ul.length).toBe(1)
const list = ul.find({'data-test': 'dropdown-item'})
expect(list.length).toBe(items.length)
const menu = dropdown.find(DropdownMenu)
expect(menu.exists()).toBe(true)
})
})
})