Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Consolidate showDropdown props #253

Merged
merged 14 commits into from
Jun 10, 2019
Merged
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ A lightweight and fast control to render a select component that can display hie
- [radioSelect](#radioSelect)
- [showPartiallySelected](#showpartiallyselected)
- [showDropdown](#showDropdown)
- [showDropdownAlways](#showDropdownAlways)
- [initial](#initial)
- [always](#always)
- [form states (disabled|readOnly)](#formstates)
- [id](#id)
- [Styling and Customization](#styling-and-customization)
Expand Down Expand Up @@ -358,15 +359,17 @@ If set to true, shows checkboxes in a partial state when one, but not all of the

### showDropdown

Type: `bool` (default: `false`)
Type: `string`

If set to true, shows the dropdown when rendered. This can be used to render the component with the dropdown open as its initial state.
Let's you choose the rendered state of the dropdown.

### showDropdownAlways
#### initial

Type: `bool`
`showDropdown: initial` shows the dropdown when rendered. This can be used to render the component with the dropdown open as its initial state.

#### always

If set to true, always shows the dropdown when rendered, and toggling dropdown will be disabled.
`showDropdown: always` shows the dropdown when rendered, and keeps it visible at all times. Toggling dropdown is disabled.

### form states (disabled|readOnly)

Expand Down
2 changes: 2 additions & 0 deletions __snapshots__/src/index.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ Generated by [AVA](https://ava.li).
onBlur={Function onBlur {}}
onChange={Function onChange {}}
onFocus={Function onFocus {}}
showDropdown="default"
texts={{}}
>
<div
Expand Down Expand Up @@ -414,6 +415,7 @@ Generated by [AVA](https://ava.li).
onBlur={Function onBlur {}}
onChange={Function onChange {}}
onFocus={Function onFocus {}}
showDropdown="default"
texts={{}}
>
<div
Expand Down
Binary file modified __snapshots__/src/index.test.js.snap
Binary file not shown.
14 changes: 14 additions & 0 deletions docs/src/stories/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class WithOptions extends PureComponent {
showPartiallySelected,
disabled,
readOnly,
showDropdown,
} = this.state

return (
Expand All @@ -68,6 +69,18 @@ class WithOptions extends PureComponent {
<option value="hierarchical">Hierarchical</option>
</select>
</div>
<div style={{ marginBottom: '10px' }}>
<label htmlFor={showDropdown}>ShowDropdown: </label>
<select
id="showDropdown"
value={showDropdown}
onChange={e => this.setState({ showDropdown: e.target.value })}
>
<option value="default">--</option>
<option value="initial">Initial</option>
<option value="always">Always</option>
</select>
</div>
<Checkbox
label="Clear search on selection"
value="clearSearchOnChange"
Expand Down Expand Up @@ -109,6 +122,7 @@ class WithOptions extends PureComponent {
showPartiallySelected={showPartiallySelected}
disabled={disabled}
readOnly={readOnly}
showDropdown={showDropdown}
/>
</div>
</div>
Expand Down
17 changes: 9 additions & 8 deletions src/a11y/a11y.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,20 @@ test.beforeEach(t => {
]
})

test('regular tree has no a11y exceptions', async t => {
test('has no a11y exceptions', async t => {
const { tree } = t.context
const component = mountToDoc(
<div>
<DropdownTreeSelect data={tree} showDropDown texts={{ label: 'test' }} />
mrchief marked this conversation as resolved.
Show resolved Hide resolved
<DropdownTreeSelect data={tree} showDropDown disabled texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropDown readOnly texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropDown mode="simpleSelect" texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropDown mode="radioSelect" texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropdown="initial" texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropdown="initial" disabled texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropdown="initial" readOnly texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropdown="initial" mode="simpleSelect" texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropdown="initial" mode="radioSelect" texts={{ label: 'test' }} />
<DropdownTreeSelect data={tree} showDropdown="initial" mode="hierarchical" texts={{ label: 'test' }} />
</div>
)
const domNode = component.getDOMNode()
const { error, violations } = await run(domNode)
t.is(error, null, JSON.stringify(error, null, 4))
t.is(violations.length, 0, JSON.stringify(violations, null, 4))
t.is(error, null, JSON.stringify(error, null, 2))
t.is(violations.length, 0, JSON.stringify(violations, null, 2))
})
33 changes: 23 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import TreeManager from './tree-manager'
import keyboardNavigation from './tree-manager/keyboardNavigation'

import styles from './index.css'
import { getAriaLabel } from './a11y'

const cx = cn.bind(styles)

Expand All @@ -34,8 +35,7 @@ class DropdownTreeSelect extends Component {
label: PropTypes.string,
labelRemove: PropTypes.string,
}),
showDropdown: PropTypes.bool,
showDropdownAlways: PropTypes.bool,
showDropdown: PropTypes.oneOf(['default', 'initial', 'always']),
className: PropTypes.string,
onChange: PropTypes.func,
onAction: PropTypes.func,
Expand All @@ -54,19 +54,19 @@ class DropdownTreeSelect extends Component {
onBlur: () => {},
onChange: () => {},
texts: {},
showDropdown: 'default',
}

constructor(props) {
super(props)
this.state = {
showDropdown: this.props.showDropdown || this.props.showDropdownAlways || false,
searchModeOn: false,
currentFocus: undefined,
}
this.clientId = props.id || clientIdGenerator.get(this)
}

initNewProps = ({ data, mode, showPartiallySelected }) => {
initNewProps = ({ data, mode, showDropdown, showPartiallySelected }) => {
this.treeManager = new TreeManager({
data,
mode,
Expand All @@ -78,7 +78,10 @@ class DropdownTreeSelect extends Component {
if (currentFocusNode) {
currentFocusNode._focused = true
}
this.setState(this.treeManager.getTreeAndTags())
this.setState({
showDropdown: /initial|always/.test(showDropdown) || false,
...this.treeManager.getTreeAndTags(),
})
}

resetSearchState = () => {
Expand All @@ -92,8 +95,7 @@ class DropdownTreeSelect extends Component {
}

componentWillMount() {
const { data, mode, showPartiallySelected } = this.props
this.initNewProps({ data, mode, showPartiallySelected })
this.initNewProps(this.props)
}

componentWillUnmount() {
Expand All @@ -107,7 +109,7 @@ class DropdownTreeSelect extends Component {
handleClick = (e, callback) => {
this.setState(prevState => {
// keep dropdown active when typing in search box
const showDropdown = this.props.showDropdownAlways || this.keepDropdownActive || !prevState.showDropdown
const showDropdown = this.props.showDropdown === 'always' || this.keepDropdownActive || !prevState.showDropdown

// register event listeners only if there is a state change
if (showDropdown !== prevState.showDropdown) {
Expand All @@ -126,7 +128,7 @@ class DropdownTreeSelect extends Component {
}

handleOutsideClick = e => {
if (this.props.showDropdownAlways || !isOutsideClick(e, this.node)) {
if (this.props.showDropdown === 'always' || !isOutsideClick(e, this.node)) {
return
}

Expand Down Expand Up @@ -260,6 +262,17 @@ class DropdownTreeSelect extends Component {
e.preventDefault()
}

getAriaAttributes = () => {
const { mode, texts } = this.props

if (mode !== 'radioSelect') return {}

return {
role: 'radiogroup',
...getAriaLabel(texts.label),
}
}

render() {
const { disabled, readOnly, mode, texts } = this.props
const { showDropdown, currentFocus } = this.state
Expand Down Expand Up @@ -298,7 +311,7 @@ class DropdownTreeSelect extends Component {
/>
</Trigger>
{showDropdown && (
<div className="dropdown-content">
<div className="dropdown-content" {...this.getAriaAttributes()}>
{this.state.allNodesHidden ? (
<span className="no-matches">{texts.noMatches || 'No matches found'}</span>
) : (
Expand Down
4 changes: 2 additions & 2 deletions src/index.keyboardNav.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ test('can collapse on keyboardNavigation', t => {
})

test('can navigate searchresult on keyboardNavigation', t => {
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown />)
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown="initial" />)
wrapper.instance().onInputChange('bb')
triggerOnKeyboardKeyDown(wrapper, ['b', 'ArrowDown', 'ArrowDown', 'ArrowDown'])
t.deepEqual(wrapper.find('li.focused').text(), 'bbb 1')
Expand Down Expand Up @@ -147,7 +147,7 @@ test('can delete tags with backspace/delete on keyboardNavigation', t => {
})

test('remembers current focus between prop updates', t => {
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown />)
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown="initial" />)
t.false(wrapper.find('li.focused').exists())
triggerOnKeyboardKeyDown(wrapper, ['ArrowDown'])
t.deepEqual(wrapper.find('li.focused').text(), 'ccc 1')
Expand Down
14 changes: 7 additions & 7 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,27 @@ test('renders default radio select state', t => {

test('shows dropdown', t => {
const { tree } = t.context
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown />)
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" />)
t.snapshot(toJson(wrapper))
})

test('always shows dropdown', t => {
const { tree } = t.context
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdownAlways />)
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="always" />)
t.snapshot(toJson(wrapper))
})

test('keeps dropdown open for showDropdownAlways', t => {
test('keeps dropdown open for showDropdown: always', t => {
const { tree } = t.context
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdownAlways />)
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="always" />)
wrapper.instance().handleClick()
t.true(wrapper.state().showDropdown)
})

test('notifies on action', t => {
const handler = spy()
const { tree } = t.context
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} onAction={handler} showDropdown />)
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} onAction={handler} showDropdown="initial" />)
wrapper.find('i.fa-ban').simulate('click')
t.true(handler.calledWithExactly(node0, action))
})
Expand Down Expand Up @@ -134,7 +134,7 @@ test('sets search mode on input change', t => {

test('hides dropdown onChange for simpleSelect', t => {
const { tree } = t.context
const wrapper = mount(<DropdownTreeSelect id={dropdownId} showDropdown data={tree} mode="simpleSelect" />)
const wrapper = mount(<DropdownTreeSelect id={dropdownId} showDropdown="initial" data={tree} mode="simpleSelect" />)
wrapper.instance().onCheckboxChange(node0._id, true)
t.false(wrapper.state().searchModeOn)
t.false(wrapper.state().allNodesHidden)
Expand All @@ -144,7 +144,7 @@ test('hides dropdown onChange for simpleSelect', t => {
test('keeps dropdown open onChange for simpleSelect and keepOpenOnSelect', t => {
const { tree } = t.context
const wrapper = mount(
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown mode="simpleSelect" keepOpenOnSelect />
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" mode="simpleSelect" keepOpenOnSelect />
)
wrapper.instance().onCheckboxChange(node0._id, true)
t.true(wrapper.state().showDropdown)
Expand Down
6 changes: 3 additions & 3 deletions src/tree-manager/tests/radioSelect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ const dropdownId = 'rdts'
const tree = ['nodeA', 'nodeB', 'nodeC'].map(nv => ({ id: nv, label: nv, value: nv }))

test('should render radio inputs with shared name', t => {
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} mode="radioSelect" showDropdown />)
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} mode="radioSelect" showDropdown="initial" />)

const inputs = wrapper.find('.dropdown-content').find(`input[type="radio"][name="${dropdownId}"]`)
t.deepEqual(inputs.length, 3)
})

test('hides dropdown onChange for radioSelect', t => {
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown mode="radioSelect" />)
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" mode="radioSelect" />)
wrapper.instance().onCheckboxChange('nodeA', true)
t.false(wrapper.state().searchModeOn)
t.false(wrapper.state().allNodesHidden)
Expand All @@ -24,7 +24,7 @@ test('hides dropdown onChange for radioSelect', t => {

test('keeps dropdown open onChange for radioSelect and keepOpenOnSelect', t => {
const wrapper = mount(
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown mode="radioSelect" keepOpenOnSelect />
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" mode="radioSelect" keepOpenOnSelect />
)
wrapper.instance().onCheckboxChange('nodeA', true)
t.true(wrapper.state().showDropdown)
Expand Down
13 changes: 8 additions & 5 deletions src/tree/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ class Tree extends Component {
}

getAriaAttributes = () => {
const { readOnly, mode } = this.props
const attributes = {}
const { mode } = this.props

const attributes = {
/* https://www.w3.org/TR/wai-aria-1.1/#select
* https://www.w3.org/TR/wai-aria-1.1/#tree */
role: mode === 'simpleSelect' ? 'listbox' : 'tree',
'aria-multiselectable': /multiSelect|hierarchical/.test(mode),
}

attributes.role = mode === 'simpleSelect' ? 'listbox' : 'tree'
attributes['aria-multiselectable'] = mode === 'multiSelect' ? 'true' : 'false'
attributes['aria-readonly'] = readOnly ? 'true' : 'false'
return attributes
}

Expand Down
17 changes: 9 additions & 8 deletions types/react-dropdown-tree-select.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ declare module 'react-dropdown-tree-select' {

export type TreeData = Object | TreeNodeProps[]

export type Mode = 'multiSelect' | 'simpleSelect' | 'radioSelect' | 'hierarchical'

export type ShowDropdownState = 'default' | 'initial' | 'always'

export interface DropdownTreeSelectProps {
data: TreeData
/** Clear the input search if a node has been selected/unselected */
Expand All @@ -21,13 +25,10 @@ declare module 'react-dropdown-tree-select' {
keepOpenOnSelect?: boolean
/** Texts to override output for */
texts?: TextProps
/** If set to true, shows the dropdown when rendered.
* This can be used to render the component with the dropdown open as its initial state
*/
showDropdown?: boolean
/** If set to true, always shows the dropdown when rendered, and toggling dropdown will be disabled.
/** If set to initial, shows the dropdown when first rendered.
* If set to always, shows the dropdown when rendered, and keeps it visible at all times. Toggling dropdown is disabled.
*/
showDropdownAlways?: boolean
showDropdown?: ShowDropdownState
/** Additional classname for container.
* The container renders with a default classname of react-dropdown-tree-select
*/
Expand Down Expand Up @@ -77,7 +78,7 @@ declare module 'react-dropdown-tree-select' {
*
*
* */
mode?: 'multiSelect' | 'simpleSelect' | 'radioSelect' | 'hierarchical'
mode?: Mode
/** If set to true, shows checkboxes in a partial state when one, but not all of their children are selected.
* Allows styling of partially selected nodes as well, by using :indeterminate pseudo class.
* Simply add desired styles to .node.partial .checkbox-item:indeterminate { ... } in your CSS
Expand All @@ -97,7 +98,7 @@ declare module 'react-dropdown-tree-select' {
}

export interface DropdownTreeSelectState {
showDropdown: boolean
showDropdown: ShowDropdownState
searchModeOn: boolean
allNodesHidden: boolean
tree: TreeNode[]
Expand Down