diff --git a/README.md b/README.md index b797d701..3e8f9247 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) diff --git a/__snapshots__/src/index.test.js.md b/__snapshots__/src/index.test.js.md index 58ac58de..5db354fa 100644 --- a/__snapshots__/src/index.test.js.md +++ b/__snapshots__/src/index.test.js.md @@ -276,6 +276,7 @@ Generated by [AVA](https://ava.li). onBlur={Function onBlur {}} onChange={Function onChange {}} onFocus={Function onFocus {}} + showDropdown="default" texts={{}} >
Hierarchical
+
+ + +
diff --git a/src/a11y/a11y.test.js b/src/a11y/a11y.test.js index 446c3542..c0eaca02 100644 --- a/src/a11y/a11y.test.js +++ b/src/a11y/a11y.test.js @@ -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(
- - - - - + + + + + +
) 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)) }) diff --git a/src/index.js b/src/index.js index 6d03592c..f812a41d 100644 --- a/src/index.js +++ b/src/index.js @@ -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) @@ -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, @@ -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, @@ -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 = () => { @@ -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() { @@ -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) { @@ -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 } @@ -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 @@ -298,7 +311,7 @@ class DropdownTreeSelect extends Component { /> {showDropdown && ( -
+
{this.state.allNodesHidden ? ( {texts.noMatches || 'No matches found'} ) : ( diff --git a/src/index.keyboardNav.test.js b/src/index.keyboardNav.test.js index dc78779d..3cd039a2 100644 --- a/src/index.keyboardNav.test.js +++ b/src/index.keyboardNav.test.js @@ -98,7 +98,7 @@ test('can collapse on keyboardNavigation', t => { }) test('can navigate searchresult on keyboardNavigation', t => { - const wrapper = mount() + const wrapper = mount() wrapper.instance().onInputChange('bb') triggerOnKeyboardKeyDown(wrapper, ['b', 'ArrowDown', 'ArrowDown', 'ArrowDown']) t.deepEqual(wrapper.find('li.focused').text(), 'bbb 1') @@ -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() + const wrapper = mount() t.false(wrapper.find('li.focused').exists()) triggerOnKeyboardKeyDown(wrapper, ['ArrowDown']) t.deepEqual(wrapper.find('li.focused').text(), 'ccc 1') diff --git a/src/index.test.js b/src/index.test.js index 9b3e2cb6..c79f22d2 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -76,19 +76,19 @@ test('renders default radio select state', t => { test('shows dropdown', t => { const { tree } = t.context - const wrapper = shallow() + const wrapper = shallow() t.snapshot(toJson(wrapper)) }) test('always shows dropdown', t => { const { tree } = t.context - const wrapper = shallow() + const wrapper = shallow() 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() + const wrapper = mount() wrapper.instance().handleClick() t.true(wrapper.state().showDropdown) }) @@ -96,7 +96,7 @@ test('keeps dropdown open for showDropdownAlways', t => { test('notifies on action', t => { const handler = spy() const { tree } = t.context - const wrapper = mount() + const wrapper = mount() wrapper.find('i.fa-ban').simulate('click') t.true(handler.calledWithExactly(node0, action)) }) @@ -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() + const wrapper = mount() wrapper.instance().onCheckboxChange(node0._id, true) t.false(wrapper.state().searchModeOn) t.false(wrapper.state().allNodesHidden) @@ -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( - + ) wrapper.instance().onCheckboxChange(node0._id, true) t.true(wrapper.state().showDropdown) diff --git a/src/tree-manager/tests/radioSelect.test.js b/src/tree-manager/tests/radioSelect.test.js index 66d900df..73c18897 100644 --- a/src/tree-manager/tests/radioSelect.test.js +++ b/src/tree-manager/tests/radioSelect.test.js @@ -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() + const wrapper = mount() 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() + const wrapper = mount() wrapper.instance().onCheckboxChange('nodeA', true) t.false(wrapper.state().searchModeOn) t.false(wrapper.state().allNodesHidden) @@ -24,7 +24,7 @@ test('hides dropdown onChange for radioSelect', t => { test('keeps dropdown open onChange for radioSelect and keepOpenOnSelect', t => { const wrapper = mount( - + ) wrapper.instance().onCheckboxChange('nodeA', true) t.true(wrapper.state().showDropdown) diff --git a/src/tree/index.js b/src/tree/index.js index d8b53c82..71dc577f 100644 --- a/src/tree/index.js +++ b/src/tree/index.js @@ -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 } diff --git a/types/react-dropdown-tree-select.d.ts b/types/react-dropdown-tree-select.d.ts index ebe58afd..cae7f334 100644 --- a/types/react-dropdown-tree-select.d.ts +++ b/types/react-dropdown-tree-select.d.ts @@ -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 */ @@ -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 */ @@ -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 @@ -97,7 +98,7 @@ declare module 'react-dropdown-tree-select' { } export interface DropdownTreeSelectState { - showDropdown: boolean + showDropdown: ShowDropdownState searchModeOn: boolean allNodesHidden: boolean tree: TreeNode[]