diff --git a/.gitignore b/.gitignore
index f5c581e6..359f5895 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,6 @@ dist
# Webstorm cache
.idea
/webpack-stats.json
+
+# Mac crap
+.DS_Store
diff --git a/README.md b/README.md
index 3df4b915..ea0f33af 100644
--- a/README.md
+++ b/README.md
@@ -29,8 +29,8 @@ A lightweight and fast control to render a select component that can display hie
## Table of Contents
- [Screenshot](#screenshot)
-- [Demo](#example)
- - [Vanilla (no framework)](#vanilla-no-framework)
+- [Demo](#demo)
+ - [Vanilla, no framework](#vanilla-no-framework)
- [With Bootstrap](#with-bootstrap)
- [With Material Design](#with-material-design)
- [As Single Select](#as-single-select)
@@ -44,36 +44,41 @@ A lightweight and fast control to render a select component that can display hie
- [clearSearchOnChange](#clearsearchonchange)
- [onChange](#onchange)
- [onNodeToggle](#onnodetoggle)
+ - [onAction](#onaction)
+ - [onFocus](#onfocus)
+ - [onBlur](#onblur)
- [data](#data)
- [texts](#texts)
- [keepTreeOnSearch](#keeptreeonsearch)
- [keepChildrenOnSearch](#keepchildrenonsearch)
- [keepOpenOnSelect](#keepopenonselect)
- [mode](#mode)
- - [multiSelect](#multiSelect)
+ - [multiSelect](#multiselect)
- [hierarchical](#hierarchical)
- - [simpleSelect](#simpleSelect)
- - [radioSelect](#radioSelect)
+ - [simpleSelect](#simpleselect)
+ - [radioSelect](#radioselect)
- [showPartiallySelected](#showpartiallyselected)
- - [showDropdown](#showDropdown)
+ - [showDropdown](#showdropdown)
- [initial](#initial)
- [always](#always)
- - [form states (disabled|readOnly)](#formstates)
+ - [form states (disabled|readOnly)](#form-states-disabled-readonly)
- [id](#id)
- [searchPredicate](#searchpredicate)
+ - [inlineSearchInput](#inlinesearchinput)
- [Styling and Customization](#styling-and-customization)
- [Using default styles](#default-styles)
- [Customizing with Bootstrap, Material Design styles](#customizing-styles)
+- [Keyboard navigation](#keyboard-navigation)
- [Performance](#performance)
- [Search optimizations](#search-optimizations)
- [Search debouncing](#search-debouncing)
- [Virtualized rendering](#virtualized-rendering)
- [Reducing costly DOM manipulations](#reducing-costly-dom-manipulations)
-- [Keyboard navigation](#keyboard-navigation)
- [FAQ](#faq)
- [Doing more with HOCs](/docs/HOC.md)
- [Development](#development)
- [License](#license)
+- [Contributors](#contributors)
## Screenshot
@@ -401,6 +406,12 @@ function searchPredicate(node, searchTerm) {
return
```
+### inlineSearchInput
+
+Type: `bool` (default: `false`)
+
+`inlineSearchInput=true` makes the search input renders **inside** the dropdown-content. This can be useful when your UX looks something like [this comment](https://github.com/dowjones/react-dropdown-tree-select/issues/308#issue-526467109).
+
## Styling and Customization
### Default styles
diff --git a/__snapshots__/src/index.test.js.md b/__snapshots__/src/index.test.js.md
index 37d5c713..3c7d255b 100644
--- a/__snapshots__/src/index.test.js.md
+++ b/__snapshots__/src/index.test.js.md
@@ -22,17 +22,22 @@ Generated by [AVA](https://ava.li).
tags={[]}
texts={{}}
>
-
+ >
+
+
-## doesn't toggle dropdown if it's disabled
+## always shows dropdown with inline search Input
> Snapshot 1
@@ -181,24 +186,198 @@ Generated by [AVA](https://ava.li).
>
+
+
+
+ {
+ _children: [
+ 'rdts-0-0',
+ 'rdts-0-1',
+ ],
+ _depth: 0,
+ _id: 'rdts-0',
+ actions: [
+ {
+ className: 'fa fa-ban',
+ id: 'NOT',
+ title: 'NOT',
+ },
+ ],
+ children: undefined,
+ label: 'item1',
+ value: 'value1',
+ },
+ 'rdts-0-0' => {
+ _children: [
+ 'rdts-0-0-0',
+ 'rdts-0-0-1',
+ ],
+ _depth: 1,
+ _id: 'rdts-0-0',
+ _parent: 'rdts-0',
+ children: undefined,
+ label: 'item1-1',
+ value: 'value1-1',
+ },
+ 'rdts-0-0-0' => {
+ _depth: 2,
+ _id: 'rdts-0-0-0',
+ _parent: 'rdts-0-0',
+ label: 'item1-1-1',
+ value: 'value1-1-1',
+ },
+ 'rdts-0-0-1' => {
+ _depth: 2,
+ _id: 'rdts-0-0-1',
+ _parent: 'rdts-0-0',
+ label: 'item1-1-2',
+ value: 'value1-1-2',
+ },
+ 'rdts-0-1' => {
+ _depth: 1,
+ _id: 'rdts-0-1',
+ _parent: 'rdts-0',
+ label: 'item1-2',
+ value: 'value1-2',
+ },
+ 'rdts-1' => {
+ _children: [
+ 'rdts-1-0',
+ 'rdts-1-1',
+ ],
+ _depth: 0,
+ _id: 'rdts-1',
+ children: undefined,
+ label: 'item2',
+ value: 'value2',
+ },
+ 'rdts-1-0' => {
+ _children: [
+ 'rdts-1-0-0',
+ 'rdts-1-0-1',
+ 'rdts-1-0-2',
+ ],
+ _depth: 1,
+ _id: 'rdts-1-0',
+ _parent: 'rdts-1',
+ children: undefined,
+ label: 'item2-1',
+ value: 'value2-1',
+ },
+ 'rdts-1-0-0' => {
+ _depth: 2,
+ _id: 'rdts-1-0-0',
+ _parent: 'rdts-1-0',
+ label: 'item2-1-1',
+ value: 'value2-1-1',
+ },
+ 'rdts-1-0-1' => {
+ _depth: 2,
+ _id: 'rdts-1-0-1',
+ _parent: 'rdts-1-0',
+ label: 'item2-1-2',
+ value: 'value2-1-2',
+ },
+ 'rdts-1-0-2' => {
+ _children: [
+ 'rdts-1-0-2-0',
+ ],
+ _depth: 2,
+ _id: 'rdts-1-0-2',
+ _parent: 'rdts-1-0',
+ children: undefined,
+ label: 'item2-1-3',
+ value: 'value2-1-3',
+ },
+ 'rdts-1-0-2-0' => {
+ _depth: 3,
+ _id: 'rdts-1-0-2-0',
+ _parent: 'rdts-1-0-2',
+ label: 'item2-1-3-1',
+ value: 'value2-1-3-1',
+ },
+ 'rdts-1-1' => {
+ _depth: 1,
+ _id: 'rdts-1-1',
+ _parent: 'rdts-1',
+ label: 'item2-2',
+ value: 'value2-2',
+ },
+ }
+ }
+ onAction={Function {}}
+ onCheckboxChange={Function {}}
+ onNodeToggle={Function {}}
+ pageSize={100}
+ searchModeOn={false}
+ texts={{}}
+ />
+
+
+
+
+## doesn't toggle dropdown if it's disabled
+
+> Snapshot 1
+
+
@@ -278,6 +457,7 @@ Generated by [AVA](https://ava.li).
]
}
id="rdts"
+ inlineSearchInput={false}
mode="radioSelect"
onBlur={Function onBlur {}}
onChange={Function onChange {}}
@@ -310,14 +490,9 @@ Generated by [AVA](https://ava.li).
role="button"
tabIndex={0}
>
-
-
+ texts={{}}
+ >
+
+
-
+
@@ -422,6 +608,7 @@ Generated by [AVA](https://ava.li).
]
}
id="rdts"
+ inlineSearchInput={false}
onBlur={Function onBlur {}}
onChange={Function onChange {}}
onFocus={Function onFocus {}}
@@ -452,13 +639,8 @@ Generated by [AVA](https://ava.li).
role="button"
tabIndex={0}
>
-
-
+ texts={{}}
+ >
+
+
-
+
@@ -506,17 +698,22 @@ Generated by [AVA](https://ava.li).
tags={[]}
texts={{}}
>
-
+ >
+
+
Snapshot 1
-
+
## renders placeholder
> Snapshot 1
-
-
-## renders tags
-
-> Snapshot 1
-
-
-
-## should render data attributes
-
-> Snapshot 1
-
-
+
## should render disabled input
> Snapshot 1
-
+
diff --git a/__snapshots__/src/input/index.test.js.snap b/__snapshots__/src/input/index.test.js.snap
index 48001613..81941268 100644
Binary files a/__snapshots__/src/input/index.test.js.snap and b/__snapshots__/src/input/index.test.js.snap differ
diff --git a/__snapshots__/src/tags/index.test.js.md b/__snapshots__/src/tags/index.test.js.md
new file mode 100644
index 00000000..93158c67
--- /dev/null
+++ b/__snapshots__/src/tags/index.test.js.md
@@ -0,0 +1,94 @@
+# Snapshot report for `src/tags/index.test.js`
+
+The actual snapshot is saved in `index.test.js.snap`.
+
+Generated by [AVA](https://ava.li).
+
+## renders tags
+
+> Snapshot 1
+
+
+
+## renders tags when no tags are passed
+
+> Snapshot 1
+
+
+
+## renders tags when no tags are passed nor Input
+
+> Snapshot 1
+
+
+
+## should render data attributes
+
+> Snapshot 1
+
+
diff --git a/__snapshots__/src/tags/index.test.js.snap b/__snapshots__/src/tags/index.test.js.snap
new file mode 100644
index 00000000..7a340dc4
Binary files /dev/null and b/__snapshots__/src/tags/index.test.js.snap differ
diff --git a/docs/src/stories/Options/index.js b/docs/src/stories/Options/index.js
index 459dd9ca..b6536767 100644
--- a/docs/src/stories/Options/index.js
+++ b/docs/src/stories/Options/index.js
@@ -15,6 +15,7 @@ class WithOptions extends PureComponent {
keepTreeOnSearch: false,
keepOpenOnSelect: false,
mode: 'multiSelect',
+ inlineSearchInput: false,
showPartiallySelected: false,
disabled: false,
readOnly: false,
@@ -46,6 +47,7 @@ class WithOptions extends PureComponent {
disabled,
readOnly,
showDropdown,
+ inlineSearchInput,
} = this.state
return (
@@ -81,6 +83,12 @@ class WithOptions extends PureComponent {
+
diff --git a/package.json b/package.json
index e5384126..2daeb90d 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,6 @@
],
"dependencies": {
"array.partial": "^1.0.5",
- "classnames": "^2.2.6",
"react-infinite-scroll-component": "^4.0.2"
},
"devDependencies": {
diff --git a/src/index.css b/src/index.css
index 665e3a57..1f65a511 100644
--- a/src/index.css
+++ b/src/index.css
@@ -51,6 +51,13 @@
border-top: rgba(0, 0, 0, 0.05) 1px solid;
box-shadow: 0 5px 8px rgba(0, 0, 0, 0.15);
+ .search {
+ width: 100%;
+ border: none;
+ border-bottom: solid 1px #ccc;
+ outline: none;
+ }
+
ul {
margin: 0;
padding: 0;
diff --git a/src/index.js b/src/index.js
index 65866816..28a9c279 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,22 +6,20 @@
* license MIT
* see https://github.com/dowjones/react-dropdown-tree-select
*/
-import cn from 'classnames/bind'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { isOutsideClick, clientIdGenerator } from './utils'
import Input from './input'
+import Tags from './tags'
import Trigger from './trigger'
import Tree from './tree'
import TreeManager from './tree-manager'
import keyboardNavigation from './tree-manager/keyboardNavigation'
-import styles from './index.css'
+import './index.css'
import { getAriaLabel } from './a11y'
-const cx = cn.bind(styles)
-
class DropdownTreeSelect extends Component {
static propTypes = {
data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
@@ -48,6 +46,7 @@ class DropdownTreeSelect extends Component {
readOnly: PropTypes.bool,
id: PropTypes.string,
searchPredicate: PropTypes.func,
+ inlineSearchInput: PropTypes.bool,
}
static defaultProps = {
@@ -56,6 +55,7 @@ class DropdownTreeSelect extends Component {
onChange: () => {},
texts: {},
showDropdown: 'default',
+ inlineSearchInput: false,
}
constructor(props) {
@@ -276,44 +276,48 @@ class DropdownTreeSelect extends Component {
}
render() {
- const { disabled, readOnly, mode, texts } = this.props
+ const { disabled, readOnly, mode, texts, inlineSearchInput } = this.props
const { showDropdown, currentFocus, tags } = this.state
const activeDescendant = currentFocus ? `${currentFocus}_li` : undefined
const commonProps = { disabled, readOnly, activeDescendant, texts, mode, clientId: this.clientId }
+ const searchInput = (
+ {
+ this.searchInput = el
+ }}
+ onInputChange={this.onInputChange}
+ onFocus={this.onInputFocus}
+ onBlur={this.onInputBlur}
+ onKeyDown={this.onKeyboardKeyDown}
+ {...commonProps}
+ />
+ )
return (
{
this.node = node
}}
>
- {
- this.searchInput = el
- }}
- tags={tags}
- onInputChange={this.onInputChange}
- onFocus={this.onInputFocus}
- onBlur={this.onInputBlur}
- onTagRemove={this.onTagRemove}
- onKeyDown={this.onKeyboardKeyDown}
- {...commonProps}
- />
+
+ {!inlineSearchInput && searchInput}
+
{showDropdown && (
+ {inlineSearchInput && searchInput}
{this.state.allNodesHidden ? (
{texts.noMatches || 'No matches found'}
) : (
diff --git a/src/index.test.js b/src/index.test.js
index 284e0fe0..adcdd1e9 100644
--- a/src/index.test.js
+++ b/src/index.test.js
@@ -86,6 +86,12 @@ test('always shows dropdown', t => {
t.snapshot(toJson(wrapper))
})
+test('always shows dropdown with inline search Input', t => {
+ const { tree } = t.context
+ const wrapper = shallow(
)
+ t.snapshot(toJson(wrapper))
+})
+
test('keeps dropdown open for showDropdown: always', t => {
const { tree } = t.context
const wrapper = mount(
)
diff --git a/src/input/index.js b/src/input/index.js
index 781e99f4..e331f946 100644
--- a/src/input/index.js
+++ b/src/input/index.js
@@ -1,30 +1,8 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
-import cn from 'classnames/bind'
-import Tag from '../tag'
-import styles from './index.css'
-import { getDataset, debounce } from '../utils'
+import { debounce } from '../utils'
import { getAriaLabel } from '../a11y'
-const cx = cn.bind(styles)
-
-const getTags = (tags = [], onDelete, readOnly, disabled, labelRemove) =>
- tags.map(tag => {
- const { _id, label, tagClassName, dataset } = tag
- return (
-
-
-
- )
- })
-
class Input extends PureComponent {
static propTypes = {
tags: PropTypes.array,
@@ -38,6 +16,7 @@ class Input extends PureComponent {
disabled: PropTypes.bool,
readOnly: PropTypes.bool,
activeDescendant: PropTypes.string,
+ inlineSearchInput: PropTypes.bool,
}
constructor(props) {
@@ -51,40 +30,24 @@ class Input extends PureComponent {
}
render() {
- const {
- tags,
- onTagRemove,
- inputRef,
- texts = {},
- onFocus,
- onBlur,
- disabled,
- readOnly,
- onKeyDown,
- activeDescendant,
- } = this.props
+ const { inputRef, texts = {}, onFocus, onBlur, disabled, readOnly, onKeyDown, activeDescendant } = this.props
return (
-
+
)
}
}
diff --git a/src/input/index.test.js b/src/input/index.test.js
index f02d7417..66c56c05 100644
--- a/src/input/index.test.js
+++ b/src/input/index.test.js
@@ -6,13 +6,7 @@ import toJson from 'enzyme-to-json'
import Input from './index'
-test('renders tags', t => {
- const tags = [{ _id: 'i1', label: 'l1' }, { _id: 'i2', label: 'l2' }]
- const wrapper = toJson(shallow(
))
- t.snapshot(wrapper)
-})
-
-test('renders input when no tags are passed', t => {
+test('renders input', t => {
const wrapper = toJson(shallow(
))
t.snapshot(wrapper)
})
@@ -30,24 +24,6 @@ test('raises onchange', t => {
t.true(onChange.calledWith('hello'))
})
-test('should render data attributes', t => {
- const tags = [
- {
- _id: 'i1',
- label: 'l1',
- tagClassName: 'test',
- dataset: {
- first: 'john',
- last: 'smith',
- },
- },
- ]
-
- const wrapper = toJson(shallow(
))
-
- t.snapshot(wrapper)
-})
-
test('should render disabled input', t => {
const wrapper = toJson(shallow(
))
t.snapshot(wrapper)
diff --git a/src/tag/index.js b/src/tag/index.js
index 86d62555..a0e9ce44 100644
--- a/src/tag/index.js
+++ b/src/tag/index.js
@@ -1,10 +1,7 @@
-import cn from 'classnames/bind'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
-import styles from './index.css'
-
-const cx = cn.bind(styles)
+import './index.css'
export const getTagId = id => `${id}_tag`
@@ -44,11 +41,11 @@ class Tag extends PureComponent {
const tagId = getTagId(id)
const buttonId = `${id}_button`
- const className = cx('tag-remove', { readOnly }, { disabled })
+ const className = ['tag-remove', readOnly && 'readOnly', disabled && 'disabled'].filter(Boolean).join(' ')
const isDisabled = readOnly || disabled
return (
-
+
{label}