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

feat: Add new props for search, keyboard navigation, and scrolling #589

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ A lightweight and fast control to render a select component that can display hie
- [Usage](#usage)
- [Props](#props)
- [className](#classname)
- [searchTerm](#searchterm)
- [clearSearchOnChange](#clearsearchonchange)
- [onChange](#onchange)
- [onNodeToggle](#onnodetoggle)
- [onAction](#onaction)
- [onFocus](#onfocus)
- [onBlur](#onblur)
- [onSearchChange](#onsearchchange)
- [data](#data)
- [texts](#texts)
- [keepTreeOnSearch](#keeptreeonsearch)
Expand All @@ -57,6 +59,7 @@ A lightweight and fast control to render a select component that can display hie
- [hierarchical](#hierarchical)
- [simpleSelect](#simpleselect)
- [radioSelect](#radioselect)
- [pageSize](#pagesize)
- [showPartiallySelected](#showpartiallyselected)
- [showDropdown](#showdropdown)
- [initial](#initial)
Expand All @@ -66,7 +69,8 @@ A lightweight and fast control to render a select component that can display hie
- [searchPredicate](#searchpredicate)
- [inlineSearchInput](#inlinesearchinput)
- [tabIndex](#tabIndex)
- [disablePoppingOnBackspace](#disablePoppingOnBackspace)
- [disablePoppingOnBackspace](#disablepoppingonbackspace)
- [disableKeyboardNavigation](#disablekeyboardnavigation)
- [Styling and Customization](#styling-and-customization)
- [Using default styles](#default-styles)
- [Customizing with Bootstrap, Material Design styles](#customizing-styles)
Expand Down Expand Up @@ -188,6 +192,12 @@ Type: `string`

Additional classname for container. The container renders with a default classname of `react-dropdown-tree-select`.

### searchTerm

Type: `string`

Initializes or adjusts the active search term. Set to an empty string or `undefined` to turn search mode off.

### clearSearchOnChange

Type: `bool`
Expand Down Expand Up @@ -256,6 +266,24 @@ Type: `function`

Fires when input box loses focus or the dropdown arrow is clicked again (and the dropdown collapses). This is helpful for setting `dirty` or `touched` flags with forms.

### onSearchChange

Type: `function`

Called when the search input box is changed with the current search term. This can be fired either through user input or automatically due to `clearSearchOnChange`. Example:

```jsx
function onSearchChange(searchTerm: str) {
if (searchTerm) {
console.log('New search term is', searchTerm)
} else {
console.log('Search mode has been disabled')
}
}

return <DropdownTreeSelect data={data} onSearchChange={onSearchChange} />
```

### data

Type: `Object` or `Array`
Expand Down Expand Up @@ -361,6 +389,12 @@ Like `simpleSelect`, you can only select one value; but keeps the tree/children

⚠️ If multiple nodes in data are selected - by setting either `checked` or `isDefaultValue`, only the first visited node stays selected.

### pageSize

Type: `number` (default: `100`)

Customize the number of nodes displayed in the tree before a scroll to near the bottom is required to load additional nodes.

### showPartiallySelected

Type: `bool` (default: `false`)
Expand Down Expand Up @@ -428,6 +462,12 @@ Type: `bool` (default: `false`)

`disablePoppingOnBackspace=true` attribute indicates that when a user triggers a 'backspace' keyDown in the empty search bar, the tree will not deselect nodes.

### disableKeyboardNavigation

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

`disableKeyboardNavigation=true` prevents keyboard navigation actions from being taken on the nodes when the user triggers a keyDown in the search bar. This restores standard input box semantics.

## Styling and Customization

### Default styles
Expand Down
31 changes: 31 additions & 0 deletions docs/src/stories/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ class WithOptions extends PureComponent {
super(props)

this.state = {
searchTerm: '',
clearSearchOnChange: false,
keepTreeOnSearch: false,
keepOpenOnSelect: false,
mode: 'multiSelect',
pageSize: undefined,
inlineSearchInput: false,
showPartiallySelected: false,
disabled: false,
Expand All @@ -39,12 +41,18 @@ class WithOptions extends PureComponent {
this.setState({ [value]: !this.state[value] })
}

onSearchChange = searchTerm => {
this.setState({ searchTerm: searchTerm })
}

render() {
const {
searchTerm,
clearSearchOnChange,
keepTreeOnSearch,
keepOpenOnSelect,
mode,
pageSize,
showPartiallySelected,
disabled,
readOnly,
Expand Down Expand Up @@ -105,6 +113,26 @@ class WithOptions extends PureComponent {
onChange={e => this.setState({ inlineSearchPlaceholder: e.target.value })}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<label htmlFor="searchTerm">Search term: </label>
<input
id="searchTerm"
type="text"
value={searchTerm}
onChange={e => this.setState({ searchTerm: e.target.value })}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<label htmlFor="pageSize">Page size ({pageSize || 100}): </label>
<input
id="pageSize"
type="range"
min={50}
max={1000}
value={pageSize || 100}
onChange={e => this.setState({ pageSize: e.target.valueAsNumber || undefined })}
/>
</div>
<Checkbox
label="Inline Search Input"
value="inlineSearchInput"
Expand Down Expand Up @@ -142,13 +170,16 @@ class WithOptions extends PureComponent {
<DropdownTreeSelect
id="rdts"
data={data}
searchTerm={searchTerm}
onChange={this.onChange}
onAction={this.onAction}
onNodeToggle={this.onNodeToggle}
onSearchChange={this.onSearchChange}
clearSearchOnChange={clearSearchOnChange}
keepTreeOnSearch={keepTreeOnSearch}
keepOpenOnSelect={keepOpenOnSelect}
mode={mode}
pageSize={pageSize}
showPartiallySelected={showPartiallySelected}
disabled={disabled}
readOnly={readOnly}
Expand Down
5 changes: 5 additions & 0 deletions docs/src/stories/Simple/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const onBlur = () => {
console.log('onBlur')
}

const onSearchChange = searchTerm => {
console.log('onSearchChange::', searchTerm)
}

const Simple = () => (
<div>
<h1>Basic component</h1>
Expand All @@ -45,6 +49,7 @@ const Simple = () => (
onNodeToggle={onNodeToggle}
onFocus={onFocus}
onBlur={onBlur}
onSearchChange={onSearchChange}
className="demo"
/>
</div>
Expand Down
24 changes: 22 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getAriaLabel } from './a11y'
class DropdownTreeSelect extends Component {
static propTypes = {
data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
searchTerm: PropTypes.string,
clearSearchOnChange: PropTypes.bool,
keepTreeOnSearch: PropTypes.bool,
keepChildrenOnSearch: PropTypes.bool,
Expand All @@ -41,7 +42,9 @@ class DropdownTreeSelect extends Component {
onNodeToggle: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onSearchChange: PropTypes.func,
mode: PropTypes.oneOf(['multiSelect', 'simpleSelect', 'radioSelect', 'hierarchical']),
pageSize: PropTypes.number,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gandhis1 I did think about it during the infinite scroll implementation but ultimately decided against it. Reasons being

  • an arbitrary pageSize doesn't help with the overall intent of optimization - e.g, setting a lower number is actually counter productive and setting a higher number will slow down the rendering. This gets exacerbated if you have multiple dropdowns on the page. Furthermore, unless you've a really big screen, you can't see more than 100 anyway so why hold it in DOM?

showPartiallySelected: PropTypes.bool,
disabled: PropTypes.bool,
readOnly: PropTypes.bool,
Expand All @@ -50,18 +53,21 @@ class DropdownTreeSelect extends Component {
inlineSearchInput: PropTypes.bool,
tabIndex: PropTypes.number,
disablePoppingOnBackspace: PropTypes.bool,
disableKeyboardNavigation: PropTypes.bool,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case for this? At a glance it seems like an a11y blocker but people don't have to use the keyboard if they don't want to so curios as to why this is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my PR description above:

The new prop disableKeyboardNavigation allows disabling all actions associated with a key down event in the search input box. This is important if you need an input box to behave normally. For instance, when I press SHIFT+HOME, it should select the entire line, so I can press BACKSPACE to clear it.

I can't use arrow keys either on the search box right now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an input box to behave normally

I hear you but it's not really a regular input box - hence the special treatment.

when I press SHIFT+HOME, it should select the entire line, so I can press BACKSPACE to clear it.

Can this handled by adding additional functionality to the keyboard navigation logic? So that we don't have to choose between having it or not at all?

}

static defaultProps = {
onAction: () => {},
onFocus: () => {},
onBlur: () => {},
onChange: () => {},
onSearchChange: (_) => {},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an incompatibility between the prettier pre-commit hook and the codeclimate checks on GitHub with regard to this line. One tool insists on parentheses being present, the other insists it it on being absent. Is there any way we can use the same linter on both so that one tool does not conflict with the other like this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure. Will have to check with CC docs to see if they support a lint rc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can ignore the warning manually in CC dashboard as a last resort

texts: {},
showDropdown: 'default',
inlineSearchInput: false,
tabIndex: 0,
disablePoppingOnBackspace: false,
disableKeyboardNavigation: true,
}

constructor(props) {
Expand All @@ -73,7 +79,7 @@ class DropdownTreeSelect extends Component {
this.clientId = props.id || clientIdGenerator.get(this)
}

initNewProps = ({ data, mode, showDropdown, showPartiallySelected, searchPredicate }) => {
initNewProps = ({ data, searchTerm, mode, showDropdown, showPartiallySelected, searchPredicate }) => {
this.treeManager = new TreeManager({
data,
mode,
Expand All @@ -86,7 +92,12 @@ class DropdownTreeSelect extends Component {
if (currentFocusNode) {
currentFocusNode._focused = true
}
const searchTermChanged = searchTerm !== prevState.searchTerm
if (this.searchInput && searchTermChanged) {
this.searchInput.value = searchTerm
}
return {
searchModeOn: searchTermChanged,
showDropdown: /initial|always/.test(showDropdown) || prevState.showDropdown === true,
...this.treeManager.getTreeAndTags(),
}
Expand All @@ -96,7 +107,10 @@ class DropdownTreeSelect extends Component {
resetSearchState = () => {
// clear the search criteria and avoid react controlled/uncontrolled warning
if (this.searchInput) {
this.searchInput.value = ''
if (this.searchInput.value !== '') {
this.props.onSearchChange(this.searchInput.value)
this.searchInput.value = ''
}
}

return {
Expand Down Expand Up @@ -154,6 +168,7 @@ class DropdownTreeSelect extends Component {
this.props.keepChildrenOnSearch
)
const searchModeOn = value.length > 0
this.props.onSearchChange(value)

this.setState({
tree,
Expand Down Expand Up @@ -238,6 +253,10 @@ class DropdownTreeSelect extends Component {
}

onKeyboardKeyDown = e => {
if (this.props.disableKeyboardNavigation) {
return // Will fire the default action
}

const { readOnly, mode, disablePoppingOnBackspace } = this.props
const { showDropdown, tags, searchModeOn, currentFocus } = this.state
const tm = this.treeManager
Expand Down Expand Up @@ -360,6 +379,7 @@ class DropdownTreeSelect extends Component {
onCheckboxChange={this.onCheckboxChange}
onNodeToggle={this.onNodeToggle}
mode={mode}
pageSize={this.props.pageSize}
showPartiallySelected={this.props.showPartiallySelected}
{...commonProps}
/>
Expand Down
9 changes: 9 additions & 0 deletions types/react-dropdown-tree-select.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ declare module 'react-dropdown-tree-select' {

export interface DropdownTreeSelectProps {
data: TreeData
/** Initialize the search input with the specified term and search the nodes */
searchTerm?: str
/** Clear the input search if a node has been selected/unselected */
clearSearchOnChange?: boolean
/** Displays search results as a tree instead of flattened results */
Expand Down Expand Up @@ -54,6 +56,9 @@ declare module 'react-dropdown-tree-select' {
* This is helpful for setting dirty or touched flags with forms
*/
onBlur?: () => void
/** Fires when search input is modified.
*/
onSearchChange?: (searchTerm: str) => void
/** Defines how the dropdown is rendered / behaves
*
* - multiSelect
Expand All @@ -79,6 +84,8 @@ declare module 'react-dropdown-tree-select' {
*
* */
mode?: Mode
/** The size (in nodes) of a single page in the infinite scroll component. */
pageSize?: number
/** 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 @@ -103,6 +110,8 @@ declare module 'react-dropdown-tree-select' {
* search bar, the tree will not deselect nodes.
*/
disablePoppingOnBackspace?: boolean
/** dsiableKeyboardNavigation will disable keyboard navigation of the tree */
dsiableKeyboardNavigation?: boolean
Comment on lines +113 to +114
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** dsiableKeyboardNavigation will disable keyboard navigation of the tree */
dsiableKeyboardNavigation?: boolean
/** disableKeyboardNavigation will disable keyboard navigation of the tree */
disableKeyboardNavigation?: boolean

}

export interface DropdownTreeSelectState {
Expand Down