Refactor Dropdown component and specs
parent
1a809b4c81
commit
7e90245dba
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue