Skip to content

Commit

Permalink
fix(blur): don't reset when tabbing between the input and button (#374)
Browse files Browse the repository at this point in the history
* WIP: test coverage issues with onBlur

* fixed existing jest tests, 100% coverage

* Update downshift.js

* Add data-toggle attribute to getButtonProps

* Edit downshiftButtonIsActive in downshift.js
  • Loading branch information
austintackaberry authored and Kent C. Dodds committed Mar 15, 2018
1 parent f6e838b commit 04635b2
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 25 deletions.
10 changes: 10 additions & 0 deletions cypress/integration/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,14 @@ describe('Basic', () => {
.getInStoryByTestId('basic-input')
.should('have.value', 'Red')
})

it('resets when tabbing from input to button', () => {
cy
.getInStoryByTestId('basic-input')
.type('re')
.getInStoryByTestId('clear-selection')
.focus()
.getInStoryByTestId('downshift-item-0')
.should('not.be.visible')
})
})
37 changes: 37 additions & 0 deletions cypress/integration/semantic-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
describe('Semantic-UI', () => {
before(() => {
cy.visitStory('semantic-ui')
})

beforeEach(() => {
cy.getInStoryByTestId('semantic-ui-input').type('{selectAll}{del}')
})

afterEach(() => {
cy.getInStoryByTestId('semantic-ui-clear-button').click()
})

it('does not reset when tabbing from input to button', () => {
cy
.getInStoryByTestId('semantic-ui-input')
.type('Alg')
.getInStoryByTestId('semantic-ui-toggle-button')
.focus()
.getInStoryByTestId('downshift-item-0')
.click()
.getInStoryByTestId('semantic-ui-input')
.should('have.value', 'Algeria')
})

it('does not reset when tabbing from button to input', () => {
cy
.getInStoryByTestId('semantic-ui-toggle-button')
.click()
.getInStoryByTestId('semantic-ui-input')
.focus()
.getInStoryByTestId('downshift-item-0')
.click()
.getInStoryByTestId('semantic-ui-input')
.should('have.value', 'Afghanistan')
})
})
4 changes: 4 additions & 0 deletions src/__tests__/downshift.get-button-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ test('button ignores key events it does not handle', () => {
expect(renderSpy).not.toHaveBeenCalled()
})

jest.useFakeTimers()

test('on button blur resets the state', () => {
const {button, renderSpy} = setup()
button.simulate('blur')
jest.runAllTimers()
expect(renderSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: false,
Expand All @@ -51,6 +54,7 @@ test('on button blur does not reset the state when the mouse is down', () => {
new window.MouseEvent('mousedown', {bubbles: true}),
)
button.simulate('blur')
jest.runAllTimers()
expect(renderSpy).not.toHaveBeenCalled()
})

Expand Down
53 changes: 35 additions & 18 deletions src/__tests__/downshift.get-input-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,12 @@ test('escape on an input with a selection should reset downshift and close the m
)
})

jest.useFakeTimers()

test('on input blur resets the state', () => {
const {input, renderSpy, items} = setupDownshiftWithState()
input.simulate('blur')
jest.runAllTimers()
expect(renderSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: false,
Expand All @@ -185,6 +188,16 @@ test('on input blur does not reset the state when the mouse is down', () => {
new window.MouseEvent('mousedown', {bubbles: true}),
)
input.simulate('blur')
jest.runAllTimers()
expect(renderSpy).not.toHaveBeenCalled()
})

test('on input blur does not reset the state when new focus is on downshift button', () => {
const {input, renderSpy, button} = setupDownshiftWithState()
const buttonNode = button.getDOMNode()
input.simulate('blur')
buttonNode.focus()
jest.runAllTimers()
expect(renderSpy).not.toHaveBeenCalled()
})

Expand Down Expand Up @@ -309,6 +322,7 @@ function setupDownshiftWithState() {
const {Component, renderSpy} = setup({items})
const wrapper = mount(<Component />)
const input = wrapper.find(sel('input'))
const button = wrapper.find(sel('button'))
input.simulate('keydown')
input.simulate('change', {target: {value: 'a'}})
// ↓
Expand All @@ -320,28 +334,31 @@ function setupDownshiftWithState() {
input.simulate('keydown', {key: 'ArrowDown'})
input.simulate('change', {target: {value: 'bu'}})
renderSpy.mockClear()
return {renderSpy, input, items, wrapper}
return {renderSpy, input, button, items, wrapper}
}

function setup({items = colors} = {}) {
/* eslint-disable react/jsx-closing-bracket-location */
const renderSpy = jest.fn(({isOpen, getInputProps, getItemProps}) => (
<div>
<input {...getInputProps({'data-test': 'input'})} />
{isOpen && (
<div>
{items.map((item, index) => (
<div
key={item.index || index}
{...getItemProps({item, index: item.index || index})}
>
{item.value ? item.value : item}
</div>
))}
</div>
)}
</div>
))
const renderSpy = jest.fn(
({isOpen, getInputProps, getButtonProps, getItemProps}) => (
<div>
<input {...getInputProps({'data-test': 'input'})} />
<button {...getButtonProps({'data-test': 'button'})} />
{isOpen && (
<div>
{items.map((item, index) => (
<div
key={item.index || index}
{...getItemProps({item, index: item.index || index})}
>
{item.value ? item.value : item}
</div>
))}
</div>
)}
</div>
),
)

function BasicDownshift(props) {
return <Downshift {...props} render={renderSpy} />
Expand Down
25 changes: 19 additions & 6 deletions src/downshift.js
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ class Downshift extends Component {
'aria-label': isOpen ? 'close menu' : 'open menu',
'aria-expanded': isOpen,
'aria-haspopup': true,
'data-toggle': true,
...eventHandlers,
...rest,
}
Expand All @@ -575,9 +576,15 @@ class Downshift extends Component {
}

button_handleBlur = () => {
if (!this.isMouseDown) {
this.reset({type: Downshift.stateChangeTypes.blurButton})
}
// Need setTimeout, so that when the user presses Tab, the activeElement is the next focused element, not body element
setTimeout(() => {
if (
!this.isMouseDown &&
this.props.environment.document.activeElement.id !== this.inputId
) {
this.reset({type: Downshift.stateChangeTypes.blurButton})
}
})
}

//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ BUTTON
Expand Down Expand Up @@ -679,9 +686,15 @@ class Downshift extends Component {
}

input_handleBlur = () => {
if (!this.isMouseDown) {
this.reset({type: Downshift.stateChangeTypes.blurInput})
}
// Need setTimeout, so that when the user presses Tab, the activeElement is the next focused element, not the body element
setTimeout(() => {
const downshiftButtonIsActive =
this.props.environment.document.activeElement.dataset.toggle &&
this._rootNode.contains(this.props.environment.document.activeElement)
if (!this.isMouseDown && !downshiftButtonIsActive) {
this.reset({type: Downshift.stateChangeTypes.blurInput})
}
})
}

//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ INPUT
Expand Down
9 changes: 8 additions & 1 deletion stories/examples/semantic-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,24 @@ function SemanticUIAutocomplete() {
{...getInputProps({
isOpen,
placeholder: 'Enter some info',
'data-test': 'semantic-ui-input',
})}
/>
{selectedItem ? (
<ControlButton
css={{paddingTop: 4}}
onClick={clearSelection}
aria-label="clear selection"
data-test="semantic-ui-clear-button"
>
<XIcon />
</ControlButton>
) : (
<ControlButton {...getButtonProps()}>
<ControlButton
{...getButtonProps({
'data-test': 'semantic-ui-toggle-button',
})}
>
<ArrowIcon isOpen={isOpen} />
</ControlButton>
)}
Expand All @@ -176,6 +182,7 @@ function SemanticUIAutocomplete() {
key={item.code}
{...getItemProps({
item,
'data-test': `downshift-item-${index}`,
isActive: highlightedIndex === index,
isSelected: selectedItem === item,
})}
Expand Down

0 comments on commit 04635b2

Please sign in to comment.