Skip to content

Commit dc1be2b

Browse files
authored
Merge pull request #3656 from nextcloud/enh/a11y-select-dropdown-placement
2 parents a06c251 + f50ed60 commit dc1be2b

File tree

3 files changed

+202
-22
lines changed

3 files changed

+202
-22
lines changed

package-lock.json

Lines changed: 43 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"dist"
4141
],
4242
"dependencies": {
43+
"@floating-ui/dom": "^1.1.0",
4344
"@nextcloud/auth": "^2.0.0",
4445
"@nextcloud/axios": "^2.0.0",
4546
"@nextcloud/browser-storage": "^0.2.0",

src/components/NcSelect/NcSelect.vue

Lines changed: 158 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ const selectArray = [
6161
},
6262
},
6363

64+
{
65+
title: 'Simple (top placement)',
66+
props: {
67+
inputId: getRandomId(),
68+
placement: 'top',
69+
options: [
70+
'foo',
71+
'bar',
72+
'baz',
73+
'qux',
74+
'quux',
75+
],
76+
},
77+
},
78+
6479
{
6580
title: 'Multiple (with placeholder)',
6681
props: {
@@ -508,6 +523,14 @@ export default {
508523
<script>
509524
import VueSelect from 'vue-select'
510525
import 'vue-select/dist/vue-select.css'
526+
import {
527+
autoUpdate,
528+
computePosition,
529+
flip,
530+
limitShift,
531+
offset,
532+
shift,
533+
} from '@floating-ui/dom'
511534
512535
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
513536
import Close from 'vue-material-design-icons/Close.vue'
@@ -537,6 +560,32 @@ export default {
537560
// Add VueSelect props to $props
538561
...VueSelect.props,
539562
563+
/**
564+
* Append the dropdown element to the end of the body
565+
* and size/position it dynamically.
566+
*
567+
* @see https://vue-select.org/api/props.html#appendtobody
568+
*/
569+
appendToBody: {
570+
type: Boolean,
571+
default: true,
572+
},
573+
574+
/**
575+
* When `appendToBody` is true, this function is responsible for
576+
* positioning the drop down list.
577+
*
578+
* If a function is returned from `calculatePosition`, it will
579+
* be called when the drop down list is removed from the DOM.
580+
* This allows for any garbage collection you may need to do.
581+
*
582+
* @see https://vue-select.org/api/props.html#calculateposition
583+
*/
584+
calculatePosition: {
585+
type: Function,
586+
default: null,
587+
},
588+
540589
/**
541590
* Close the dropdown when selecting an option
542591
*
@@ -673,6 +722,16 @@ export default {
673722
default: '',
674723
},
675724
725+
/**
726+
* When `appendToBody` is true, this sets the placement of the dropdown
727+
*
728+
* @type {'bottom' | 'top'}
729+
*/
730+
placement: {
731+
type: String,
732+
default: 'bottom',
733+
},
734+
676735
/**
677736
* Enable the user selector with avatars
678737
*
@@ -723,6 +782,66 @@ export default {
723782
},
724783
725784
computed: {
785+
localCalculatePosition() {
786+
if (this.calculatePosition !== null) {
787+
return this.calculatePosition
788+
}
789+
790+
return (dropdownMenu, component, { width }) => {
791+
dropdownMenu.style.width = width
792+
793+
const addClass = {
794+
name: 'addClass',
795+
fn(_middlewareArgs) {
796+
dropdownMenu.classList.add('vs__dropdown-menu--floating')
797+
return {}
798+
},
799+
}
800+
801+
const togglePlacementClass = {
802+
name: 'togglePlacementClass',
803+
fn({ placement }) {
804+
component.$el.classList.toggle(
805+
'select--drop-up',
806+
placement === 'top',
807+
)
808+
dropdownMenu.classList.toggle(
809+
'vs__dropdown-menu--floating-placement-top',
810+
placement === 'top',
811+
)
812+
return {}
813+
},
814+
}
815+
816+
const updatePosition = () => {
817+
computePosition(component.$refs.toggle, dropdownMenu, {
818+
placement: this.placement,
819+
middleware: [
820+
offset(-1),
821+
addClass,
822+
togglePlacementClass,
823+
// Match popperjs default collision prevention behavior by appending the following middleware in order
824+
flip(),
825+
shift({ limiter: limitShift() }),
826+
],
827+
}).then(({ x, y }) => {
828+
Object.assign(dropdownMenu.style, {
829+
left: `${x}px`,
830+
top: `${y}px`,
831+
})
832+
})
833+
}
834+
835+
const cleanup = autoUpdate(
836+
component.$refs.toggle,
837+
dropdownMenu,
838+
updatePosition,
839+
)
840+
841+
return cleanup
842+
}
843+
},
844+
726845
localFilterBy() {
727846
if (this.filterBy !== null) {
728847
return this.filterBy
@@ -752,17 +871,20 @@ export default {
752871
propsToForward() {
753872
const {
754873
// Custom overrides of vue-select props
874+
calculatePosition,
755875
filterBy,
756876
label,
757877
// Props handled by the component itself
758878
noWrap,
879+
placement,
759880
userSelect,
760881
// Props to forward
761882
...initialPropsToForward
762883
} = this.$props
763884
764885
const propsToForward = {
765886
...initialPropsToForward,
887+
calculatePosition: this.localCalculatePosition,
766888
label: this.localLabel,
767889
}
768890
@@ -776,8 +898,8 @@ export default {
776898
}
777899
</script>
778900

779-
<style lang="scss" scoped>
780-
.select {
901+
<style lang="scss">
902+
:root {
781903
/* Set custom vue-select CSS variables */
782904
783905
/* Search Input */
@@ -826,26 +948,54 @@ export default {
826948
827949
/* Transitions */
828950
--vs-transition-duration: 0ms;
951+
}
829952
953+
.v-select.select {
830954
/* Override default vue-select styles */
831955
min-height: $clickable-area;
832956
min-width: 260px;
833957
margin: 0;
834958
959+
.vs__selected {
960+
min-height: 36px;
961+
padding: 0 0.5em;
962+
}
963+
964+
.vs__clear {
965+
margin-right: 2px;
966+
}
967+
835968
&--no-wrap {
836-
&:deep(.vs__selected-options) {
969+
.vs__selected-options {
837970
flex-wrap: nowrap;
838971
overflow: auto;
839972
}
840973
}
841974
842-
&:deep(.vs__selected) {
843-
min-height: 36px;
844-
padding: 0 0.5em;
975+
&--drop-up {
976+
&.vs--open {
977+
.vs__dropdown-toggle {
978+
border-radius: 0 0 var(--vs-border-radius) var(--vs-border-radius);
979+
border-top-color: transparent;
980+
border-bottom-color: var(--vs-border-color);
981+
}
982+
}
845983
}
984+
}
846985
847-
&:deep(.vs__clear) {
848-
margin-right: 2px;
986+
.vs__dropdown-menu {
987+
&--floating {
988+
width: max-content;
989+
position: absolute;
990+
top: 0;
991+
left: 0;
992+
993+
&-placement-top {
994+
border-radius: var(--vs-border-radius) var(--vs-border-radius) 0 0;
995+
border-top-style: var(--vs-border-style);
996+
border-bottom-style: none;
997+
box-shadow: 0px -1px 1px 0px var(--color-box-shadow);
998+
}
849999
}
8501000
}
8511001
</style>

0 commit comments

Comments
 (0)