diff --git a/lib/KModal.vue b/lib/KModal.vue index dca8a956c..c917d8ee5 100644 --- a/lib/KModal.vue +++ b/lib/KModal.vue @@ -24,7 +24,7 @@ :style="[ modalSizeStyles, { background: $themeTokens.surface }, - containsKSelect ? { overflowY: 'unset' } : { overflowY: 'auto' }, + { overflowY: 'auto' }, ]" > @@ -65,7 +65,6 @@ ]" :class="{ 'scroll-shadow': scrollShadow, - 'contains-kselect': containsKSelect, }" > @@ -216,7 +215,6 @@ return { lastFocus: null, maxContentHeight: '1000', - containsKSelect: false, scrollShadow: false, delayedEnough: false, }; @@ -278,9 +276,6 @@ window.addEventListener('focus', this.focusElementTest, true); window.setTimeout(() => (this.delayedEnough = true), 500); - // if modal contains KSelect, special classes & styles will be applied - const kSelectCheck = document.querySelector('div.modal div.ui-select'); - this.containsKSelect = !!kSelectCheck; this.updateContentSectionStyle(); }, updated() { @@ -320,9 +315,7 @@ // make sure that overflow-y won't be updated to 'auto' if this function is running // for the first time (otherwise Firefox would add a vertical scrollbar right away) - // + don't apply if modal contains KSelect - // (otherwise KSelect will be trapped inside modal if KSelect is opened a second time) - if (this.$refs.content.clientHeight !== 0 && !this.containsKSelect) { + if (this.$refs.content.clientHeight !== 0) { // add a vertical scrollbar if content doesn't fit if (this.$refs.content.scrollHeight > this.$refs.content.clientHeight) { this.$refs.content.style.overflowY = 'auto'; @@ -453,10 +446,6 @@ 100% 10px; } - .contains-kselect { - overflow: unset; - } - .actions { padding: 24px; text-align: right; diff --git a/lib/KSelect/index.vue b/lib/KSelect/index.vue index 4437ebbce..1fb67ab9a 100644 --- a/lib/KSelect/index.vue +++ b/lib/KSelect/index.vue @@ -28,14 +28,15 @@ class="ui-select-label" :class="$computedClass({ ':focus': $coreOutline })" :tabindex="disabled ? null : '0'" - @click="toggleDropdown" @focus="onFocus" - @keydown.enter.prevent="openDropdown" - @keydown.space.prevent="openDropdown" + @blur="selectBlur" + @keydown.enter.prevent="onEnterSpace" + @keydown.space.prevent="onEnterSpace" @keydown.tab="onBlur" @keydown.up.prevent="highlightPreviousOption" @keydown.down.prevent="highlightNextOption" @keydown.self="highlightQuickMatch" + @keydown.esc.prevent="closeDropdown()" >
- - +
    + the option in the options dropdown -->
-
+
{ @@ -225,6 +232,7 @@ components: { UiIcon, KSelectOption, + Popper, }, model: { event: 'change', @@ -354,7 +362,17 @@ default: false, }, }, + setup() { + const { getOverlayEl } = _useOverlay(); + const appendToEl = ref(null); + + onMounted(() => { + const overlayEl = getOverlayEl(); + appendToEl.value = overlayEl; + }); + return { appendToEl }; + }, data() { return { query: '', @@ -362,11 +380,10 @@ isActive: false, isTouched: false, highlightedOption: null, - showDropdown: false, + isDropdownOpen: false, initialValue: JSON.stringify(this.value), quickMatchString: '', quickMatchTimeout: null, - scrollableAncestor: null, dropdownButtonBottom: 'auto', maxDropdownHeight: 256, // workaround for Keen-ui not displaying floating labels for empty objects @@ -455,6 +472,26 @@ return this.filteredOptions.length === 0; }, + dropdownPopperOptions() { + return { + placement: 'bottom-start', + modifiers: { + applyStyle: { + enabled: true, + fn: throttle((data) => { + data.styles.width = `${data.offsets.reference.width}px`; + Object.assign(data.instance.popper.style, data.styles); + return data; + }, 50), + }, + preventOverflow: { + enabled: true, + boundariesElement: 'viewport', + }, + }, + } + }, + submittedValue() { // Assuming that if there is no name, then there's no // need to computed the submittedValue @@ -544,23 +581,6 @@ this.highlightedOption = this.filteredOptions[0]; resetScroll(this.$refs.optionsList); }, - - showDropdown() { - if (this.showDropdown) { - this.onOpen(); - /** - * Emit on opening dropdown - */ - this.$emit('dropdown-open'); - } else { - this.onClose(); - /** - * Emit on closing dropdown - */ - this.$emit('dropdown-close'); - } - }, - query() { /** * Emits whenever the query value changes. @@ -598,25 +618,6 @@ }, mounted() { - document.addEventListener('click', this.onExternalClick); - // Find nearest scrollable ancestor - this.scrollableAncestor = this.$el; - while ( - (this.scrollableAncestor && - this.scrollableAncestor.clientHeight < this.scrollableAncestor.scrollHeight) || - !/auto|scroll/.test(window.getComputedStyle(this.scrollableAncestor).overflowY) - ) { - if (!this.scrollableAncestor.parentNode) { - break; - } - this.scrollableAncestor = this.scrollableAncestor.parentNode; - - // Stop if we reach the body-- tagName is likely uppercase - if (/body/i.test(this.scrollableAncestor.tagName)) { - break; - } - } - // look for KSelects nested within modals const allSelects = document.querySelectorAll('div.modal div.ui-select'); // create array from a nodelist [IE does not support Array.from()] @@ -624,10 +625,6 @@ this.isInsideModal = allSelectsArr.includes(this.$el); }, - beforeDestroy() { - document.removeEventListener('click', this.onExternalClick); - }, - methods: { setValue(value) { value = value ? value : this.multiple ? [] : ''; @@ -696,7 +693,6 @@ } this.highlightedOption = option; - this.openDropdown(); if (options.autoScroll) { const index = this.filteredOptions.findIndex(option => @@ -793,51 +789,25 @@ this.query = ''; }, - toggleDropdown() { - // if called on dropdown inside modal, - // dropdown will generally render above input/placeholder when opened, rather than below it. - // We want to render dropdown above input only in cases where there isn't enough space - // available beneath input, but when dropdown extends outside a modal - // the func doesn't work as intended - if (!this.isInsideModal) this.calculateSpaceBelow(); - - this[this.showDropdown ? 'closeDropdown' : 'openDropdown'](); - }, - - openDropdown() { - if (this.disabled || this.clearableState) { - return; - } - - if (this.highlightedIndex === -1) { - this.highlightNextOption(); - } - - this.showDropdown = true; - // IE: clicking label doesn't focus the select element - // to set isActive to true - if (!this.isActive) { - this.isActive = true; + onEnterSpace() { + if (this.isDropdownOpen) { + this.selectHighlighted(); + } else { + this.$refs.label.click(); } }, - closeDropdown(options = { autoBlur: false }) { - this.showDropdown = false; - this.query = ''; - if (!this.isTouched) { - this.isTouched = true; - this.$emit('touch'); - } - - if (options.autoBlur) { - this.isActive = false; - } else { + async closeDropdown(options = { autoBlur: false }) { + this.$refs.dropdownPopper.doClose(); + await this.$nextTick(); + if (!options.autoBlur) { this.$refs.label.focus(); + this.isActive = true; } }, onMouseover(option) { - if (this.showDropdown) { + if (this.isDropdownOpen) { this.highlightOption(option, { autoScroll: false }); } }, @@ -854,6 +824,12 @@ this.$emit('focus', e); }, + selectBlur() { + if (!this.isDropdownOpen) { + this.isActive = false; + } + }, + onBlur(e) { this.isActive = false; /** @@ -861,12 +837,18 @@ */ this.$emit('blur', e); - if (this.showDropdown) { + if (this.isDropdownOpen) { this.closeDropdown({ autoBlur: true }); } }, onOpen() { + if (this.highlightedIndex === -1) { + this.highlightNextOption(); + } + + this.isActive = true; + this.highlightedOption = this.multiple ? null : this.selection; this.$nextTick(() => { this.$refs['dropdown'].focus(); @@ -879,20 +861,21 @@ ); } }); + this.isDropdownOpen = true; + this.$emit('dropdown-open'); }, onClose() { - this.highlightedOption = this.multiple ? null : this.selection; - }, - - onExternalClick(e) { - if (!this.$el.contains(e.target)) { - if (this.showDropdown) { - this.closeDropdown({ autoBlur: true }); - } else if (this.isActive) { - this.isActive = false; - } + this.isActive = false; + this.query = ''; + if (!this.isTouched) { + this.isTouched = true; + this.$emit('touch'); } + + this.highlightedOption = this.multiple ? null : this.selection; + this.isDropdownOpen = false; + this.$emit('dropdown-close'); }, scrollOptionIntoView(optionEl) { @@ -916,23 +899,8 @@ resetTouched(options = { touched: false }) { this.isTouched = options.touched; }, - calculateSpaceBelow() { - // Get the height of element - const buttonHeight = this.$el.getBoundingClientRect().height; - - // Get the position of the element relative to the viewport - const buttonPosition = this.$el.getBoundingClientRect().top; - - // Check if there is enough space below element - // and update the "dropdownButtonBottom" data property accordingly - const notEnoughSpaceBelow = - buttonPosition > this.maxDropdownHeight && - this.scrollableAncestor.offsetHeight - buttonPosition < - buttonHeight + this.maxDropdownHeight; - - this.dropdownButtonBottom = notEnoughSpaceBelow ? buttonHeight + 'px' : 'auto'; - }, }, + }; @@ -1028,6 +996,7 @@ } &.is-disabled { + pointer-events: none; .ui-select-display { color: $ui-input-text-color--disabled; cursor: default; @@ -1111,16 +1080,10 @@ .ui-select-dropdown { @extend %dropshadow-2dp; - position: absolute; z-index: $z-index-dropdown; - display: block; - width: 100%; - min-width: rem-calc(180px); - padding: 0; - margin: 0; - margin-bottom: rem-calc(8px); list-style-type: none; outline: none; + margin-top: 2px; } .ui-select-options { diff --git a/lib/KTooltip/Popper.vue b/lib/KTooltip/Popper.vue index 094ef5bef..3eeb91552 100644 --- a/lib/KTooltip/Popper.vue +++ b/lib/KTooltip/Popper.vue @@ -201,10 +201,16 @@ } }, + /** + * @public + */ doShow() { this.showPopper = true; }, + /** + * @public + */ doClose() { this.showPopper = false; }, diff --git a/lib/composables/_useOverlay/index.js b/lib/composables/_useOverlay/index.js index a1f39e024..6439f1bfd 100644 --- a/lib/composables/_useOverlay/index.js +++ b/lib/composables/_useOverlay/index.js @@ -41,6 +41,10 @@ export default function _useOverlay() { * @returns {HTMLElement} The overlay container element #k-overlay */ function getOverlayEl() { + // skip for SSR + if (typeof window === 'undefined') { + return; + } // do not query DOM for performance reasons const overlayEl = window.overlayEl; // unlikely to happen, but just in case