mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
feat: calculated positioning (#1049)
Adds `appendToBody` and `calculatePosition` props. https://vue-select.org/guide/positioning.html Co-authored-by: Jeff <sagalbot@gmail.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-select :options="countries" append-to-body :calculate-position="withPopper" />
|
||||||
|
|
||||||
|
<label for="position" style="display: block; margin: 1rem 0;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="position"
|
||||||
|
v-model="placement"
|
||||||
|
true-value="top"
|
||||||
|
false-value="bottom"
|
||||||
|
>
|
||||||
|
Position dropdown above
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import countries from '../data/countries'
|
||||||
|
import { createPopper } from '@popperjs/core';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({countries, placement: 'top'}),
|
||||||
|
methods: {
|
||||||
|
withPopper (dropdownList, component, {width},) {
|
||||||
|
/**
|
||||||
|
* We need to explicitly define the dropdown width since
|
||||||
|
* it is usually inherited from the parent with CSS.
|
||||||
|
*/
|
||||||
|
dropdownList.style.width = width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we position the dropdownList relative to the $refs.toggle Element.
|
||||||
|
*
|
||||||
|
* The 'offset' modifier aligns the dropdown so that the $refs.toggle and
|
||||||
|
* the dropdownList overlap by 1 pixel.
|
||||||
|
*
|
||||||
|
* The 'toggleClass' modifier adds a 'drop-up' class to the Vue Select
|
||||||
|
* wrapper so that we can set some styles for when the dropdown is placed
|
||||||
|
* above.
|
||||||
|
*/
|
||||||
|
createPopper(component.$refs.toggle, dropdownList, {
|
||||||
|
placement: this.placement,
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'offset', options: {
|
||||||
|
offset: [0, -1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'toggleClass',
|
||||||
|
enabled: true,
|
||||||
|
phase: 'write',
|
||||||
|
fn ({state}) {
|
||||||
|
component.$el.classList.toggle('drop-up', state.placement === 'top')
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-select.drop-up.vs--open .vs__dropdown-toggle {
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-bottom-color: rgba(60, 60, 60, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-popper-placement='top'] {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-bottom-style: none;
|
||||||
|
box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.15)
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -129,6 +129,7 @@ module.exports = {
|
|||||||
collapsable: false,
|
collapsable: false,
|
||||||
children: [
|
children: [
|
||||||
['guide/keydown', 'Keydown Events'],
|
['guide/keydown', 'Keydown Events'],
|
||||||
|
['guide/positioning', 'Dropdown Position']
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+43
-3
@@ -1,3 +1,18 @@
|
|||||||
|
## appendToBody <Badge text="v3.7.0+" />
|
||||||
|
|
||||||
|
Append the dropdown element to the end of the body
|
||||||
|
and size/position it dynamically. Use it if you have
|
||||||
|
overflow or z-index issues.
|
||||||
|
|
||||||
|
See [Dropdown Position](../guide/positioning.md) for more details.
|
||||||
|
|
||||||
|
```js
|
||||||
|
appendToBody: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
## value
|
## value
|
||||||
|
|
||||||
Contains the currently selected value. Very similar to a
|
Contains the currently selected value. Very similar to a
|
||||||
@@ -109,6 +124,30 @@ transition: {
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## calculatePosition <Badge text="v3.7.0+" />
|
||||||
|
|
||||||
|
When `appendToBody` is true, this function is responsible for positioning the drop down list.
|
||||||
|
|
||||||
|
See [Dropdown Position](../guide/positioning.md) for more details.
|
||||||
|
|
||||||
|
```js
|
||||||
|
calculatePosition: {
|
||||||
|
type: Function,
|
||||||
|
/**
|
||||||
|
* @param dropdownList {HTMLUListElement}
|
||||||
|
* @param component {Vue} current instance of vue select
|
||||||
|
* @param width {string} calculated width in pixels of the dropdown menu
|
||||||
|
* @param top {string} absolute position top value in pixels relative to the document
|
||||||
|
* @param left {string} absolute position left value in pixels relative to the document
|
||||||
|
*/
|
||||||
|
default(dropdownList, component, {width, top, left}) {
|
||||||
|
dropdownList.style.top = top;
|
||||||
|
dropdownList.style.left = left;
|
||||||
|
dropdownList.style.width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## clearSearchOnSelect
|
## clearSearchOnSelect
|
||||||
|
|
||||||
Enables/disables clearing the search text when an option is selected.
|
Enables/disables clearing the search text when an option is selected.
|
||||||
@@ -353,10 +392,10 @@ createOption: {
|
|||||||
## resetOnOptionsChange
|
## resetOnOptionsChange
|
||||||
|
|
||||||
When false, updating the options will not reset the selected value.
|
When false, updating the options will not reset the selected value.
|
||||||
|
|
||||||
Since `v3.4+` the prop accepts either a `boolean` or `function` that returns a `boolean`.
|
|
||||||
|
|
||||||
If defined as a function, it will receive the params listed below.
|
Since `v3.4+` the prop accepts either a `boolean` or `function` that returns a `boolean`.
|
||||||
|
|
||||||
|
If defined as a function, it will receive the params listed below.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/**
|
/**
|
||||||
@@ -412,3 +451,4 @@ selectOnTab: {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## Default
|
||||||
|
|
||||||
|
With the default CSS, Vue Select uses absolute positioning to render the dropdown menu. The root
|
||||||
|
`.v-select` container (the components `$el`) is used as the `relative` parent for the dropdown. The
|
||||||
|
dropdown will be displayed below the `$el` regardless of the available space.
|
||||||
|
|
||||||
|
This works for most cases, but you might run into issues placing into a modal or near the bottom of
|
||||||
|
the viewport. If you need more fine grain control, you can use calculated positioning.
|
||||||
|
|
||||||
|
## Calculated <Badge text="v3.7.0+" />
|
||||||
|
|
||||||
|
If you want more control over how the dropdown is rendered, or if you're running into z-index issues,
|
||||||
|
you may use the `appendToBody` boolean prop. When enabled, Vue Select will append the dropdown to
|
||||||
|
the document, outside of the `.v-select` container, and position it with Javscript.
|
||||||
|
|
||||||
|
When `appendToBody` is true, the positioning will be handled by the `calculatePosition` prop. This
|
||||||
|
function is responsible for setting top/left absolute positioning values for the dropdown. The
|
||||||
|
default implementation places the dropdown in the same position that it would normally appear.
|
||||||
|
|
||||||
|
## Popper.js Integration <Badge text="v3.7.0+" />
|
||||||
|
|
||||||
|
[Popper.js](https://popper.js.org/) is an awesome, 3kb utility for calculating positions of just
|
||||||
|
about any DOM element relative to another.
|
||||||
|
|
||||||
|
By using the `appendToBody` and `calculatePosition` props, we're able to integrate directly with
|
||||||
|
popper to calculate positioning for us.
|
||||||
|
|
||||||
|
<PositionedWithPopper />
|
||||||
|
|
||||||
|
Check out the [Popper Docs](https://popper.js.org/docs/v2/modifiers/) to see the full `modifiers`
|
||||||
|
API being used below.
|
||||||
|
|
||||||
|
<<< @/.vuepress/components/PositionedWithPopper.vue{25-59}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build"
|
"build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@popperjs/core": "^2.1.0",
|
||||||
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
|
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
|
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
|
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
|
||||||
|
|||||||
@@ -715,6 +715,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
||||||
|
|
||||||
|
"@popperjs/core@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
|
||||||
|
integrity sha512-ntN5t5spqhQv28cLfmmt1dYabsudzR5A7PU15gr/gzcT/gzqAOnYFQPaLPFraDa7ZCJG2eJ1JsO7pgXbYXGIrw==
|
||||||
|
|
||||||
"@types/events@*":
|
"@types/events@*":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||||
|
|||||||
+57
-21
@@ -53,28 +53,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition :name="transition">
|
<transition :name="transition">
|
||||||
<ul ref="dropdownMenu" v-show="dropdownOpen" :id="`vs${uid}__listbox`" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp">
|
<ul ref="dropdownMenu" v-if="dropdownOpen" :id="`vs${uid}__listbox`" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp" v-append-to-body>
|
||||||
<template v-if="dropdownOpen">
|
<li
|
||||||
<li
|
role="option"
|
||||||
role="option"
|
v-for="(option, index) in filteredOptions"
|
||||||
v-for="(option, index) in filteredOptions"
|
:key="getOptionKey(option)"
|
||||||
:key="getOptionKey(option)"
|
:id="`vs${uid}__option-${index}`"
|
||||||
:id="`vs${uid}__option-${index}`"
|
class="vs__dropdown-option"
|
||||||
class="vs__dropdown-option"
|
:class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(option) }"
|
||||||
:class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(option) }"
|
:aria-selected="index === typeAheadPointer ? true : null"
|
||||||
:aria-selected="index === typeAheadPointer ? true : null"
|
@mouseover="selectable(option) ? typeAheadPointer = index : null"
|
||||||
@mouseover="selectable(option) ? typeAheadPointer = index : null"
|
@mousedown.prevent.stop="selectable(option) ? select(option) : null"
|
||||||
@mousedown.prevent.stop="selectable(option) ? select(option) : null"
|
>
|
||||||
>
|
<slot name="option" v-bind="normalizeOptionForSlot(option)">
|
||||||
<slot name="option" v-bind="normalizeOptionForSlot(option)">
|
{{ getOptionLabel(option) }}
|
||||||
{{ getOptionLabel(option) }}
|
</slot>
|
||||||
</slot>
|
</li>
|
||||||
</li>
|
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
|
||||||
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
|
<slot name="no-options" v-bind="scope.noOptions">Sorry, no matching options.</slot>
|
||||||
<slot name="no-options" v-bind="scope.noOptions">Sorry, no matching options.</slot>
|
</li>
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul v-else :id="`vs${uid}__listbox`" role="listbox" style="display: none; visibility: hidden;"></ul>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -84,6 +83,7 @@
|
|||||||
import typeAheadPointer from '../mixins/typeAheadPointer'
|
import typeAheadPointer from '../mixins/typeAheadPointer'
|
||||||
import ajax from '../mixins/ajax'
|
import ajax from '../mixins/ajax'
|
||||||
import childComponents from './childComponents';
|
import childComponents from './childComponents';
|
||||||
|
import appendToBody from '../directives/appendToBody';
|
||||||
import uniqueId from '../utility/uniqueId';
|
import uniqueId from '../utility/uniqueId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,6 +94,8 @@
|
|||||||
|
|
||||||
mixins: [pointerScroll, typeAheadPointer, ajax],
|
mixins: [pointerScroll, typeAheadPointer, ajax],
|
||||||
|
|
||||||
|
directives: {appendToBody},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* Contains the currently selected value. Very similar to a
|
* Contains the currently selected value. Very similar to a
|
||||||
@@ -517,6 +519,40 @@
|
|||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
default: (map, vm) => map,
|
default: (map, vm) => map,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the dropdown element to the end of the body
|
||||||
|
* and size/position it dynamically. Use it if you have
|
||||||
|
* overflow or z-index issues.
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
appendToBody: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When `appendToBody` is true, this function
|
||||||
|
* is responsible for positioning the drop
|
||||||
|
* down list.
|
||||||
|
* @since v3.7.0
|
||||||
|
* @see http://vue-select.org/guide/positioning.html
|
||||||
|
*/
|
||||||
|
calculatePosition: {
|
||||||
|
type: Function,
|
||||||
|
/**
|
||||||
|
* @param dropdownList {HTMLUListElement}
|
||||||
|
* @param component {Vue} current instance of vue select
|
||||||
|
* @param width {string} calculated width in pixels of the dropdown menu
|
||||||
|
* @param top {string} absolute position top value in pixels relative to the document
|
||||||
|
* @param left {string} absolute position left value in pixels relative to the document
|
||||||
|
*/
|
||||||
|
default(dropdownList, component, {width, top, left}) {
|
||||||
|
dropdownList.style.top = top;
|
||||||
|
dropdownList.style.left = left;
|
||||||
|
dropdownList.style.width = width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
inserted (el, bindings, {context}) {
|
||||||
|
if (context.appendToBody) {
|
||||||
|
const {height, top, left} = context.$refs.toggle.getBoundingClientRect();
|
||||||
|
|
||||||
|
context.calculatePosition(el, context, {
|
||||||
|
width: context.$refs.toggle.clientWidth + 'px',
|
||||||
|
top: (window.scrollY + top + height) + 'px',
|
||||||
|
left: (window.scrollX + left) + 'px',
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unbind (el, bindings, vnode) {
|
||||||
|
if (vnode.context.appendToBody && el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -137,7 +137,7 @@ describe("Toggling Dropdown", () => {
|
|||||||
expect(Select.vm.open).toEqual(true);
|
expect(Select.vm.open).toEqual(true);
|
||||||
await Select.vm.$nextTick();
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.find('.vs__dropdown-menu').element.style['display']).toEqual('none');
|
expect(Select.contains('.vs__dropdown-menu')).toBeFalsy();
|
||||||
expect(Select.contains('.vs__dropdown-option')).toBeFalsy();
|
expect(Select.contains('.vs__dropdown-option')).toBeFalsy();
|
||||||
expect(Select.contains('.vs__no-options')).toBeFalsy();
|
expect(Select.contains('.vs__no-options')).toBeFalsy();
|
||||||
expect(Select.vm.stateClasses['vs--open']).toBeFalsy();
|
expect(Select.vm.stateClasses['vs--open']).toBeFalsy();
|
||||||
|
|||||||
Reference in New Issue
Block a user