This page has intended scroll so you can see multiple scenarios.
+
+
@@ -25,7 +39,7 @@
v-close-popup
@click="showNotify()"
>
-
Label
+
Label {{ n }}/20
@@ -37,7 +51,7 @@
ref="popover11"
persistent
transition-show="jump-up"
- anchor="top right"
+ anchor="top end"
@show="log('@show popover11 decoupled')"
@hide="log('@hide popover11 decoupled')"
>
@@ -50,7 +64,7 @@
v-close-popup
@click="showNotify()"
>
-
Label
+
Label {{ n }}/20
@@ -70,14 +84,14 @@
v-close-popup
@click="showNotify()"
>
-
Label
+
Label {{ n }}/20
-
+
- Label
+ Label {{ n }}/20
-
+
+
@@ -140,7 +155,7 @@
- Menu Item {{ n }}
+ Menu Item {{ n }}/5
Submenu Label
@@ -150,7 +165,7 @@
- Menu Item {{ n }}
+ Menu Item {{ n }}/5
Close dialog
@@ -166,7 +181,7 @@
-
+
Settings
@@ -194,6 +209,82 @@
+
+
+
+
+
+ New tab
+
+
+ New incognito tab
+
+
+
+ Recent tabs
+
+
+ History
+
+
+ Downloads
+
+
+
+ Settings
+
+
+
+ Help & Feedback
+
+
+
+
+
+
+
+
+
+
+ New tab
+
+
+
+
+ New incognito tab
+
+
+
+
+
+ Recent tabs
+
+
+
+
+ History
+
+
+
+
+ Downloads
+
+
+
+
+
+ Settings
+
+
+
+
+
+ Help & Feedback
+
+
+
+
+
@@ -205,7 +296,9 @@
:cover="cover"
:anchor="anchor"
:self="self"
+ :offset="calculatedOffset"
auto-close
+ v-bind="transition"
>
- Label
+ Label {{ n }}/50
@@ -225,6 +318,8 @@
:cover="cover"
:anchor="anchor"
:self="self"
+ :offset="calculatedOffset"
+ v-bind="transition"
>
- Label
+ Label {{ n }}/5
@@ -266,6 +361,7 @@
+
@@ -317,12 +413,18 @@
-
+
-
+
- Label
+ Label {{ n }}/5
With model: {{ menuModelTouch }}
-
+
- Label
+ Label {{ n }}/5
@@ -371,7 +480,7 @@
clickable
@click="showNotify()"
>
- Label
+ Label {{ n }}/5
@@ -389,7 +498,7 @@
clickable
@click="showNotify()"
>
- Label
+ Label {{ n }}/5
@@ -432,7 +541,7 @@
clickable
@click="showNotify()"
>
- Label
+ Label {{ n }}/5
@@ -463,7 +572,7 @@
-
+
-
+
-
+
@@ -521,7 +630,7 @@
-
+
Left col
@@ -536,8 +645,8 @@
-
-
+
+
-
-
+
+
@@ -558,8 +667,8 @@
-
-
+
+
- Label
+ Label {{ n }}/20
@@ -588,6 +697,7 @@ export default {
gigi: '',
fit: false,
cover: false,
+ offset: false,
toggle: true,
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
selfOrigin: { vertical: 'top', horizontal: 'left' },
@@ -612,7 +722,15 @@ export default {
dialog2: false,
mobileData: true,
- bluetooth: false
+ bluetooth: false,
+
+ transitions: [
+ { label: 'Flip', value: { transitionShow: 'flip-right', transitionHide: 'flip-left' } },
+ { label: 'Scale', value: { transitionShow: 'scale', transitionHide: 'scale' } },
+ { label: 'Jump', value: { transitionShow: 'jump-down', transitionHide: 'jump-up' } },
+ { label: 'Rotate', value: { transitionShow: 'rotate', transitionHide: 'rotate' } }
+ ],
+ transition: null
}
},
computed: {
@@ -621,6 +739,9 @@ export default {
},
self () {
return `${this.selfOrigin.vertical} ${this.selfOrigin.horizontal}`
+ },
+ calculatedOffset () {
+ return this.offset === true ? [50, 50] : void 0
}
},
methods: {
diff --git a/ui/dev/src/pages/components/popup-proxy.vue b/ui/dev/src/pages/components/popup-proxy.vue
index 308418403e48..a4464d695528 100644
--- a/ui/dev/src/pages/components/popup-proxy.vue
+++ b/ui/dev/src/pages/components/popup-proxy.vue
@@ -154,7 +154,7 @@
-
+
diff --git a/ui/dev/src/pages/form/date-part1-basic.vue b/ui/dev/src/pages/form/date-part1-basic.vue
index 1251c102ed95..147ed13ba647 100644
--- a/ui/dev/src/pages/form/date-part1-basic.vue
+++ b/ui/dev/src/pages/form/date-part1-basic.vue
@@ -247,7 +247,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -131,11 +142,33 @@
color="primary"
@click="fullscreenNone"
/>
+
+
+
+
+
+
+
+
+
+
+
Fullscreen inside modal dialog
Hover here for tooltip Some tooltip
+
+
+
+
+
+
+
+
+
+
+
@@ -170,6 +203,17 @@
color="primary"
@click="fullscreenNone"
/>
+
+
+
+
+
+
+
+
+
+
+
@@ -209,6 +253,17 @@
color="primary"
@click="fullscreenNone"
/>
+
+
+
+
+
+
+
+
+
+
+
@@ -261,11 +316,33 @@
color="primary"
@click="fullscreenNone"
/>
+
+
+
+
+
+
+
+
+
+
+
Fullscreen inside seamless dialog (outside fullscreen el)
Hover here for tooltip Some tooltip
+
+
+
+
+
+
+
+
+
+
+
@@ -279,6 +356,9 @@ export default {
model: null,
options: ['Option 1', 'Option 2', 'Option 3'],
+ date: '2019/03/01',
+ proxyDate: '2019/03/01',
+
dialog1: false,
dialog2: false,
dialog3: false,
@@ -350,6 +430,14 @@ export default {
this.fullscreenNone()
}
})
+ },
+
+ updateProxy () {
+ this.proxyDate = this.date
+ },
+
+ save () {
+ this.date = this.proxyDate
}
}
}
diff --git a/ui/dev/src/pages/web-tests/menu-positioning.vue b/ui/dev/src/pages/web-tests/menu-positioning.vue
new file mode 100644
index 000000000000..e03e6b34091c
--- /dev/null
+++ b/ui/dev/src/pages/web-tests/menu-positioning.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+ Wide content wide content wide content wide content wide content wide content
+ Wide content wide content wide content wide content wide content wide content
+
+ Wide content wide content wide content wide content wide content wide content
+
+
+
+
+
+
+
+
+
+
+
+ Running Quasar v{{ version }}
+
+
+
+ Space below
+
+
+
+
diff --git a/ui/src/components/field/QField.js b/ui/src/components/field/QField.js
index 2f8f2bc668a8..5d315a3ad71b 100644
--- a/ui/src/components/field/QField.js
+++ b/ui/src/components/field/QField.js
@@ -540,7 +540,6 @@ export default Vue.extend({
render (h) {
this.__onPreRender !== void 0 && this.__onPreRender()
- this.__onPostRender !== void 0 && this.$nextTick(this.__onPostRender)
return h('label', {
staticClass: 'q-field q-validation-component row no-wrap items-start',
diff --git a/ui/src/components/menu/QMenu.js b/ui/src/components/menu/QMenu.js
index 7cc9df593ac8..53a4510667b0 100644
--- a/ui/src/components/menu/QMenu.js
+++ b/ui/src/components/menu/QMenu.js
@@ -65,7 +65,19 @@ export default Vue.extend({
default: void 0
},
- touchPosition: Boolean,
+ touchPosition: {
+ type: Boolean,
+ default: null
+ },
+
+ minHeight: {
+ type: String,
+ default: null
+ },
+ minWidth: {
+ type: String,
+ default: null
+ },
maxHeight: {
type: String,
@@ -157,7 +169,7 @@ export default Vue.extend({
this.absoluteOffset = void 0
- if (evt !== void 0 && (this.touchPosition || this.contextMenu)) {
+ if (evt !== void 0 && (this.touchPosition === true || (this.touchPosition !== false && this.contextMenu))) {
const pos = position(evt)
if (pos.left !== void 0) {
@@ -175,15 +187,20 @@ export default Vue.extend({
this.$el.dispatchEvent(create('popup-show', { bubbles: true }))
- // IE can have null document.activeElement
- if (this.noFocus !== true && document.activeElement !== null) {
- document.activeElement.blur()
- }
+ if (this.noFocus !== true) {
+ // IE can have null document.activeElement
+ document.activeElement !== null && document.activeElement.blur()
- this.__nextTick(() => {
- this.updatePosition()
- this.noFocus !== true && this.focus()
- })
+ this.__nextTick(() => {
+ this.focus()
+ this.updatePosition()
+ })
+ }
+ else {
+ this.__nextTick(() => {
+ this.updatePosition()
+ })
+ }
this.__setTimeout(() => {
// required in order to avoid the "double-tap needed" issue
@@ -194,7 +211,6 @@ export default Vue.extend({
this.__portal.$el.click()
}
- this.updatePosition()
this.$emit('show', evt)
}, 300)
},
@@ -239,16 +255,12 @@ export default Vue.extend({
},
__unconfigureScrollTarget () {
- if (this.__scrollTarget !== void 0) {
- this.__changeScrollEvent(this.__scrollTarget)
- this.__scrollTarget = void 0
- }
+ this.__changeScrollEvent()
},
__configureScrollTarget () {
- if (this.anchorEl !== void 0 || this.scrollTarget !== void 0) {
- this.__scrollTarget = getScrollTarget(this.anchorEl, this.scrollTarget)
- this.__changeScrollEvent(this.__scrollTarget, this.updatePosition)
+ if (this.showing && (this.anchorEl !== void 0 || this.scrollTarget !== void 0)) {
+ this.__changeScrollEvent(this.updatePosition, getScrollTarget(this.anchorEl, this.scrollTarget))
}
},
@@ -276,17 +288,26 @@ export default Vue.extend({
return
}
- setPosition({
- el,
- offset: this.offset,
- anchorEl: this.anchorEl,
- anchorOrigin: this.anchorOrigin,
- selfOrigin: this.selfOrigin,
- absoluteOffset: this.absoluteOffset,
- fit: this.fit,
- cover: this.cover,
- maxHeight: this.maxHeight,
- maxWidth: this.maxWidth
+ this.settingPosition !== void 0 && cancelAnimationFrame(this.settingPosition)
+ this.settingPosition = requestAnimationFrame(() => {
+ const touchPosition = this.touchPosition === true || (this.touchPosition !== false && this.contextMenu)
+
+ setPosition({
+ el,
+ offset: this.offset,
+ anchorEl: this.anchorEl,
+ anchorOrigin: this.anchorOrigin,
+ selfOrigin: this.selfOrigin,
+ absoluteOffset: this.absoluteOffset,
+ fit: touchPosition !== true && this.fit === true,
+ cover: touchPosition !== true && this.cover === true,
+ minHeight: this.minHeight,
+ minWidth: this.minWidth,
+ maxHeight: this.maxHeight,
+ maxWidth: this.maxWidth,
+ rtl: this.$q.lang.rtl
+ })
+ this.settingPosition = void 0
})
},
@@ -299,7 +320,7 @@ export default Vue.extend({
// always prevent touch event
e.type === 'touchstart' ||
// prevent click if it's on a dialog backdrop
- targetClassList.contains('q-dialog__backdrop')
+ targetClassList.contains('q-dialog__backdrop') === true
) {
stopAndPrevent(e)
this.$q.interaction.preventClick(e.target, true)
@@ -313,18 +334,22 @@ export default Vue.extend({
props: { name: this.transition }
}, [
this.showing === true ? h('div', {
- ref: 'inner',
- staticClass: 'q-menu q-position-engine scroll' + this.menuClass,
- class: this.contentClass,
- style: this.contentStyle,
- attrs: this.attrs,
- on: this.onEvents,
+ class: 'q-menu__container column no-pointer-events',
directives: [{
name: 'click-outside',
value: this.__onClickOutside,
arg: this.anchorEl
}]
- }, slot(this, 'default')) : null
+ }, [
+ h('div', {
+ ref: 'inner',
+ staticClass: 'q-menu scroll all-pointer-events' + this.menuClass,
+ class: this.contentClass,
+ style: this.contentStyle,
+ attrs: this.attrs,
+ on: this.onEvents
+ }, slot(this, 'default'))
+ ]) : null
])
}
},
@@ -334,6 +359,8 @@ export default Vue.extend({
},
beforeDestroy () {
+ this.settingPosition !== void 0 && cancelAnimationFrame(this.settingPosition)
+
// When the menu is destroyed while open we can only emit the event on anchorEl
if (this.showing === true && this.anchorEl !== void 0) {
this.anchorEl.dispatchEvent(
diff --git a/ui/src/components/menu/QMenu.json b/ui/src/components/menu/QMenu.json
index 7a222f18c9b0..bcea6e4bdd9d 100644
--- a/ui/src/components/menu/QMenu.json
+++ b/ui/src/components/menu/QMenu.json
@@ -112,6 +112,20 @@
"category": "behavior"
},
+ "min-height": {
+ "extends": "size",
+ "desc": "The minimum height of the menu; Size in CSS units, including unit name",
+ "category": "style",
+ "addedIn": "v1.15.5"
+ },
+
+ "min-width": {
+ "extends": "size",
+ "desc": "The minimum width of the menu; Size in CSS units, including unit name",
+ "category": "style",
+ "addedIn": "v1.15.5"
+ },
+
"max-height": {
"extends": "size",
"desc": "The maximum height of the menu; Size in CSS units, including unit name",
diff --git a/ui/src/components/menu/QMenu.sass b/ui/src/components/menu/QMenu.sass
index 86fa23c1537f..b7335a3919f2 100644
--- a/ui/src/components/menu/QMenu.sass
+++ b/ui/src/components/menu/QMenu.sass
@@ -1,16 +1,17 @@
.q-menu
- position: fixed !important
display: inline-block
max-width: $menu-max-width
-
+ max-height: 65vh
+ outline: 0
+ border-radius: $generic-border-radius
box-shadow: $menu-box-shadow
background: $menu-background
- border-radius: $generic-border-radius
- overflow-y: auto
- overflow-x: hidden
- outline: 0
- max-height: 65vh
- z-index: $z-menu
+ opacity: 0
+
+ &__container
+ position: fixed
+ z-index: $z-menu
+ contain: layout
&--square
border-radius: 0
diff --git a/ui/src/components/menu/QMenu.styl b/ui/src/components/menu/QMenu.styl
index 86fa23c1537f..b7335a3919f2 100644
--- a/ui/src/components/menu/QMenu.styl
+++ b/ui/src/components/menu/QMenu.styl
@@ -1,16 +1,17 @@
.q-menu
- position: fixed !important
display: inline-block
max-width: $menu-max-width
-
+ max-height: 65vh
+ outline: 0
+ border-radius: $generic-border-radius
box-shadow: $menu-box-shadow
background: $menu-background
- border-radius: $generic-border-radius
- overflow-y: auto
- overflow-x: hidden
- outline: 0
- max-height: 65vh
- z-index: $z-menu
+ opacity: 0
+
+ &__container
+ position: fixed
+ z-index: $z-menu
+ contain: layout
&--square
border-radius: 0
diff --git a/ui/src/components/select/QSelect.js b/ui/src/components/select/QSelect.js
index 665e29ca1c2e..c8457d1a8445 100755
--- a/ui/src/components/select/QSelect.js
+++ b/ui/src/components/select/QSelect.js
@@ -851,7 +851,7 @@ export default Vue.extend({
? this.$refs.menuContent
: (
this.$refs.menu !== void 0 && this.$refs.menu.__portal !== void 0
- ? this.$refs.menu.__portal.$el
+ ? (this.$refs.menu.__portal.$el.children || [])[0] // fallback [] for IE
: void 0
)
},
@@ -1470,14 +1470,10 @@ export default Vue.extend({
: this.transitionShow
},
- __onPostRender () {
+ updateMenuPosition () {
if (this.dialog === false && this.$refs.menu !== void 0) {
this.$refs.menu.updatePosition()
}
- },
-
- updateMenuPosition () {
- this.__onPostRender()
}
},
diff --git a/ui/src/components/select/QSelect.sass b/ui/src/components/select/QSelect.sass
index c4b83af012ca..7f93a47fe269 100644
--- a/ui/src/components/select/QSelect.sass
+++ b/ui/src/components/select/QSelect.sass
@@ -62,7 +62,7 @@
// addressbar always gets shown, so need to slim this down
body.mobile:not(.native-mobile) .q-select__dialog
- max-height: calc(100vh - 108px) !important
+ max-height: calc(100vh - 168px) !important
body.platform-android
@@ -82,4 +82,4 @@ body.platform-ios
max-height: 47vh !important
&:not(.native-mobile) .q-dialog__inner--top .q-select__dialog--focused
- max-height: 50vh !important
+ max-height: 48vh !important
diff --git a/ui/src/components/select/QSelect.styl b/ui/src/components/select/QSelect.styl
index c4b83af012ca..7f93a47fe269 100644
--- a/ui/src/components/select/QSelect.styl
+++ b/ui/src/components/select/QSelect.styl
@@ -62,7 +62,7 @@
// addressbar always gets shown, so need to slim this down
body.mobile:not(.native-mobile) .q-select__dialog
- max-height: calc(100vh - 108px) !important
+ max-height: calc(100vh - 168px) !important
body.platform-android
@@ -82,4 +82,4 @@ body.platform-ios
max-height: 47vh !important
&:not(.native-mobile) .q-dialog__inner--top .q-select__dialog--focused
- max-height: 50vh !important
+ max-height: 48vh !important
diff --git a/ui/src/components/tooltip/QTooltip.js b/ui/src/components/tooltip/QTooltip.js
index 9db655b0e4c9..c986ada6d59f 100644
--- a/ui/src/components/tooltip/QTooltip.js
+++ b/ui/src/components/tooltip/QTooltip.js
@@ -20,6 +20,15 @@ export default Vue.extend({
mixins: [ AnchorMixin, ModelToggleMixin, PortalMixin, TransitionMixin ],
props: {
+ minHeight: {
+ type: String,
+ default: null
+ },
+ minWidth: {
+ type: String,
+ default: null
+ },
+
maxHeight: {
type: String,
default: null
@@ -85,13 +94,6 @@ export default Vue.extend({
__show (evt) {
this.__showPortal()
- this.__nextTick(() => {
- this.observer = new MutationObserver(() => this.updatePosition())
- this.observer.observe(this.__portal.$el, { attributes: false, childList: true, characterData: true, subtree: true })
- this.updatePosition()
- this.__configureScrollTarget()
- })
-
if (this.unwatch === void 0) {
this.unwatch = this.$watch(
() => this.$q.screen.width + '|' + this.$q.screen.height + '|' + this.self + '|' + this.anchor + '|' + this.$q.lang.rtl,
@@ -99,6 +101,11 @@ export default Vue.extend({
)
}
+ this.__nextTick(() => {
+ this.updatePosition()
+ this.__configureScrollTarget()
+ })
+
this.__setTimeout(() => {
this.$emit('show', evt)
}, 300)
@@ -114,11 +121,6 @@ export default Vue.extend({
},
__anchorCleanup () {
- if (this.observer !== void 0) {
- this.observer.disconnect()
- this.observer = void 0
- }
-
if (this.unwatch !== void 0) {
this.unwatch()
this.unwatch = void 0
@@ -140,14 +142,21 @@ export default Vue.extend({
return
}
- setPosition({
- el,
- offset: this.offset,
- anchorEl: this.anchorEl,
- anchorOrigin: this.anchorOrigin,
- selfOrigin: this.selfOrigin,
- maxHeight: this.maxHeight,
- maxWidth: this.maxWidth
+ this.settingPosition !== void 0 && cancelAnimationFrame(this.settingPosition)
+ this.settingPosition = requestAnimationFrame(() => {
+ setPosition({
+ el,
+ offset: this.offset,
+ anchorEl: this.anchorEl,
+ anchorOrigin: this.anchorOrigin,
+ selfOrigin: this.selfOrigin,
+ minHeight: this.minHeight,
+ minWidth: this.minWidth,
+ maxHeight: this.maxHeight,
+ maxWidth: this.maxWidth,
+ rtl: this.$q.lang.rtl
+ })
+ this.settingPosition = void 0
})
},
@@ -201,20 +210,16 @@ export default Vue.extend({
},
__unconfigureScrollTarget () {
- if (this.__scrollTarget !== void 0) {
- this.__changeScrollEvent(this.__scrollTarget)
- this.__scrollTarget = void 0
- }
+ this.__changeScrollEvent()
},
__configureScrollTarget () {
- if (this.anchorEl !== void 0 || this.scrollTarget !== void 0) {
- this.__scrollTarget = getScrollTarget(this.anchorEl, this.scrollTarget)
+ if (this.showing === true && (this.anchorEl !== void 0 || this.scrollTarget !== void 0)) {
const fn = this.noParentEvent === true
? this.updatePosition
: this.hide
- this.__changeScrollEvent(this.__scrollTarget, fn)
+ this.__changeScrollEvent(fn, getScrollTarget(this.anchorEl, this.scrollTarget))
}
},
@@ -223,18 +228,26 @@ export default Vue.extend({
props: { name: this.transition }
}, [
this.showing === true ? h('div', {
- staticClass: 'q-tooltip q-tooltip--style q-position-engine no-pointer-events',
- class: this.contentClass,
- style: this.contentStyle,
- attrs: {
- role: 'complementary'
- }
- }, slot(this, 'default')) : null
+ class: 'q-tooltip__container column no-pointer-events'
+ }, [
+ h('div', {
+ staticClass: 'q-tooltip q-tooltip--style scroll',
+ class: this.contentClass,
+ style: this.contentStyle,
+ attrs: {
+ role: 'complementary'
+ }
+ }, slot(this, 'default'))
+ ]) : null
])
}
},
mounted () {
this.__processModelChange(this.value)
+ },
+
+ beforeDestroy () {
+ this.settingPosition !== void 0 && cancelAnimationFrame(this.settingPosition)
}
})
diff --git a/ui/src/components/tooltip/QTooltip.json b/ui/src/components/tooltip/QTooltip.json
index 539bab5687cf..e8a46937d986 100644
--- a/ui/src/components/tooltip/QTooltip.json
+++ b/ui/src/components/tooltip/QTooltip.json
@@ -6,16 +6,30 @@
},
"props": {
+ "min-height": {
+ "extends": "size",
+ "desc": "The minimum height of the Tooltip; Size in CSS units, including unit name",
+ "category": "style",
+ "addedIn": "v1.15.5"
+ },
+
+ "min-width": {
+ "extends": "size",
+ "desc": "The minimum width of the Tooltip; Size in CSS units, including unit name",
+ "category": "style",
+ "addedIn": "v1.15.5"
+ },
+
"max-height": {
"extends": "size",
"desc": "The maximum height of the Tooltip; Size in CSS units, including unit name",
- "category": "content"
+ "category": "style"
},
"max-width": {
"extends": "size",
"desc": "The maximum width of the Tooltip; Size in CSS units, including unit name",
- "category": "content"
+ "category": "style"
},
"transition-show": {
diff --git a/ui/src/components/tooltip/QTooltip.sass b/ui/src/components/tooltip/QTooltip.sass
index 7b55c375afba..4987ea78f9eb 100644
--- a/ui/src/components/tooltip/QTooltip.sass
+++ b/ui/src/components/tooltip/QTooltip.sass
@@ -1,17 +1,19 @@
-.q-tooltip--style
- font-size: $tooltip-fontsize
- color: $tooltip-color
- background: $tooltip-background
- border-radius: $tooltip-border-radius
- text-transform: none
- font-weight: normal
-
.q-tooltip
- z-index: $z-tooltip
- position: fixed !important
- overflow-y: auto
- overflow-x: hidden
padding: $tooltip-padding
+ opacity: 0
+
+ &__container
+ position: fixed
+ z-index: $z-tooltip
+ contain: layout
+
+ &--style
+ font-size: $tooltip-fontsize
+ color: $tooltip-color
+ background: $tooltip-background
+ border-radius: $tooltip-border-radius
+ text-transform: none
+ font-weight: normal
@media (max-width: $breakpoint-xs-max)
font-size: $tooltip-mobile-fontsize
diff --git a/ui/src/components/tooltip/QTooltip.styl b/ui/src/components/tooltip/QTooltip.styl
index 7b55c375afba..4987ea78f9eb 100644
--- a/ui/src/components/tooltip/QTooltip.styl
+++ b/ui/src/components/tooltip/QTooltip.styl
@@ -1,17 +1,19 @@
-.q-tooltip--style
- font-size: $tooltip-fontsize
- color: $tooltip-color
- background: $tooltip-background
- border-radius: $tooltip-border-radius
- text-transform: none
- font-weight: normal
-
.q-tooltip
- z-index: $z-tooltip
- position: fixed !important
- overflow-y: auto
- overflow-x: hidden
padding: $tooltip-padding
+ opacity: 0
+
+ &__container
+ position: fixed
+ z-index: $z-tooltip
+ contain: layout
+
+ &--style
+ font-size: $tooltip-fontsize
+ color: $tooltip-color
+ background: $tooltip-background
+ border-radius: $tooltip-border-radius
+ text-transform: none
+ font-weight: normal
@media (max-width: $breakpoint-xs-max)
font-size: $tooltip-mobile-fontsize
diff --git a/ui/src/css/core/helpers.sass b/ui/src/css/core/helpers.sass
index 9126d8e7244c..0550db64cfe0 100644
--- a/ui/src/css/core/helpers.sass
+++ b/ui/src/css/core/helpers.sass
@@ -36,6 +36,14 @@
.q-body--force-scrollbar
overflow-y: scroll
+.q-portal__clone
+ position: fixed !important
+ transform: none !important
+ transition: none !important
+ flex-shrink: 0 !important
+ opacity: 0 !important
+ z-index: -1 !important
+
.q-no-input-spinner
-moz-appearance: textfield !important
&::-webkit-outer-spin-button,
diff --git a/ui/src/css/core/helpers.styl b/ui/src/css/core/helpers.styl
index 9126d8e7244c..0550db64cfe0 100644
--- a/ui/src/css/core/helpers.styl
+++ b/ui/src/css/core/helpers.styl
@@ -36,6 +36,14 @@
.q-body--force-scrollbar
overflow-y: scroll
+.q-portal__clone
+ position: fixed !important
+ transform: none !important
+ transition: none !important
+ flex-shrink: 0 !important
+ opacity: 0 !important
+ z-index: -1 !important
+
.q-no-input-spinner
-moz-appearance: textfield !important
&::-webkit-outer-spin-button,
diff --git a/ui/src/css/core/positioning.sass b/ui/src/css/core/positioning.sass
index 2a04f81666a5..0334aa858550 100644
--- a/ui/src/css/core/positioning.sass
+++ b/ui/src/css/core/positioning.sass
@@ -92,10 +92,3 @@
margin-right: 12px
.on-right
margin-left: 12px
-
-/* internal: */
-.q-position-engine
- margin-top: var(--q-pe-top, 0) !important
- margin-left: var(--q-pe-left, 0) !important
- will-change: auto
- visibility: collapse // needed for animation - is removed on first positioning
diff --git a/ui/src/css/core/positioning.styl b/ui/src/css/core/positioning.styl
index 2a04f81666a5..0334aa858550 100644
--- a/ui/src/css/core/positioning.styl
+++ b/ui/src/css/core/positioning.styl
@@ -92,10 +92,3 @@
margin-right: 12px
.on-right
margin-left: 12px
-
-/* internal: */
-.q-position-engine
- margin-top: var(--q-pe-top, 0) !important
- margin-left: var(--q-pe-left, 0) !important
- will-change: auto
- visibility: collapse // needed for animation - is removed on first positioning
diff --git a/ui/src/mixins/anchor.js b/ui/src/mixins/anchor.js
index 1c2421e00ca3..d14dfeb23ba0 100644
--- a/ui/src/mixins/anchor.js
+++ b/ui/src/mixins/anchor.js
@@ -1,8 +1,18 @@
import { clearSelection } from '../utils/selection.js'
-import { addEvt, cleanEvt, prevent, listenOpts } from '../utils/event.js'
+import { addEvt, cleanEvt, prevent, listenOpts, eventOnAncestors } from '../utils/event.js'
import { getTouchTarget } from '../utils/touch.js'
import { isKeyCode } from '../utils/key-composition.js'
+const scrollListenerHandlers = []
+
+function scrollEventDispatcher (e) {
+ scrollListenerHandlers
+ .slice()
+ .forEach(fn => {
+ eventOnAncestors(e, fn.scrollTarget) === true && fn(e)
+ })
+}
+
export default {
props: {
target: {
@@ -168,17 +178,32 @@ export default {
}
},
- __changeScrollEvent (scrollTarget, fn) {
- const fnProp = `${fn !== void 0 ? 'add' : 'remove'}EventListener`
- const fnHandler = fn !== void 0 ? fn : this.__scrollFn
+ __changeScrollEvent (fn, scrollTarget) {
+ const hadHandlers = scrollListenerHandlers.length > 0
+
+ if (this.__scrollFn !== void 0) {
+ const index = scrollListenerHandlers.indexOf(this.__scrollFn)
+
+ if (index > -1) {
+ scrollListenerHandlers.splice(index, 1)
+ }
- if (scrollTarget !== window) {
- scrollTarget[fnProp]('scroll', fnHandler, listenOpts.passive)
+ this.__scrollFn = void 0
}
- window[fnProp]('scroll', fnHandler, listenOpts.passive)
+ if (fn !== void 0 && scrollTarget !== null && scrollTarget !== void 0) {
+ fn.scrollTarget = scrollTarget === window ? document : scrollTarget
+ scrollListenerHandlers.push(fn)
- this.__scrollFn = fn
+ this.__scrollFn = fn
+ }
+
+ if (hadHandlers === true && scrollListenerHandlers.length === 0) {
+ window.removeEventListener('scroll', scrollEventDispatcher, listenOpts.passiveCapture)
+ }
+ else if (hadHandlers === false && scrollListenerHandlers.length > 0) {
+ window.addEventListener('scroll', scrollEventDispatcher, listenOpts.passiveCapture)
+ }
}
},
@@ -188,10 +213,8 @@ export default {
typeof this.__unconfigureScrollTarget === 'function'
) {
this.noParentEventWatcher = this.$watch('noParentEvent', () => {
- if (this.__scrollTarget !== void 0) {
- this.__unconfigureScrollTarget()
- this.__configureScrollTarget()
- }
+ this.__unconfigureScrollTarget()
+ this.__configureScrollTarget()
})
}
},
diff --git a/ui/src/mixins/virtual-scroll.js b/ui/src/mixins/virtual-scroll.js
index bb8bde7b3314..03ee0acec1eb 100644
--- a/ui/src/mixins/virtual-scroll.js
+++ b/ui/src/mixins/virtual-scroll.js
@@ -1,4 +1,5 @@
import debounce from '../utils/debounce.js'
+import { isBuggyRTLScroll } from '../utils/scroll.js'
const aggBucketSize = 1000
@@ -13,30 +14,6 @@ const scrollToEdges = [
const slice = Array.prototype.slice
-let buggyRTL = void 0
-
-// mobile Chrome takes the crown for this
-function detectBuggyRTL () {
- const scroller = document.createElement('div')
- const spacer = document.createElement('div')
-
- scroller.setAttribute('dir', 'rtl')
- scroller.style.width = '1px'
- scroller.style.height = '1px'
- scroller.style.overflow = 'auto'
-
- spacer.style.width = '1000px'
- spacer.style.height = '1px'
-
- document.body.appendChild(scroller)
- scroller.appendChild(spacer)
- scroller.scrollLeft = -1000
-
- buggyRTL = scroller.scrollLeft >= 0
-
- scroller.remove()
-}
-
function sumFn (acc, h) {
return acc + h
}
@@ -65,7 +42,7 @@ function getScrollDetails (
if (horizontal === true) {
if (parent === window) {
details.scrollStart = window.pageXOffset || window.scrollX || document.body.scrollLeft || 0
- details.scrollViewSize += window.innerWidth
+ details.scrollViewSize += document.documentElement.clientWidth
}
else {
details.scrollStart = parentCalc.scrollLeft
@@ -74,13 +51,13 @@ function getScrollDetails (
details.scrollMaxSize = parentCalc.scrollWidth
if (rtl === true) {
- details.scrollStart = (buggyRTL === true ? details.scrollMaxSize - details.scrollViewSize : 0) - details.scrollStart
+ details.scrollStart = (isBuggyRTLScroll() === true ? details.scrollMaxSize - details.scrollViewSize : 0) - details.scrollStart
}
}
else {
if (parent === window) {
details.scrollStart = window.pageYOffset || window.scrollY || document.body.scrollTop || 0
- details.scrollViewSize += window.innerHeight
+ details.scrollViewSize += document.documentElement.clientHeight
}
else {
details.scrollStart = parentCalc.scrollTop
@@ -131,7 +108,7 @@ function setScroll (parent, scroll, horizontal, rtl) {
if (parent === window) {
if (horizontal === true) {
if (rtl === true) {
- scroll = (buggyRTL === true ? document.body.scrollWidth - window.innerWidth : 0) - scroll
+ scroll = (isBuggyRTLScroll() === true ? document.body.scrollWidth - document.documentElement.clientWidth : 0) - scroll
}
window.scrollTo(scroll, window.pageYOffset || window.scrollY || document.body.scrollTop || 0)
}
@@ -141,7 +118,7 @@ function setScroll (parent, scroll, horizontal, rtl) {
}
else if (horizontal === true) {
if (rtl === true) {
- scroll = (buggyRTL === true ? parent.scrollWidth - parent.offsetWidth : 0) - scroll
+ scroll = (isBuggyRTLScroll() === true ? parent.scrollWidth - parent.offsetWidth : 0) - scroll
}
parent.scrollLeft = scroll
}
@@ -518,7 +495,7 @@ export default {
if (contentEl !== void 0) {
const
- children = slice.call(contentEl.children).filter(el => el.classList.contains('q-virtual-scroll--skip') === false),
+ children = slice.call(contentEl.children || []).filter(el => el.classList.contains('q-virtual-scroll--skip') === false), // fallback [] for IE
childrenLength = children.length,
sizeFn = this.virtualScrollHorizontal === true
? el => el.getBoundingClientRect().width
@@ -708,7 +685,6 @@ export default {
},
beforeMount () {
- buggyRTL === void 0 && detectBuggyRTL()
this.__onVirtualScrollEvt = debounce(this.__onVirtualScrollEvt, this.$q.platform.is.ios === true ? 120 : 35)
this.__setVirtualScrollSize()
},
diff --git a/ui/src/utils/event.js b/ui/src/utils/event.js
index 6124aec91ddc..6b6cc2a28685 100644
--- a/ui/src/utils/event.js
+++ b/ui/src/utils/event.js
@@ -76,6 +76,19 @@ export function getEventPath (e) {
}
}
+export function eventOnAncestors (e, el) {
+ const { target } = e
+
+ while (el !== null && el !== void 0) {
+ if (el === target) {
+ return true
+ }
+ el = el.parentNode
+ }
+
+ return false
+}
+
// Reasonable defaults
const
LINE_HEIGHT = 40,
@@ -174,6 +187,7 @@ export default {
rightClick,
position,
getEventPath,
+ eventOnAncestors,
getMouseWheelDistance,
stop,
prevent,
diff --git a/ui/src/utils/position-engine.js b/ui/src/utils/position-engine.js
index 4a4027f68e20..ce348bc46357 100644
--- a/ui/src/utils/position-engine.js
+++ b/ui/src/utils/position-engine.js
@@ -1,7 +1,60 @@
-import { getScrollbarWidth } from './scroll.js'
import { client } from '../plugins/Platform.js'
+import { isBuggyRTLScroll } from './scroll.js'
-let vpLeft, vpTop
+const SIDE_SPACE = 4 // how many pixels to reserve on the edge
+
+const horizontalPos = {
+ 'start#ltr': 'left',
+ 'start#rtl': 'right',
+ 'end#ltr': 'right',
+ 'end#rtl': 'left'
+}
+
+;['left', 'middle', 'right'].forEach(pos => {
+ horizontalPos[`${pos}#ltr`] = pos
+ horizontalPos[`${pos}#rtl`] = pos
+})
+
+function isFixedPositioned (el) {
+ while (el && el !== document) {
+ if (window.getComputedStyle(el).position === 'fixed') {
+ return true
+ }
+ el = el.parentNode
+ }
+
+ return false
+}
+
+function isDocumentScrollableX () {
+ return window.getComputedStyle(document.documentElement).overflowX !== 'hidden' &&
+ window.getComputedStyle(document.body).overflowX !== 'hidden'
+}
+
+function isDocumentScrollableY () {
+ return window.getComputedStyle(document.documentElement).overflowY !== 'hidden' &&
+ window.getComputedStyle(document.body).overflowY !== 'hidden'
+}
+
+function computeScrollLeft (fixedPositioned, viewport, rtl) {
+ if (fixedPositioned === true) {
+ return { vpLeft: viewport.offsetLeft, apLeft: viewport.offsetLeft }
+ }
+
+ const scrollLeft = window.pageXOffset || window.scrollX || document.body.scrollLeft || 0
+
+ if (rtl !== true) {
+ return { vpLeft: scrollLeft, apLeft: scrollLeft }
+ }
+
+ // TODO: check if the correction is needed also if isBuggyRTL
+ const buggyRTL = isBuggyRTLScroll()
+ const vpLeft = (buggyRTL === true ? 0 : document.documentElement.scrollWidth - document.documentElement.clientWidth) + scrollLeft
+ return {
+ vpLeft,
+ apLeft: vpLeft + document.documentElement.scrollWidth - document.documentElement.clientWidth
+ }
+}
export function validatePosition (pos) {
const parts = pos.split(' ')
@@ -20,7 +73,7 @@ export function validatePosition (pos) {
}
export function validateOffset (val) {
- if (!val) { return true }
+ if (val !== true) { return true }
if (val.length !== 2) { return false }
if (typeof val[0] !== 'number' || typeof val[1] !== 'number') {
return false
@@ -28,18 +81,6 @@ export function validateOffset (val) {
return true
}
-const horizontalPos = {
- 'start#ltr': 'left',
- 'start#rtl': 'right',
- 'end#ltr': 'right',
- 'end#rtl': 'left'
-}
-
-;[ 'left', 'middle', 'right' ].forEach(pos => {
- horizontalPos[`${pos}#ltr`] = pos
- horizontalPos[`${pos}#rtl`] = pos
-})
-
export function parsePosition (pos, rtl) {
const parts = pos.split(' ')
return {
@@ -53,192 +94,293 @@ export function validateCover (val) {
return validatePosition(val)
}
-export function getAnchorProps (el, offset) {
+export function getAnchorProps (el, offset, rtlCorrection) {
let { top, left, right, bottom, width, height } = el.getBoundingClientRect()
+ if (width === 0) {
+ width = el.offsetWidth
+ }
+ if (height === 0) {
+ height = el.offsetHeight
+ }
+
if (offset !== void 0) {
- top -= offset[1]
left -= offset[0]
- bottom += offset[1]
right += offset[0]
+ top -= offset[1]
+ bottom += offset[1]
+ }
- width += offset[0]
- height += offset[1]
+ // TODO: check if the correction is needed also if isBuggyRTL
+ if (rtlCorrection === true) {
+ const diff = document.documentElement.scrollWidth - document.documentElement.clientWidth
+ left -= diff
+ right -= diff
}
return {
- top,
left,
+ middle: left + (right - left) / 2,
right,
+
+ top,
+ center: top + (bottom - top) / 2,
bottom,
+
+ leftRev: right,
+ middleRev: left + (right - left) / 2,
+ rightRev: left,
+
+ topRev: bottom,
+ centerRev: top + (bottom - top) / 2,
+ bottomRev: top,
+
width,
- height,
- middle: left + (right - left) / 2,
- center: top + (bottom - top) / 2
+ height
}
}
export function getTargetProps (el) {
+ let { width, height } = el.getBoundingClientRect()
+
+ if (width === 0) {
+ width = el.offsetWidth
+ }
+ if (height === 0) {
+ height = el.offsetHeight
+ }
+
return {
- top: 0,
- center: el.offsetHeight / 2,
- bottom: el.offsetHeight,
- left: 0,
- middle: el.offsetWidth / 2,
- right: el.offsetWidth
+ width,
+ height
}
}
-// cfg: { el, anchorEl, anchorOrigin, selfOrigin, offset, absoluteOffset, cover, fit, maxHeight, maxWidth }
+// cfg: { el, anchorEl, anchorOrigin, selfOrigin, offset, absoluteOffset, cover, fit, minHeight, minWidth, maxHeight, maxWidth, rtl }
export function setPosition (cfg) {
- if (client.is.ios === true && window.visualViewport !== void 0) {
- // uses the q-position-engine CSS class
+ const extEl = cfg.el
- const el = document.body.style
- const { offsetLeft: left, offsetTop: top } = window.visualViewport
-
- if (left !== vpLeft) {
- el.setProperty('--q-pe-left', left + 'px')
- vpLeft = left
- }
- if (top !== vpTop) {
- el.setProperty('--q-pe-top', top + 'px')
- vpTop = top
- }
+ if (extEl.classList.contains('q-body--prevent-scroll-reposition') === true) {
+ return
}
- let anchorProps
+ let anchorProps, targetProps
- // scroll position might change
- // if max-height/-width changes, so we
- // need to restore it after we calculate
- // the new positioning
- const { scrollLeft, scrollTop } = cfg.el
+ const
+ intEl = extEl.children[0],
+ firstRender = intEl.style.opacity !== 1,
+ fixedPositioned = isFixedPositioned(cfg.anchorEl),
+ anchorOrigin = { ...cfg.anchorOrigin },
+ selfOrigin = { ...cfg.selfOrigin },
+ viewport = fixedPositioned === true && client.is.ios === true && window.visualViewport !== void 0
+ ? window.visualViewport
+ : { offsetLeft: 0, offsetTop: 0 },
+ { vpLeft, apLeft } = computeScrollLeft(fixedPositioned, viewport, cfg.rtl),
+ vpTop = fixedPositioned === true
+ ? viewport.offsetTop
+ : (window.pageYOffset || window.scrollY || document.body.scrollTop || 0),
+ vpWidth = document.documentElement[fixedPositioned === true || isDocumentScrollableX() !== true ? 'clientWidth' : 'scrollWidth'],
+ vpHeight = document.documentElement[fixedPositioned === true || isDocumentScrollableY() !== true ? 'clientHeight' : 'scrollHeight']
if (cfg.absoluteOffset === void 0) {
- anchorProps = getAnchorProps(cfg.anchorEl, cfg.cover === true ? [0, 0] : cfg.offset)
+ anchorProps = getAnchorProps(cfg.anchorEl, cfg.cover === true ? [0, 0] : cfg.offset, cfg.rtl === true && fixedPositioned !== true)
}
else {
const
+ leftOffset = cfg.rtl === true && fixedPositioned !== true
+ ? document.documentElement.scrollWidth - document.documentElement.clientWidth
+ : 0,
{ top: anchorTop, left: anchorLeft } = cfg.anchorEl.getBoundingClientRect(),
- top = anchorTop + cfg.absoluteOffset.top,
- left = anchorLeft + cfg.absoluteOffset.left
+ top = anchorTop + (cfg.cover === true ? 0 : cfg.absoluteOffset.top),
+ left = anchorLeft + (cfg.cover === true || cfg.fit === true ? 0 : cfg.absoluteOffset.left) - leftOffset
+
+ anchorProps = {
+ left,
+ middle: left,
+ right: left,
+
+ top,
+ center: top,
+ bottom: top,
+
+ leftRev: left,
+ middleRev: left,
+ rightRev: left,
- anchorProps = { top, left, width: 1, height: 1, right: left + 1, center: top, middle: left, bottom: top + 1 }
+ topRev: top,
+ centerRev: top,
+ bottomRev: top,
+
+ width: 0,
+ height: 0
+ }
}
- let elStyle = {
- maxHeight: cfg.maxHeight,
- maxWidth: cfg.maxWidth,
- visibility: 'visible'
+ const intElStyle = {
+ minWidth: cfg.minWidth || null,
+ minHeight: cfg.minHeight || null,
+ maxWidth: cfg.maxWidth || null,
+ maxHeight: cfg.maxHeight || null
}
if (cfg.fit === true || cfg.cover === true) {
- elStyle.minWidth = anchorProps.width + 'px'
- if (cfg.cover === true) {
- elStyle.minHeight = anchorProps.height + 'px'
+ if (cfg.minWidth === null) {
+ intElStyle.minWidth = anchorProps.width + 'px'
+ }
+ if (cfg.cover === true && cfg.minHeight === null) {
+ intElStyle.minHeight = anchorProps.height + 'px'
}
}
- Object.assign(cfg.el.style, elStyle)
+ Object.assign(intEl.style, intElStyle)
- const
- targetProps = getTargetProps(cfg.el),
- props = {
- top: anchorProps[cfg.anchorOrigin.vertical] - targetProps[cfg.selfOrigin.vertical],
- left: anchorProps[cfg.anchorOrigin.horizontal] - targetProps[cfg.selfOrigin.horizontal]
- }
-
- applyBoundaries(props, anchorProps, targetProps, cfg.anchorOrigin, cfg.selfOrigin)
+ if (firstRender === true) {
+ const clone = intEl.cloneNode(true)
+ clone.classList.add('q-portal__clone')
+ document.body.appendChild(clone)
+ targetProps = getTargetProps(clone)
+ clone.remove()
+ }
+ else {
+ targetProps = getTargetProps(intEl)
+ }
- elStyle = {
- top: props.top + 'px',
- left: props.left + 'px'
+ if (intElStyle.minWidth !== null && anchorProps.width > targetProps.width) {
+ intElStyle.minWidth = targetProps.width + 'px'
+ }
+ if (intElStyle.minHeight !== null && anchorProps.height > targetProps.height) {
+ intElStyle.minHeight = targetProps.height + 'px'
}
- if (props.maxHeight !== void 0) {
- elStyle.maxHeight = props.maxHeight + 'px'
+ const extElStyle = {
+ position: fixedPositioned === true ? 'fixed' : 'absolute',
- if (anchorProps.height > props.maxHeight) {
- elStyle.minHeight = elStyle.maxHeight
- }
+ left: null,
+ right: null,
+ marginLeft: null,
+ marginRight: null,
+ maxWidth: null,
+
+ top: null,
+ bottom: null,
+ marginTop: null,
+ marginBottom: null,
+ maxHeight: null
}
- if (props.maxWidth !== void 0) {
- elStyle.maxWidth = props.maxWidth + 'px'
- if (anchorProps.width > props.maxWidth) {
- elStyle.minWidth = elStyle.maxWidth
- }
+ const
+ halfWidth = Math.min(vpLeft + anchorProps[cfg.anchorOrigin.horizontal], vpWidth - apLeft - anchorProps[cfg.anchorOrigin.horizontal]) - SIDE_SPACE,
+ halfHeight = Math.min(vpTop + anchorProps[cfg.anchorOrigin.vertical], vpHeight - vpTop - anchorProps[cfg.anchorOrigin.vertical]) - SIDE_SPACE
+
+ // horizontal repositioning
+ if (
+ selfOrigin.horizontal === 'left' &&
+ targetProps.width + SIDE_SPACE > vpWidth - apLeft - anchorProps[anchorOrigin.horizontal] &&
+ vpLeft + anchorProps[anchorOrigin.horizontal + 'Rev'] > vpWidth - apLeft - anchorProps[anchorOrigin.horizontal]
+ ) {
+ selfOrigin.horizontal = 'right'
+ anchorOrigin.horizontal = anchorOrigin.horizontal + 'Rev'
+ }
+ else if (
+ selfOrigin.horizontal === 'right' &&
+ targetProps.width + SIDE_SPACE > vpLeft + anchorProps[anchorOrigin.horizontal] &&
+ vpWidth - apLeft - anchorProps[anchorOrigin.horizontal + 'Rev'] > vpLeft + anchorProps[anchorOrigin.horizontal]
+ ) {
+ selfOrigin.horizontal = 'left'
+ anchorOrigin.horizontal = anchorOrigin.horizontal + 'Rev'
+ }
+ else if (
+ selfOrigin.horizontal === 'middle' &&
+ targetProps.width / 2 > halfWidth
+ ) {
+ selfOrigin.horizontal = vpLeft + anchorProps[anchorOrigin.horizontal] < vpWidth / 2
+ ? 'left'
+ : 'right'
+ anchorOrigin.horizontal = selfOrigin.horizontal
}
- Object.assign(cfg.el.style, elStyle)
+ // horizontal styles
+ if (selfOrigin.horizontal === 'left') {
+ extElStyle.left = 0
+ extElStyle.marginLeft = `${vpLeft + anchorProps[anchorOrigin.horizontal]}px`
+ extElStyle.maxWidth = `${vpWidth - vpLeft - anchorProps[anchorOrigin.horizontal] - SIDE_SPACE}px`
+ }
+ else if (selfOrigin.horizontal === 'right') {
+ extElStyle.right = '100%'
+ extElStyle.marginRight = `-${vpLeft + anchorProps[anchorOrigin.horizontal]}px`
+ extElStyle.maxWidth = `${vpLeft + anchorProps[anchorOrigin.horizontal] - SIDE_SPACE}px`
+ }
+ else {
+ extElStyle.right = '100%'
+ extElStyle.marginRight = `-${vpLeft + anchorProps[anchorOrigin.horizontal]}px`
+ }
- // restore scroll position
- if (cfg.el.scrollTop !== scrollTop) {
- cfg.el.scrollTop = scrollTop
+ // vertical repositioning
+ if (
+ selfOrigin.vertical === 'top' &&
+ targetProps.height + SIDE_SPACE > vpHeight - vpTop - anchorProps[anchorOrigin.vertical] &&
+ vpTop + anchorProps[anchorOrigin.vertical + 'Rev'] > vpHeight - vpTop - anchorProps[anchorOrigin.vertical]
+ ) {
+ selfOrigin.vertical = 'bottom'
+ anchorOrigin.vertical = anchorOrigin.vertical + 'Rev'
}
- if (cfg.el.scrollLeft !== scrollLeft) {
- cfg.el.scrollLeft = scrollLeft
+ else if (
+ selfOrigin.vertical === 'bottom' &&
+ targetProps.height + SIDE_SPACE > vpTop + anchorProps[anchorOrigin.vertical] &&
+ vpHeight - vpTop - anchorProps[anchorOrigin.vertical + 'Rev'] > vpTop + anchorProps[anchorOrigin.vertical]
+ ) {
+ selfOrigin.vertical = 'top'
+ anchorOrigin.vertical = anchorOrigin.vertical + 'Rev'
+ }
+ else if (
+ selfOrigin.vertical === 'center' &&
+ targetProps.height / 2 > halfHeight
+ ) {
+ selfOrigin.vertical = vpTop + anchorProps[anchorOrigin.vertical] < vpHeight / 2
+ ? 'top'
+ : 'bottom'
+ anchorOrigin.vertical = selfOrigin.vertical
}
-}
-function applyBoundaries (props, anchorProps, targetProps, anchorOrigin, selfOrigin) {
- const
- currentHeight = targetProps.bottom,
- currentWidth = targetProps.right,
- margin = getScrollbarWidth(),
- innerHeight = window.innerHeight - margin,
- innerWidth = document.body.clientWidth
-
- if (props.top < 0 || props.top + currentHeight > innerHeight) {
- if (selfOrigin.vertical === 'center') {
- props.top = anchorProps[anchorOrigin.vertical] > innerHeight / 2
- ? Math.max(0, innerHeight - currentHeight)
- : 0
- props.maxHeight = Math.min(currentHeight, innerHeight)
- }
- else if (anchorProps[anchorOrigin.vertical] > innerHeight / 2) {
- const anchorY = Math.min(
- innerHeight,
- anchorOrigin.vertical === 'center'
- ? anchorProps.center
- : (anchorOrigin.vertical === selfOrigin.vertical ? anchorProps.bottom : anchorProps.top)
- )
- props.maxHeight = Math.min(currentHeight, anchorY)
- props.top = Math.max(0, anchorY - currentHeight)
- }
- else {
- props.top = Math.max(0, anchorOrigin.vertical === 'center'
- ? anchorProps.center
- : (anchorOrigin.vertical === selfOrigin.vertical ? anchorProps.top : anchorProps.bottom)
- )
- props.maxHeight = Math.min(currentHeight, innerHeight - props.top)
- }
+ // vertical styles
+ if (selfOrigin.vertical === 'top') {
+ extElStyle.top = 0
+ extElStyle.marginTop = `${vpTop + anchorProps[anchorOrigin.vertical]}px`
+ extElStyle.maxHeight = `${vpHeight - vpTop - anchorProps[anchorOrigin.vertical] - SIDE_SPACE}px`
+ }
+ else if (selfOrigin.vertical === 'bottom') {
+ extElStyle.bottom = '100%'
+ extElStyle.marginBottom = `-${vpTop + anchorProps[anchorOrigin.vertical]}px`
+ extElStyle.maxHeight = `${vpTop + anchorProps[anchorOrigin.vertical] - SIDE_SPACE}px`
+ }
+ else {
+ extElStyle.bottom = '100%'
+ extElStyle.marginBottom = `-${vpTop + anchorProps[anchorOrigin.vertical]}px`
}
- if (props.left < 0 || props.left + currentWidth > innerWidth) {
- props.maxWidth = Math.min(currentWidth, innerWidth)
- if (selfOrigin.horizontal === 'middle') {
- props.left = anchorProps[anchorOrigin.horizontal] > innerWidth / 2
- ? Math.max(0, innerWidth - currentWidth)
- : 0
- }
- else if (anchorProps[anchorOrigin.horizontal] > innerWidth / 2) {
- const anchorX = Math.min(
- innerWidth,
- anchorOrigin.horizontal === 'middle'
- ? anchorProps.middle
- : (anchorOrigin.horizontal === selfOrigin.horizontal ? anchorProps.right : anchorProps.left)
- )
- props.maxWidth = Math.min(currentWidth, anchorX)
- props.left = Math.max(0, anchorX - props.maxWidth)
- }
- else {
- props.left = Math.max(0, anchorOrigin.horizontal === 'middle'
- ? anchorProps.middle
- : (anchorOrigin.horizontal === selfOrigin.horizontal ? anchorProps.left : anchorProps.right)
- )
- props.maxWidth = Math.min(currentWidth, innerWidth - props.left)
- }
+ if (selfOrigin.horizontal === 'middle' && selfOrigin.vertical === 'center') {
+ intElStyle.transform = 'translate(50%, 50%)'
+ extElStyle.transformOrigin = '100% 100%'
+ }
+ else if (selfOrigin.horizontal === 'middle') {
+ intElStyle.transform = 'translateX(50%)'
+ extElStyle.transformOrigin = '100% 50%'
+ }
+ else if (selfOrigin.vertical === 'center') {
+ intElStyle.transform = 'translateY(50%)'
+ extElStyle.transformOrigin = '50% 100%'
+ }
+ else {
+ intElStyle.transform = null
+ extElStyle.transformOrigin = '50% 50%'
+ }
+
+ Object.assign(extEl.style, extElStyle)
+ Object.assign(intEl.style, intElStyle)
+
+ if (firstRender === true) {
+ requestAnimationFrame(() => {
+ intEl.style.opacity = 1
+ })
}
}
diff --git a/ui/src/utils/scroll.js b/ui/src/utils/scroll.js
index 1046242f2914..588f170c150b 100644
--- a/ui/src/utils/scroll.js
+++ b/ui/src/utils/scroll.js
@@ -5,6 +5,40 @@ const scrollTargets = isSSR === false
? [ null, document, document.body, document.scrollingElement, document.documentElement ]
: []
+let buggyRTLScroll
+export function isBuggyRTLScroll () {
+ if (isSSR === true) {
+ return false
+ }
+
+ if (buggyRTLScroll === void 0) {
+ const scroller = document.createElement('div')
+ const spacer = document.createElement('div')
+
+ Object.assign(scroller.style, {
+ direction: 'rtl',
+ width: '1px',
+ height: '1px',
+ overflow: 'auto'
+ })
+
+ Object.assign(spacer.style, {
+ width: '1000px',
+ height: '1px'
+ })
+
+ scroller.appendChild(spacer)
+ document.body.appendChild(scroller)
+ scroller.scrollLeft = -1000
+
+ buggyRTLScroll = scroller.scrollLeft >= 0
+
+ scroller.remove()
+ }
+
+ return buggyRTLScroll
+}
+
export function getScrollTarget (el, target) {
if (typeof target === 'string') {
try {
@@ -195,6 +229,7 @@ export default {
getScrollPosition,
getHorizontalScrollPosition,
+ isBuggyRTLScroll,
animScrollTo,
animHorizontalScrollTo,
diff --git a/ui/types/utils/event.d.ts b/ui/types/utils/event.d.ts
index 65096966a0db..1b8a92ba05bc 100644
--- a/ui/types/utils/event.d.ts
+++ b/ui/types/utils/event.d.ts
@@ -20,6 +20,7 @@ export namespace event {
function rightClick(evt: MouseEvent): boolean;
function position(evt: TouchEvent): { top: number; left: number };
function getEventPath(evt: Event): EventTarget[];
+ function eventOnAncestors(evt: Event, el: Element | null | undefined): boolean;
function getMouseWheelDistance(evt: WheelEvent): { x: number; y: number };
function stop(evt: Event): void;
function prevent(evt: Event): void;
diff --git a/ui/types/utils/scroll.d.ts b/ui/types/utils/scroll.d.ts
index ceb948807838..9bfaaad8c279 100644
--- a/ui/types/utils/scroll.d.ts
+++ b/ui/types/utils/scroll.d.ts
@@ -8,6 +8,7 @@ export namespace scroll {
function getScrollPosition(scrollTarget: Element | Window): number;
function getHorizontalScrollPosition(scrollTarget: Element | Window): number;
+ function isBuggyRTLScroll(): boolean;
function animScrollTo(el: Element | Window, to: number, duration: number): void;
function animHorizontalScrollTo(el: Element | Window, to: number, duration: number): void;