Skip to content
This repository has been archived by the owner on Dec 23, 2022. It is now read-only.

Add delayBeforeAdd option #263

Merged
merged 4 commits into from
Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import ChipInput from 'material-ui-chip-input'
|dataSource|`array`||Data source for auto complete. This should be an array of strings or objects.|
|dataSourceConfig|`shape`||Config for objects list dataSource, e.g. `{ text: 'text', value: 'value' }`. If not specified, the `dataSource` must be a flat array of strings or a custom `chipRenderer` must be set to handle the objects.|
|defaultValue|`array`||The chips to display by default (for uncontrolled mode).|
|delayBeforeAdd|`bool`|`false`|Use `setTimeout` 150ms delay before adding chips in case other input callbacks like `onSelection` need to fire first.|
|disabled|`bool`||Disables the chip input if set to true.|
|FormHelperTextProps|`object`||Props to pass through to the `FormHelperText` component.|
|fullWidth|`bool`||If true, the chip input will fill the available width.|
Expand Down
150 changes: 90 additions & 60 deletions src/ChipInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ const styles = (theme) => {
}
}

const keyCodes = {
BACKSPACE: 8,
DELETE: 46,
LEFT_ARROW: 37,
RIGHT_ARROW: 39
}

class ChipInput extends React.Component {
state = {
chips: [],
Expand Down Expand Up @@ -223,22 +230,29 @@ class ChipInput extends React.Component {
if (this.state.focusedChip != null) {
this.setState({ focusedChip: null })
}
if (this.props.blurBehavior === 'add') {
// Lets assume that we only want to add the existing content as chip, when
// another event has not added a chip within 200ms .
// e.g. onSelection Callback in Autocomplete case
let numChipsBefore = (this.props.value || this.state.chips).length
let value = event.target.value
this.inputBlurTimeout = setTimeout(() => {
let numChipsAfter = (this.props.value || this.state.chips).length
if (numChipsBefore === numChipsAfter) {
this.handleAddChip(value)
const value = event.target.value
switch (this.props.blurBehavior) {
case 'add':
if (this.props.delayBeforeAdd) {
// Lets assume that we only want to add the existing content as chip, when
// another event has not added a chip within 200ms .
// e.g. onSelection Callback in Autocomplete case
let numChipsBefore = (this.props.value || this.state.chips).length
this.inputBlurTimeout = setTimeout(() => {
let numChipsAfter = (this.props.value || this.state.chips).length
if (numChipsBefore === numChipsAfter) {
this.handleAddChip(value)
} else {
this.clearInput()
}
}, 150)
} else {
this.clearInput()
this.handleAddChip(value)
}
}, 150)
} else if (this.props.blurBehavior === 'clear') {
this.clearInput()
break
case 'clear':
this.clearInput()
break
}
}

Expand All @@ -262,53 +276,61 @@ class ChipInput extends React.Component {
return
}
}

const chips = this.props.value || this.state.chips
if (this.props.newChipKeyCodes.indexOf(event.keyCode) >= 0) {
let result = this.handleAddChip(event.target.value)
const result = this.handleAddChip(event.target.value)
if (result !== false) {
event.preventDefault()
}
} else if (event.keyCode === 8 || event.keyCode === 46) {
if (event.target.value === '') {
const chips = this.props.value || this.state.chips
if (focusedChip == null && event.keyCode === 8) {
this.setState({ focusedChip: chips.length - 1 })
} else if (focusedChip != null) {
const chips = this.props.value || this.state.chips
const value = chips[focusedChip]
this.handleDeleteChip(value, focusedChip)
if (event.keyCode === 8 && focusedChip > 0) {
this.setState({ focusedChip: focusedChip - 1 })
} else if (event.keyCode === 46 && focusedChip <= chips.length - 1) {
return
}

switch (event.keyCode) {
case keyCodes.BACKSPACE:
if (event.target.value === '') {
if (focusedChip != null) {
this.handleDeleteChip(chips[focusedChip], focusedChip)
if (focusedChip > 0) {
this.setState({ focusedChip: focusedChip - 1 })
}
} else {
this.setState({ focusedChip: chips.length - 1 })
}
}
break
case keyCodes.DELETE:
if (event.target.value === '' && focusedChip != null) {
this.handleDeleteChip(chips[focusedChip], focusedChip)
if (focusedChip <= chips.length - 1) {
this.setState({ focusedChip })
}
}
}
} else if (event.keyCode === 37) {
const chips = this.props.value || this.state.chips
if (focusedChip == null && event.target.value === '' && chips.length) {
return this.setState({ focusedChip: chips.length - 1 })
}
if (focusedChip != null && focusedChip > 0) {
this.setState({ focusedChip: focusedChip - 1 })
}
} else if (event.keyCode === 39) {
const chips = this.props.value || this.state.chips
if (focusedChip != null && focusedChip < chips.length - 1) {
this.setState({ focusedChip: focusedChip + 1 })
} else {
break
case keyCodes.LEFT_ARROW:
if (focusedChip == null && event.target.value === '' && chips.length) {
this.setState({ focusedChip: chips.length - 1 })
} else if (focusedChip != null && focusedChip > 0) {
this.setState({ focusedChip: focusedChip - 1 })
}
break
case keyCodes.RIGHT_ARROW:
if (focusedChip != null && focusedChip < chips.length - 1) {
this.setState({ focusedChip: focusedChip + 1 })
} else {
this.setState({ focusedChip: null })
}
break
default:
this.setState({ focusedChip: null })
}
} else {
this.setState({ focusedChip: null })
break
}
}

handleKeyUp = (event) => {
if (!this._preventChipCreation && this.props.newChipKeyCodes.indexOf(event.keyCode) > 0 && this._keyPressed) {
this.clearInput()
} else {
this.setState({ inputValue: event.target.value })
this.updateInput(event.target.value)
}
if (this.props.onKeyUp) { this.props.onKeyUp(event) }
}
Expand All @@ -320,7 +342,7 @@ class ChipInput extends React.Component {

handleUpdateInput = (e) => {
if (this.props.inputValue == null) {
this.setState({ inputValue: e.target.value })
this.updateInput(e.target.value)
}

if (this.props.onUpdateInput) {
Expand All @@ -338,7 +360,7 @@ class ChipInput extends React.Component {
this._preventChipCreation = true
return false
}
this.setState({ inputValue: '' })
this.clearInput()
const chips = this.props.value || this.state.chips

if (this.props.dataSourceConfig) {
Expand All @@ -356,26 +378,24 @@ class ChipInput extends React.Component {
this.updateChips([ ...this.state.chips, chip ])
}
}
} else if (chip.trim().length > 0) {
return true
}

if (chip.trim().length > 0) {
if (this.props.allowDuplicates || chips.indexOf(chip) === -1) {
if (this.props.value && this.props.onAdd) {
this.props.onAdd(chip)
} else {
this.updateChips([ ...this.state.chips, chip ])
}
}
} else {
return false
return true
}
return true
return false
}

handleDeleteChip (chip, i) {
if (this.props.value) {
if (this.props.onDelete) {
this.props.onDelete(chip, i)
}
} else {
if (!this.props.value) {
const chips = this.state.chips.slice()
const changed = chips.splice(i, 1) // remove the chip at index i
if (changed) {
Expand All @@ -387,6 +407,8 @@ class ChipInput extends React.Component {
}
this.updateChips(chips, { focusedChip })
}
} else if (this.props.onDelete) {
this.props.onDelete(chip, i)
}
}

Expand All @@ -403,7 +425,11 @@ class ChipInput extends React.Component {
* @public
*/
clearInput () {
this.setState({ inputValue: '' })
this.updateInput('')
}

updateInput (value) {
this.setState({ inputValue: value })
}

/**
Expand All @@ -427,9 +453,10 @@ class ChipInput extends React.Component {
classes,
className,
clearInputValueOnChange,
defaultValue,
dataSource,
dataSourceConfig,
defaultValue,
delayBeforeAdd,
disabled,
disableUnderline,
error,
Expand Down Expand Up @@ -521,7 +548,7 @@ class ChipInput extends React.Component {
{label && (
<InputLabel
htmlFor={id}
classes={{root: cx(classes[variant], classes.label), shrink: classes.labelShrink}}
classes={{ root: cx(classes[variant], classes.label), shrink: classes.labelShrink }}
shrink={shrinkFloatingLabel}
focused={this.state.isFocused}
variant={variant}
Expand Down Expand Up @@ -600,6 +627,8 @@ ChipInput.propTypes = {
}),
/** The chips to display by default (for uncontrolled mode). */
defaultValue: PropTypes.array,
/** Whether to use `setTimeout` to delay adding chips in case other input events like `onSelection` need to fire first */
delayBeforeAdd: PropTypes.bool,
/** Disables the chip input if set to true. */
disabled: PropTypes.bool,
/** Disable the input underline. Only valid for 'standard' variant */
Expand Down Expand Up @@ -646,6 +675,7 @@ ChipInput.defaultProps = {
allowDuplicates: false,
blurBehavior: 'clear',
clearInputValueOnChange: false,
delayBeforeAdd: false,
disableUnderline: false,
newChipKeyCodes: [13],
variant: 'standard'
Expand Down
32 changes: 26 additions & 6 deletions src/ChipInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ describe('uncontrolled mode', () => {
tree.find('input').getDOMNode().value = 'foo'
tree.find('input').simulate('change', { target: tree.find('input').getDOMNode() })
expect(handleUpdateInput).toBeCalledWith(
expect.objectContaining({
target: expect.anything()
})
expect.objectContaining({
target: expect.anything()
})
)
})
})
Expand Down Expand Up @@ -392,11 +392,11 @@ describe('blurBehavior modes', () => {
expect(tree.find('input').getDOMNode().value).toBe('foo')
})

it('adds the input on blur with blurBehavior set to add', () => {
it('adds the input on blur with blurBehavior set to add with timeout', () => {
const handleChange = jest.fn()
jest.useFakeTimers()
const tree = mount(
<ChipInput defaultValue={['a', 'b']} blurBehavior='add' onChange={handleChange} />
<ChipInput defaultValue={['a', 'b']} blurBehavior='add' delayBeforeAdd onChange={handleChange} />
)
tree.find('input').getDOMNode().value = 'blur'
tree.find('input').simulate('blur')
Expand All @@ -411,6 +411,26 @@ describe('blurBehavior modes', () => {
tree.update()
expect(tree.find('Chip').map((chip) => chip.text())).toEqual(['a', 'b', 'blur'])
})

it('adds the input on blur with blurBehavior set to add without timeout', () => {
const handleChange = jest.fn()
jest.useFakeTimers()
const tree = mount(
<ChipInput defaultValue={['a', 'b']} blurBehavior='add' onChange={handleChange} />
)
tree.find('input').getDOMNode().value = 'blur'
tree.find('input').simulate('blur')

jest.runAllTimers()

expect(tree.find('input').getDOMNode().value).toBe('')
expect(setTimeout).toHaveBeenCalledTimes(0)

expect(handleChange.mock.calls[0][0]).toEqual(['a', 'b', 'blur'])

tree.update()
expect(tree.find('Chip').map((chip) => chip.text())).toEqual(['a', 'b', 'blur'])
})
})

describe('keys', () => {
Expand Down Expand Up @@ -500,7 +520,7 @@ describe('keys', () => {
metaKey: false,
ctrlKey: false,
altKey: false,
target: {value: 'non-empty'},
target: { value: 'non-empty' },
preventDefault: () => {
prevented = true
}
Expand Down
1 change: 1 addition & 0 deletions src/__snapshots__/ChipInput.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ exports[`uncontrolled mode matches the snapshot 1`] = `
"bar",
]
}
delayBeforeAdd={false}
disableUnderline={false}
newChipKeyCodes={
Array [
Expand Down
12 changes: 6 additions & 6 deletions stories/examples/react-autosuggest.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function renderInput (inputProps) {
value={chips}
inputRef={ref}
{...other}
/>
/>
)
}

Expand Down Expand Up @@ -183,7 +183,7 @@ class ReactAutosuggestExample extends React.Component {
handleDeleteChip (chip, index) {
let temp = this.state.value
temp.splice(index, 1)
this.setState({value: temp})
this.setState({ value: temp })
}

render () {
Expand All @@ -204,7 +204,7 @@ class ReactAutosuggestExample extends React.Component {
renderSuggestionsContainer={renderSuggestionsContainer}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
onSuggestionSelected={(e, {suggestionValue}) => { this.handleAddChip(suggestionValue); e.preventDefault() }}
onSuggestionSelected={(e, { suggestionValue }) => { this.handleAddChip(suggestionValue); e.preventDefault() }}
focusInputOnSuggestionClick={false}
inputProps={{
classes,
Expand Down Expand Up @@ -259,12 +259,12 @@ class ReactAutosuggestRemoteExample extends React.Component {
};

handleAddChip (chip) {
this.setState({value: this.state.value.concat([chip])})
this.setState({ value: this.state.value.concat([chip]) })
}
handleDeleteChip (chip, index) {
let temp = this.state.value
temp.splice(index, 1)
this.setState({value: temp})
this.setState({ value: temp })
}

render () {
Expand All @@ -285,7 +285,7 @@ class ReactAutosuggestRemoteExample extends React.Component {
renderSuggestionsContainer={renderSuggestionsContainer}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
onSuggestionSelected={(e, {suggestionValue}) => { this.handleAddChip(suggestionValue); e.preventDefault() }}
onSuggestionSelected={(e, { suggestionValue }) => { this.handleAddChip(suggestionValue); e.preventDefault() }}
focusInputOnSuggestionClick={false}
inputProps={{
classes,
Expand Down