Skip to content

Commit b33a203

Browse files
author
Dobromir Hristov
authored
fix: issues with scrolling to item (#424)
closes rdar://99279409
1 parent 9a608c3 commit b33a203

File tree

2 files changed

+57
-27
lines changed

2 files changed

+57
-27
lines changed

src/components/Navigator/NavigatorCard.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -889,16 +889,17 @@ export default {
889889
await waitFrames(1);
890890
if (!this.$refs.scroller) return;
891891
// if we are filtering, it makes more sense to scroll to top of list
892-
if (this.hasFilter) {
892+
if (this.hasFilter && !this.deprecatedHidden) {
893893
this.$refs.scroller.scrollToItem(0);
894894
return;
895895
}
896896
// check if the current element is visible and needs scrolling into
897897
const element = document.getElementById(this.activeUID);
898-
// check if item is inside scroller
899-
if (this.getChildPositionInScroller(element) === 0) return;
900-
// find the index of the current active UID in the newly added nodes
898+
// check if there is such an item AND the item is inside scroller area
899+
if (element && this.getChildPositionInScroller(element) === 0) return;
900+
// find the index of the current active UID in the nodes to render
901901
const index = this.nodesToRender.findIndex(child => child.uid === this.activeUID);
902+
if (index === -1) return;
902903
// check if the element is visible
903904
// call the scroll method on the `scroller` component.
904905
this.$refs.scroller.scrollToItem(index);

tests/unit/components/Navigator/NavigatorCard.spec.js

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,16 @@ function mergeSessionState(state) {
171171
};
172172
}
173173

174+
function attachDivWithID(id) {
175+
if (document.getElementById(id)) return;
176+
const div = document.createElement('DIV');
177+
div.id = id;
178+
document.body.appendChild(div);
179+
}
180+
174181
describe('NavigatorCard', () => {
175182
beforeEach(() => {
183+
document.body.innerHTML = '';
176184
jest.clearAllMocks();
177185
// mock the position helper function, as its too difficult to mock the boundingClientRects
178186
getChildPositionInScroller = jest.spyOn(NavigatorCard.methods, 'getChildPositionInScroller')
@@ -929,6 +937,7 @@ describe('NavigatorCard', () => {
929937
});
930938

931939
it('allows filtering the items, opening all items, that have matches in children', async () => {
940+
attachDivWithID(root0Child0.uid);
932941
const wrapper = createWrapper();
933942
await flushPromises();
934943
// item is not scrolled to
@@ -959,6 +968,7 @@ describe('NavigatorCard', () => {
959968
root0Child1GrandChild0,
960969
root1,
961970
];
971+
attachDivWithID(root0Child0.uid);
962972

963973
const wrapper = createWrapper({
964974
propsData: {
@@ -980,6 +990,7 @@ describe('NavigatorCard', () => {
980990
});
981991

982992
it('renders all the children of a directly matched parent', async () => {
993+
attachDivWithID(root0Child0.uid);
983994
const wrapper = createWrapper();
984995
const filter = wrapper.find(FilterInput);
985996
await flushPromises();
@@ -1007,6 +1018,7 @@ describe('NavigatorCard', () => {
10071018
});
10081019

10091020
it('allows filtering the items using Tags, opening all items, that have matches in children', async () => {
1021+
attachDivWithID(root0Child0.uid);
10101022
const wrapper = createWrapper();
10111023
await flushPromises();
10121024
const filter = wrapper.find(FilterInput);
@@ -1024,6 +1036,7 @@ describe('NavigatorCard', () => {
10241036
});
10251037

10261038
it('aliases `project` to `tutorial`, when filtering using tags', async () => {
1039+
attachDivWithID(root0Child0.uid);
10271040
const wrapper = createWrapper();
10281041
const filter = wrapper.find(FilterInput);
10291042
await flushPromises();
@@ -1038,6 +1051,7 @@ describe('NavigatorCard', () => {
10381051
});
10391052

10401053
it('allows filtering the items with filter and Tags, opening all items, that have matches in children', async () => {
1054+
attachDivWithID(root0Child0.uid);
10411055
const wrapper = createWrapper();
10421056
const filter = wrapper.find(FilterInput);
10431057
await flushPromises();
@@ -1742,10 +1756,14 @@ describe('NavigatorCard', () => {
17421756
describe('navigating', () => {
17431757
it('changes the open item, when navigating across pages, keeping the previously open items', async () => {
17441758
// simulate navigating to the bottom most item.
1759+
attachDivWithID(root0Child0.uid);
17451760
const wrapper = createWrapper();
17461761
await flushPromises();
1762+
// item is in viewport, no need to scroll to it
1763+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(0);
17471764
// simulate the new item is below the fold
17481765
getChildPositionInScroller.mockReturnValueOnce(1);
1766+
attachDivWithID(root0Child1GrandChild0.uid);
17491767
wrapper.setProps({
17501768
activePath: [
17511769
root0.path,
@@ -1754,6 +1772,7 @@ describe('NavigatorCard', () => {
17541772
],
17551773
});
17561774
await flushPromises();
1775+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(1);
17571776
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(3);
17581777
// assert all are open
17591778
const all = wrapper.findAll(NavigatorCardItem);
@@ -1786,13 +1805,15 @@ describe('NavigatorCard', () => {
17861805
// simulate the new item is above the scrollarea
17871806
getChildPositionInScroller.mockReturnValueOnce(-1);
17881807
// navigate to the top level sibling
1808+
attachDivWithID(root1.uid);
17891809
wrapper.setProps({
17901810
activePath: [
17911811
root1.path,
17921812
],
17931813
});
17941814
await flushPromises();
17951815
// assert it scrolls to the item
1816+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(2);
17961817
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(4);
17971818
// assert items are still open
17981819
expect(all.at(0).props()).toMatchObject({
@@ -1828,13 +1849,18 @@ describe('NavigatorCard', () => {
18281849
});
18291850

18301851
it('tracks current open item, from clicking child items, handling duplicate router changes on the way', async () => {
1852+
attachDivWithID(root0Child0.uid);
18311853
const wrapper = createWrapper();
18321854
await flushPromises();
1855+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(0);
18331856
let allItems = wrapper.findAll(NavigatorCardItem);
18341857
const targetChild = allItems.at(2);
18351858
expect(targetChild.props('item')).toEqual(root0Child1);
18361859
// simulate the new item is below the fold
18371860
getChildPositionInScroller.mockReturnValueOnce(1);
1861+
// add an element with the same ID as the one we are navigating to,
1862+
// otherwise we won't scroll to it
1863+
attachDivWithID(root0Child1.uid);
18381864
// trigger a navigation
18391865
targetChild.vm.$emit('navigate', root0Child1.uid);
18401866
await wrapper.vm.$nextTick();
@@ -2039,9 +2065,10 @@ describe('NavigatorCard', () => {
20392065

20402066
describe('scroll to item', () => {
20412067
it('resets the scroll position, if initiating a filter', async () => {
2042-
const wrapper = createWrapper();
2068+
attachDivWithID(root0Child0.uid);
20432069
// simulate item is above the scrollarea
20442070
getChildPositionInScroller.mockReturnValueOnce(1);
2071+
const wrapper = createWrapper();
20452072
await flushPromises();
20462073
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(1);
20472074
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(1);
@@ -2055,43 +2082,45 @@ describe('NavigatorCard', () => {
20552082
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(0);
20562083
});
20572084

2058-
it('keeps the scroll position, if the item is already in the viewport, on navigation', async () => {
2085+
it('scrolls to item, if HIDE_DEPRECATED_TAG is picked', async () => {
2086+
attachDivWithID(root0Child0.uid);
2087+
// simulate item is in viewport
2088+
getChildPositionInScroller.mockReturnValueOnce(0);
20592089
const wrapper = createWrapper();
20602090
await flushPromises();
20612091
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(0);
2062-
wrapper.findAll(NavigatorCardItem).at(2).vm.$emit('navigate', root0Child1.uid);
2063-
await flushPromises();
2064-
// make sure scrollToItem is not called, because active item is already in the viewport
2065-
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(0);
2066-
// simulate header scroll
2092+
// simulate item is below the viewport
20672093
getChildPositionInScroller.mockReturnValueOnce(1);
2068-
wrapper.findAll(NavigatorCardItem).at(2).vm.$emit('navigate', root0Child0.uid);
2094+
// add the "Hide Deprecated" tag
2095+
wrapper.find(FilterInput).vm.$emit('update:selectedTags', [HIDE_DEPRECATED_TAG]);
20692096
await flushPromises();
2070-
// make sure scrollToItem is not called, because active item is already in the viewport
2097+
// assert current active item is still scrolled to
20712098
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(1);
2099+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(1);
20722100
});
20732101

2074-
it('scrolls to item, if outside of visible viewport, on page navigation', async () => {
2102+
it('keeps the scroll position, if the item is already in the viewport, on navigation', async () => {
2103+
attachDivWithID(root0Child0.uid);
20752104
const wrapper = createWrapper();
20762105
await flushPromises();
2106+
// assert scrollToItem is not called, because its "in view"
20772107
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(0);
2078-
getChildPositionInScroller.mockReturnValueOnce(-1);
2079-
// scroll to the item
2108+
attachDivWithID(root0Child1.uid);
20802109
wrapper.findAll(NavigatorCardItem).at(2).vm.$emit('navigate', root0Child1.uid);
20812110
await flushPromises();
2082-
// make sure scrollToItem is called
2083-
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(1);
2084-
// assert it was called for the 3-rd item
2085-
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(2);
2086-
// assert scrolling beyond
2111+
// make sure scrollToItem is not called again, because active item is still in the viewport
2112+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(0);
2113+
// simulate item is below the fold and assert navigating to new place scrolls the item
20872114
getChildPositionInScroller.mockReturnValueOnce(1);
2088-
// scroll to the item
2089-
wrapper.findAll(NavigatorCardItem).at(2).vm.$emit('navigate', root0Child0.uid);
2115+
// prepare
2116+
attachDivWithID(root0Child1GrandChild0.uid);
2117+
// navigate
2118+
wrapper.findAll(NavigatorCardItem).at(2).vm.$emit('navigate', root0Child1GrandChild0.uid);
20902119
await flushPromises();
2091-
// make sure scrollToItem is called
2092-
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(2);
2093-
// assert it was called for the 3-rd item
2094-
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenLastCalledWith(1);
2120+
// assert scrollToItem is called, because item was under the fold
2121+
expect(RecycleScrollerStub.methods.scrollToItem).toHaveBeenCalledTimes(1);
2122+
expect(RecycleScrollerStub.methods.scrollToItem)
2123+
.toHaveBeenLastCalledWith(3);
20952124
});
20962125

20972126
it('scrolls to the focused item, if not visible', async () => {

0 commit comments

Comments
 (0)