mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
feat: add autoscroll boolean prop (#1160)
* feat: add autoscroll boolean prop Fixes #449 * refactor: update autoscroll implementation Closes #1028 Closes #910 * refactor: only call maybeAdjustScroll in the watcher * docs: upgrade vuepress
This commit is contained in:
@@ -1,3 +1,16 @@
|
|||||||
|
## autoscroll <Badge text="v3.10.0+" />
|
||||||
|
|
||||||
|
When true, the dropdown will automatically scroll to ensure
|
||||||
|
that the option highlighted is fully within the dropdown viewport
|
||||||
|
when navigating with keyboard arrows.
|
||||||
|
|
||||||
|
```js
|
||||||
|
autoscroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## appendToBody <Badge text="v3.7.0+" />
|
## appendToBody <Badge text="v3.7.0+" />
|
||||||
|
|
||||||
Append the dropdown element to the end of the body
|
Append the dropdown element to the end of the body
|
||||||
|
|||||||
+11
-11
@@ -10,23 +10,23 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/graphql": "^4.3.1",
|
"@octokit/graphql": "^4.3.1",
|
||||||
"@popperjs/core": "^2.1.0",
|
"@popperjs/core": "^2.1.0",
|
||||||
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
|
"@vuepress/plugin-active-header-links": "^1.4.0",
|
||||||
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
|
"@vuepress/plugin-google-analytics": "^1.4.0",
|
||||||
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
|
"@vuepress/plugin-nprogress": "^1.4.0",
|
||||||
"@vuepress/plugin-pwa": "^1.0.0-alpha.47",
|
"@vuepress/plugin-pwa": "^1.4.0",
|
||||||
"@vuepress/plugin-register-components": "^1.0.0-alpha.47",
|
"@vuepress/plugin-register-components": "^1.4.0",
|
||||||
"@vuepress/plugin-search": "^1.0.0-alpha.47",
|
"@vuepress/plugin-search": "^1.4.0",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^7.0.2",
|
||||||
"date-fns": "^2.11.0",
|
"date-fns": "^2.11.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"fuse.js": "^3.4.4",
|
"fuse.js": "^5.1.0",
|
||||||
"gh-pages": "^0.11.0",
|
"gh-pages": "^2.2.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"octonode": "^0.9.5",
|
"octonode": "^0.9.5",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^8.0.2",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vuepress": "^1.0.0-alpha.47",
|
"vuepress": "^1.4.0",
|
||||||
"vuex": "^3.1.0"
|
"vuex": "^3.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2244
-1774
File diff suppressed because it is too large
Load Diff
+28
-60
@@ -1,8 +1,17 @@
|
|||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
autoscroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
typeAheadPointer() {
|
typeAheadPointer() {
|
||||||
this.maybeAdjustScroll();
|
if (this.autoscroll) {
|
||||||
}
|
this.maybeAdjustScroll();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -13,75 +22,34 @@ export default {
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
maybeAdjustScroll() {
|
maybeAdjustScroll() {
|
||||||
let pixelsToPointerTop = this.pixelsToPointerTop();
|
const optionEl =
|
||||||
let pixelsToPointerBottom = this.pixelsToPointerBottom();
|
this.$refs.dropdownMenu?.children[this.typeAheadPointer] || false;
|
||||||
|
|
||||||
if (pixelsToPointerTop <= this.viewport().top) {
|
if (optionEl) {
|
||||||
return this.scrollTo(pixelsToPointerTop);
|
const bounds = this.getDropdownViewport();
|
||||||
} else if (pixelsToPointerBottom >= this.viewport().bottom) {
|
const { top, bottom, height } = optionEl.getBoundingClientRect();
|
||||||
return this.scrollTo(this.viewport().top + this.pointerHeight());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
if (top < bounds.top) {
|
||||||
* The distance in pixels from the top of the dropdown
|
return (this.$refs.dropdownMenu.scrollTop = optionEl.offsetTop);
|
||||||
* list to the top of the current pointer element.
|
} else if (bottom > bounds.bottom) {
|
||||||
* @returns {number}
|
return (this.$refs.dropdownMenu.scrollTop =
|
||||||
*/
|
optionEl.offsetTop - (bounds.height - height));
|
||||||
pixelsToPointerTop() {
|
|
||||||
let pixelsToPointerTop = 0;
|
|
||||||
if (this.$refs.dropdownMenu && this.dropdownOpen) {
|
|
||||||
for (let i = 0; i < this.typeAheadPointer; i++) {
|
|
||||||
pixelsToPointerTop += this.$refs.dropdownMenu.children[i]
|
|
||||||
.offsetHeight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pixelsToPointerTop;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The distance in pixels from the top of the dropdown
|
|
||||||
* list to the bottom of the current pointer element.
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
pixelsToPointerBottom() {
|
|
||||||
return this.pixelsToPointerTop() + this.pointerHeight();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The offsetHeight of the current pointer element.
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
pointerHeight() {
|
|
||||||
let element = this.$refs.dropdownMenu
|
|
||||||
? this.$refs.dropdownMenu.children[this.typeAheadPointer]
|
|
||||||
: false;
|
|
||||||
return element ? element.offsetHeight : 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently viewable portion of the dropdownMenu.
|
* The currently viewable portion of the dropdownMenu.
|
||||||
* @returns {{top: (string|*|number), bottom: *}}
|
* @returns {{top: (string|*|number), bottom: *}}
|
||||||
*/
|
*/
|
||||||
viewport() {
|
getDropdownViewport() {
|
||||||
return {
|
|
||||||
top: this.$refs.dropdownMenu ? this.$refs.dropdownMenu.scrollTop : 0,
|
|
||||||
bottom: this.$refs.dropdownMenu
|
|
||||||
? this.$refs.dropdownMenu.offsetHeight +
|
|
||||||
this.$refs.dropdownMenu.scrollTop
|
|
||||||
: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll the dropdownMenu to a given position.
|
|
||||||
* @param position
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
scrollTo(position) {
|
|
||||||
return this.$refs.dropdownMenu
|
return this.$refs.dropdownMenu
|
||||||
? (this.$refs.dropdownMenu.scrollTop = position)
|
? this.$refs.dropdownMenu.getBoundingClientRect()
|
||||||
: null;
|
: {
|
||||||
|
height: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ export default {
|
|||||||
for (let i = this.typeAheadPointer - 1; i >= 0; i--) {
|
for (let i = this.typeAheadPointer - 1; i >= 0; i--) {
|
||||||
if (this.selectable(this.filteredOptions[i])) {
|
if (this.selectable(this.filteredOptions[i])) {
|
||||||
this.typeAheadPointer = i;
|
this.typeAheadPointer = i;
|
||||||
if( this.maybeAdjustScroll ) {
|
|
||||||
this.maybeAdjustScroll()
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -43,10 +39,6 @@ export default {
|
|||||||
for (let i = this.typeAheadPointer + 1; i < this.filteredOptions.length; i++) {
|
for (let i = this.typeAheadPointer + 1; i < this.filteredOptions.length; i++) {
|
||||||
if (this.selectable(this.filteredOptions[i])) {
|
if (this.selectable(this.filteredOptions[i])) {
|
||||||
this.typeAheadPointer = i;
|
this.typeAheadPointer = i;
|
||||||
if( this.maybeAdjustScroll ) {
|
|
||||||
this.maybeAdjustScroll()
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { mountDefault } from "../helpers";
|
||||||
|
|
||||||
|
describe("Automatic Scrolling", () => {
|
||||||
|
it("should check if the scroll position needs to be adjusted on up arrow keyUp", async () => {
|
||||||
|
// Given
|
||||||
|
const Select = mountDefault();
|
||||||
|
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
||||||
|
Select.vm.typeAheadPointer = 1;
|
||||||
|
|
||||||
|
// When
|
||||||
|
Select.find({ ref: "search" }).trigger("keydown.up");
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check if the scroll position needs to be adjusted on down arrow keyUp", async () => {
|
||||||
|
// Given
|
||||||
|
const Select = mountDefault();
|
||||||
|
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
||||||
|
Select.vm.typeAheadPointer = 1;
|
||||||
|
|
||||||
|
// When
|
||||||
|
Select.find({ ref: "search" }).trigger("keydown.down");
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check if the scroll position needs to be adjusted when filtered options changes", async () => {
|
||||||
|
// Given
|
||||||
|
const Select = mountDefault();
|
||||||
|
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
||||||
|
Select.vm.typeAheadPointer = 1;
|
||||||
|
|
||||||
|
// When
|
||||||
|
Select.vm.search = "two";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not adjust scroll position when autoscroll is false", async () => {
|
||||||
|
// Given
|
||||||
|
const Select = mountDefault({
|
||||||
|
autoscroll: false
|
||||||
|
});
|
||||||
|
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
||||||
|
Select.vm.typeAheadPointer = 1;
|
||||||
|
|
||||||
|
// When
|
||||||
|
Select.vm.search = "two";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from "@vue/test-utils";
|
||||||
import VueSelect from "../../src/components/Select";
|
import VueSelect from "../../src/components/Select";
|
||||||
import { mountDefault, mountWithoutTestUtils } from '../helpers';
|
import { mountDefault, mountWithoutTestUtils } from "../helpers";
|
||||||
import typeAheadMixin from '../../src/mixins/typeAheadPointer';
|
import typeAheadMixin from "../../src/mixins/typeAheadPointer";
|
||||||
import Vue from 'vue';
|
import Vue from "vue";
|
||||||
|
|
||||||
describe("Moving the Typeahead Pointer", () => {
|
describe("Moving the Typeahead Pointer", () => {
|
||||||
|
it("should set the pointer to zero when the filteredOptions watcher is called", async () => {
|
||||||
it('should set the pointer to zero when the filteredOptions watcher is called', async () => {
|
|
||||||
const Select = shallowMount(VueSelect, {
|
const Select = shallowMount(VueSelect, {
|
||||||
propsData: { options: ['one', 'two', 'three'] },
|
propsData: { options: ["one", "two", "three"] },
|
||||||
sync: false
|
sync: false
|
||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.search = 'one';
|
Select.vm.search = "one";
|
||||||
|
|
||||||
await Select.vm.$nextTick();
|
await Select.vm.$nextTick();
|
||||||
expect(Select.vm.typeAheadPointer).toEqual(0);
|
expect(Select.vm.typeAheadPointer).toEqual(0);
|
||||||
@@ -45,114 +44,4 @@ describe("Moving the Typeahead Pointer", () => {
|
|||||||
Select.vm.typeAheadDown();
|
Select.vm.typeAheadDown();
|
||||||
expect(Select.vm.typeAheadPointer).toEqual(2);
|
expect(Select.vm.typeAheadPointer).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Automatic Scrolling", () => {
|
|
||||||
it("should check if the scroll position needs to be adjusted on up arrow keyUp", () => {
|
|
||||||
const Select = mountDefault();
|
|
||||||
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
|
||||||
|
|
||||||
Select.vm.typeAheadPointer = 1;
|
|
||||||
|
|
||||||
Select.find({ ref: "search" }).trigger("keydown.up");
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should check if the scroll position needs to be adjusted on down arrow keyUp", () => {
|
|
||||||
const Select = mountDefault();
|
|
||||||
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
|
||||||
|
|
||||||
Select.vm.typeAheadPointer = 1;
|
|
||||||
|
|
||||||
Select.find({ ref: "search" }).trigger("keydown.down");
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test fails despite working in the browser.
|
|
||||||
* After many attempts to get it to pass, it's been
|
|
||||||
* rewritten below.
|
|
||||||
*/
|
|
||||||
it.skip("should check if the scroll position needs to be adjusted when filtered options changes", () => {
|
|
||||||
const Select = mountDefault();
|
|
||||||
const spy = jest.spyOn(Select.vm, "maybeAdjustScroll");
|
|
||||||
|
|
||||||
Select.vm.search = "two";
|
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should scroll up if the pointer is above the current viewport bounds", () => {
|
|
||||||
const Select = mountDefault();
|
|
||||||
const spy = jest.spyOn(Select.vm, "scrollTo");
|
|
||||||
|
|
||||||
Select.setMethods({
|
|
||||||
pixelsToPointerTop() {
|
|
||||||
return 1;
|
|
||||||
},
|
|
||||||
viewport() {
|
|
||||||
return { top: 2, bottom: 0 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Select.vm.maybeAdjustScroll();
|
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should scroll down if the pointer is below the current viewport bounds", () => {
|
|
||||||
const Select = mountDefault();
|
|
||||||
const spy = jest.spyOn(Select.vm, "scrollTo");
|
|
||||||
|
|
||||||
Select.setMethods({
|
|
||||||
pixelsToPointerBottom() {
|
|
||||||
return 2;
|
|
||||||
},
|
|
||||||
viewport() {
|
|
||||||
return { top: 0, bottom: 1 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Select.vm.maybeAdjustScroll();
|
|
||||||
expect(spy).toHaveBeenCalledWith(
|
|
||||||
Select.vm.viewport().top + Select.vm.pointerHeight()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Measuring pixel distances", () => {
|
|
||||||
it("should calculate pointerHeight as the offsetHeight of the pointer element if it exists", async () => {
|
|
||||||
const Select = mountDefault();
|
|
||||||
|
|
||||||
// Drop down must be open for $refs to exist
|
|
||||||
Select.vm.open = true;
|
|
||||||
await Select.vm.$nextTick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since JSDom doesn't render layouts, set the offsetHeight explicitly
|
|
||||||
* to 25px for each list item.
|
|
||||||
*
|
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
|
|
||||||
*/
|
|
||||||
let i = 0;
|
|
||||||
for (let option of Select.vm.$refs.dropdownMenu.children) {
|
|
||||||
Object.defineProperty(option, "offsetHeight", {
|
|
||||||
value: 1 + i
|
|
||||||
});
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fresh instances start with the pointer at -1
|
|
||||||
Select.vm.typeAheadPointer = -1;
|
|
||||||
expect(Select.vm.pointerHeight()).toEqual(0);
|
|
||||||
|
|
||||||
Select.vm.typeAheadPointer = 0;
|
|
||||||
expect(Select.vm.pointerHeight()).toEqual(1);
|
|
||||||
|
|
||||||
Select.vm.typeAheadPointer = 1;
|
|
||||||
expect(Select.vm.pointerHeight()).toEqual(2);
|
|
||||||
|
|
||||||
Select.vm.typeAheadPointer = 2;
|
|
||||||
expect(Select.vm.pointerHeight()).toEqual(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user