Skip to content

Commit 70bdd04

Browse files
ellingemrchief
authored andcommitted
fix: Avoid jarring on paging (#259)
- Avoids changing scroll if the current active descendant is the same for the tree (on prop updates on tree). - Also fixes jarring issue when selecting nodes on paging (index > 100/pagesize). - Partially fixes #257
1 parent 655c45a commit 70bdd04

File tree

3 files changed

+86
-27
lines changed

3 files changed

+86
-27
lines changed

src/index.keyboardNav.test.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import test from 'ava'
22
import React from 'react'
3-
import { spy } from 'sinon'
3+
import { spy, stub } from 'sinon'
44
import { mount } from 'enzyme'
55
import DropdownTreeSelect from './index'
66

@@ -161,3 +161,80 @@ test('should set current focus as selected on tab out for simpleSelect', t => {
161161
triggerOnKeyboardKeyDown(wrapper, ['ArrowDown', 'ArrowRight', 'ArrowRight', 'Tab'])
162162
t.deepEqual(wrapper.state().tags[0].label, 'ccc 1')
163163
})
164+
165+
test('should scroll on keyboard navigation', t => {
166+
const largeTree = [...Array(150).keys()].map(i => node(`id${i}`, `label${i}`))
167+
const wrapper = mount(<DropdownTreeSelect data={largeTree} showDropdown="initial" />)
168+
const getElementById = stub(document, 'getElementById')
169+
const contentNode = wrapper.find('.dropdown-content').getDOMNode()
170+
171+
t.deepEqual(contentNode.scrollTop, 0)
172+
173+
triggerOnKeyboardKeyDown(wrapper, ['ArrowUp'])
174+
largeTree.forEach((n, index) => {
175+
getElementById.withArgs(`${n.id}_li`).returns({ offsetTop: index, clientHeight: 1 })
176+
})
177+
178+
triggerOnKeyboardKeyDown(wrapper, ['ArrowUp'])
179+
t.deepEqual(wrapper.find('li.focused').text(), 'label148')
180+
t.notDeepEqual(contentNode.scrollTop, 0)
181+
182+
getElementById.restore()
183+
})
184+
185+
test('should only scroll on keyboard navigation', t => {
186+
const largeTree = [...Array(150).keys()].map(i => node(`id${i}`, `label${i}`))
187+
const wrapper = mount(<DropdownTreeSelect data={largeTree} showDropdown="initial" />)
188+
const getElementById = stub(document, 'getElementById')
189+
const contentNode = wrapper.find('.dropdown-content').getDOMNode()
190+
191+
triggerOnKeyboardKeyDown(wrapper, ['ArrowUp'])
192+
largeTree.forEach((n, index) => {
193+
getElementById.withArgs(`${n.id}_li`).returns({ offsetTop: index, clientHeight: 1 })
194+
})
195+
196+
triggerOnKeyboardKeyDown(wrapper, ['ArrowUp'])
197+
198+
const scrollTop = contentNode.scrollTop
199+
200+
// Simulate scroll up and setting new props
201+
contentNode.scrollTop -= 20
202+
const newTree = largeTree.map(n => {
203+
return { checked: true, ...n }
204+
})
205+
wrapper.setProps({ data: newTree, showDropdown: 'initial' })
206+
t.notDeepEqual(contentNode.scrollTop, scrollTop)
207+
208+
// Verify scroll is restored to previous position after keyboard nav
209+
triggerOnKeyboardKeyDown(wrapper, ['ArrowUp', 'ArrowDown'])
210+
t.deepEqual(contentNode.scrollTop, scrollTop)
211+
212+
getElementById.restore()
213+
})
214+
215+
const keyDownTests = [
216+
{ keyCode: 13, expected: true }, // Enter
217+
{ keyCode: 32, expected: true }, // Space
218+
{ keyCode: 40, expected: true }, // Arrow down
219+
{ keyCode: 9, expected: false }, // Tab
220+
{ keyCode: 38, expected: false }, // Up arrow
221+
]
222+
223+
keyDownTests.forEach(testArgs => {
224+
test(`Key code ${testArgs.keyCode} ${testArgs.expected ? 'can' : "can't"} open dropdown on keyDown`, t => {
225+
const wrapper = mount(<DropdownTreeSelect data={tree} />)
226+
const trigger = wrapper.find('.dropdown-trigger')
227+
trigger.instance().focus()
228+
trigger.simulate('keyDown', { key: 'mock', keyCode: testArgs.keyCode })
229+
t.is(wrapper.state().showDropdown, testArgs.expected)
230+
})
231+
})
232+
233+
test(`Key event should not trigger if not focused/active element`, t => {
234+
const wrapper = mount(<DropdownTreeSelect data={tree} />)
235+
const trigger = wrapper.find('.dropdown-trigger')
236+
const input = wrapper.find('.search')
237+
input.instance().focus()
238+
trigger.simulate('keyDown', { key: 'mock', keyCode: 13 })
239+
t.is(wrapper.state().showDropdown, false)
240+
})

src/index.test.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -255,25 +255,6 @@ test('detects click outside when other dropdown instance', t => {
255255
t.false(wrapper1.state().showDropdown)
256256
})
257257

258-
const keyDownTests = [
259-
{ keyCode: 13, expected: true }, // Enter
260-
{ keyCode: 32, expected: true }, // Space
261-
{ keyCode: 40, expected: true }, // Arrow down
262-
{ keyCode: 9, expected: false }, // Tab
263-
{ keyCode: 38, expected: false }, // Up arrow
264-
]
265-
266-
keyDownTests.forEach(testArgs => {
267-
test(`Key code ${testArgs.keyCode} ${testArgs.expected ? 'can' : "can't"} open dropdown on keyDown`, t => {
268-
const { tree } = t.context
269-
const wrapper = mount(<DropdownTreeSelect data={tree} />)
270-
const trigger = wrapper.find('.dropdown-trigger')
271-
trigger.instance().focus()
272-
trigger.simulate('keyDown', { key: 'mock', keyCode: testArgs.keyCode })
273-
t.is(wrapper.state().showDropdown, testArgs.expected)
274-
})
275-
})
276-
277258
test('adds aria-labelledby when label contains # to search input', t => {
278259
const { tree } = t.context
279260
const wrapper = mount(<DropdownTreeSelect data={tree} texts={{ label: '#hello #world' }} />)

src/tree/index.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,20 @@ class Tree extends Component {
3838
constructor(props) {
3939
super(props)
4040

41-
this.computeInstanceProps(props)
41+
this.currentPage = 1
42+
this.computeInstanceProps(props, true)
4243

4344
this.state = {
4445
items: this.allVisibleNodes.slice(0, this.props.pageSize),
4546
}
4647
}
4748

4849
componentWillReceiveProps = nextProps => {
49-
this.computeInstanceProps(nextProps)
50+
const { activeDescendant } = nextProps
51+
const hasSameActiveDescendant = activeDescendant === this.props.activeDescendant
52+
this.computeInstanceProps(nextProps, !hasSameActiveDescendant)
5053
this.setState({ items: this.allVisibleNodes.slice(0, this.currentPage * this.props.pageSize) }, () => {
51-
const { activeDescendant } = nextProps
54+
if (hasSameActiveDescendant) return
5255
const { scrollableTarget } = this.state
5356
const activeLi = activeDescendant && document && document.getElementById(activeDescendant)
5457
if (activeLi && scrollableTarget) {
@@ -61,15 +64,13 @@ class Tree extends Component {
6164
this.setState({ scrollableTarget: this.node.parentNode })
6265
}
6366

64-
computeInstanceProps = props => {
67+
computeInstanceProps = (props, checkActiveDescendant) => {
6568
this.allVisibleNodes = this.getNodes(props)
6669
this.totalPages = Math.ceil(this.allVisibleNodes.length / this.props.pageSize)
67-
if (props.activeDescendant) {
70+
if (checkActiveDescendant && props.activeDescendant) {
6871
const currentId = props.activeDescendant.replace(/_li$/, '')
6972
const focusIndex = this.allVisibleNodes.findIndex(n => n.key === currentId) + 1
7073
this.currentPage = focusIndex > 0 ? Math.ceil(focusIndex / this.props.pageSize) : 1
71-
} else {
72-
this.currentPage = 1
7374
}
7475
}
7576

0 commit comments

Comments
 (0)