From fe51fec6b83c3b1f89e1d6fa03ed9944dc1ba1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20BORDIER?= Date: Sun, 8 Mar 2020 21:31:08 +0100 Subject: [PATCH] feat: calculated positioning (#1049) Adds `appendToBody` and `calculatePosition` props. https://vue-select.org/guide/positioning.html Co-authored-by: Jeff --- .../components/PositionedWithPopper.vue | 77 ++++++++++++++++++ docs/.vuepress/config.js | 1 + docs/api/props.md | 46 ++++++++++- docs/guide/positioning.md | 33 ++++++++ docs/package.json | 1 + docs/yarn.lock | 5 ++ src/components/Select.vue | 78 ++++++++++++++----- src/directives/appendToBody.js | 21 +++++ tests/unit/Dropdown.spec.js | 2 +- 9 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 docs/.vuepress/components/PositionedWithPopper.vue create mode 100644 docs/guide/positioning.md create mode 100644 src/directives/appendToBody.js diff --git a/docs/.vuepress/components/PositionedWithPopper.vue b/docs/.vuepress/components/PositionedWithPopper.vue new file mode 100644 index 0000000..d7580bf --- /dev/null +++ b/docs/.vuepress/components/PositionedWithPopper.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 2bed2c8..2ee84ca 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -129,6 +129,7 @@ module.exports = { collapsable: false, children: [ ['guide/keydown', 'Keydown Events'], + ['guide/positioning', 'Dropdown Position'] ], }, { diff --git a/docs/api/props.md b/docs/api/props.md index e54126f..d4d200d 100644 --- a/docs/api/props.md +++ b/docs/api/props.md @@ -1,3 +1,18 @@ +## appendToBody + +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 Contains the currently selected value. Very similar to a @@ -109,6 +124,30 @@ transition: { }, ``` +## calculatePosition + +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 Enables/disables clearing the search text when an option is selected. @@ -353,10 +392,10 @@ createOption: { ## resetOnOptionsChange 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 /** @@ -412,3 +451,4 @@ selectOnTab: { type: Boolean, default: false } +``` diff --git a/docs/guide/positioning.md b/docs/guide/positioning.md new file mode 100644 index 0000000..ad5a4b9 --- /dev/null +++ b/docs/guide/positioning.md @@ -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 + +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 + +[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. + + + +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} diff --git a/docs/package.json b/docs/package.json index 1fcd52b..79ca247 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,6 +8,7 @@ "build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build" }, "devDependencies": { + "@popperjs/core": "^2.1.0", "@vuepress/plugin-active-header-links": "^1.0.0-alpha.47", "@vuepress/plugin-google-analytics": "^1.0.0-alpha.47", "@vuepress/plugin-nprogress": "^1.0.0-alpha.47", diff --git a/docs/yarn.lock b/docs/yarn.lock index 8548f5b..ce6091c 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -715,6 +715,11 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" 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@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" diff --git a/src/components/Select.vue b/src/components/Select.vue index 5d58d4c..ded68c5 100644 --- a/src/components/Select.vue +++ b/src/components/Select.vue @@ -53,28 +53,27 @@ -
    - +
      +
    • + + {{ getOptionLabel(option) }} + +
    • +
    • + Sorry, no matching options. +
    +
      @@ -84,6 +83,7 @@ import typeAheadPointer from '../mixins/typeAheadPointer' import ajax from '../mixins/ajax' import childComponents from './childComponents'; + import appendToBody from '../directives/appendToBody'; import uniqueId from '../utility/uniqueId'; /** @@ -94,6 +94,8 @@ mixins: [pointerScroll, typeAheadPointer, ajax], + directives: {appendToBody}, + props: { /** * Contains the currently selected value. Very similar to a @@ -517,6 +519,40 @@ * @return {Object} */ 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; + } } }, diff --git a/src/directives/appendToBody.js b/src/directives/appendToBody.js new file mode 100644 index 0000000..ec5f6ec --- /dev/null +++ b/src/directives/appendToBody.js @@ -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); + } + }, +} diff --git a/tests/unit/Dropdown.spec.js b/tests/unit/Dropdown.spec.js index 6777410..bd39649 100755 --- a/tests/unit/Dropdown.spec.js +++ b/tests/unit/Dropdown.spec.js @@ -137,7 +137,7 @@ describe("Toggling Dropdown", () => { expect(Select.vm.open).toEqual(true); 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__no-options')).toBeFalsy(); expect(Select.vm.stateClasses['vs--open']).toBeFalsy();