From 8829d8af88f9308453a4cae56504748b7a794f30 Mon Sep 17 00:00:00 2001 From: Dan Popescu Date: Tue, 29 Oct 2019 08:42:33 +0200 Subject: [PATCH] feat(QDialog): Detect iOS soft keyboard show/hide and adjust QDialog @#*%!$ iOS ref #4264, close #5351 --- ui/dev/components/global/dialog.vue | 10 +++- ui/src/components/dialog/QDialog.js | 80 ++++++++++++++++++++++------- ui/src/mixins/prevent-scroll.js | 27 +++++++--- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/ui/dev/components/global/dialog.vue b/ui/dev/components/global/dialog.vue index 7a05a7fb91c..b6c8e46fec4 100644 --- a/ui/dev/components/global/dialog.vue +++ b/ui/dev/components/global/dialog.vue @@ -750,6 +750,9 @@ Subtitle + + + @@ -769,6 +772,9 @@ + + + @@ -799,8 +805,8 @@ - - + + diff --git a/ui/src/components/dialog/QDialog.js b/ui/src/components/dialog/QDialog.js index 642a6d05293..5e0be022912 100644 --- a/ui/src/components/dialog/QDialog.js +++ b/ui/src/components/dialog/QDialog.js @@ -5,13 +5,16 @@ import ModelToggleMixin from '../../mixins/model-toggle.js' import PortalMixin from '../../mixins/portal.js' import PreventScrollMixin from '../../mixins/prevent-scroll.js' +import { noop } from '../../utils.js' import { childHasFocus } from '../../utils/dom.js' import EscapeKey from '../../utils/escape-key.js' import slot from '../../utils/slot.js' -import { create, stop } from '../../utils/event.js' +import { create, stop, stopAndPrevent } from '../../utils/event.js' let maximizedModals = 0 +const inputTypesWithKeyboard = ['color', 'date', 'datetime-local', 'email', 'month', 'number', 'password', 'serach', 'tel', 'text', 'time', 'url', 'week', 'datetime'] + const positionClass = { standard: 'fixed-full flex-center', top: 'fixed-top justify-center', @@ -65,7 +68,8 @@ export default Vue.extend({ data () { return { - transitionState: this.showing + transitionState: this.showing, + iosKeyboard: false } }, @@ -122,6 +126,10 @@ export default Vue.extend({ return this.persistent !== true && this.noRouteDismiss !== true && this.seamless !== true + }, + + __onTouchMove () { + return this.iosKeyboard === true ? noop : stopAndPrevent } }, @@ -129,12 +137,10 @@ export default Vue.extend({ focus () { let node = this.__getInnerNode() - if (node === void 0 || node.contains(document.activeElement) === true) { - return + if (node !== void 0 && node.contains(document.activeElement) !== true) { + node = node.querySelector('[autofocus]') || node + node.focus() } - - node = node.querySelector('[autofocus]') || node - node.focus() }, shake () { @@ -190,17 +196,25 @@ export default Vue.extend({ this.__nextTick(this.focus) } + let delays = this.__isIos === true && this.useBackdrop === true && this.position === 'bottom' + ? [ 0, 300 ] + : [ 100, 200 ] + this.__setTimeout(() => { - if (this.$q.platform.is.ios === true && document.activeElement) { - const { top } = document.activeElement.getBoundingClientRect() - if (top < 0) { - document.scrollingElement.scrollTop += top - window.innerHeight / 2 - } - document.activeElement.scrollIntoView() + if (this.iosKeyboard === true && this.useBackdrop === true) { + document.scrollingElement.scrollTop = this.position === 'bottom' + ? window.innerHeight + : this.position === 'top' ? 0 : (this.$q.screen.width < 400 ? 130 : 153) } - this.$emit('show', evt) - }, 300) + this.__setTimeout(() => { + if (document.activeElement && document.activeElement.autofocus === true) { + document.activeElement.parentElement.scrollIntoView(true) + } + + this.$emit('show', evt) + }, delays[1]) + }, delays[0]) }, __hide (evt) { @@ -222,6 +236,7 @@ export default Vue.extend({ __cleanup (hiding) { clearTimeout(this.shakeTimeout) + this.__setIosKeyboard(false) if (hiding === true || this.showing === true) { EscapeKey.pop(this) @@ -246,9 +261,15 @@ export default Vue.extend({ }, __preventFocusout (state) { - if (this.$q.platform.is.desktop === true) { - const action = `${state === true ? 'add' : 'remove'}EventListener` - document.body[action]('focusin', this.__onFocusChange) + const action = `${state === true ? 'add' : 'remove'}EventListener` + + if (this.$q.platform.is.desktop === true || this.__isIos === true) { + document.body[action]('focusin', this.__onFocusChange, true) + } + + // Required to detect keyboard closing + if (this.__isIos === true) { + document.body[action]('focusout', this.__onFocusoutChange, true) } }, @@ -275,6 +296,26 @@ export default Vue.extend({ ) { this.focus() } + + if (this.__isIos === true && e !== void 0 && e.target !== void 0) { + const nodeName = e.target.nodeName.toLowerCase() + + this.__setIosKeyboard(nodeName === 'textarea' || + (nodeName === 'input' && inputTypesWithKeyboard.indexOf(e.target.type.toLowerCase()) > -1) || + (e.path !== void 0 && e.path.some(el => el.hasAttribute !== void 0 && el.hasAttribute('contenteditable')) === true) + ) + } + }, + + // Only called on iOS to detect keyboard hide + __onFocusoutChange () { + this.iosKeyboard === true && this.__setIosKeyboard(false) + }, + + __setIosKeyboard (status) { + if (this.iosKeyboard !== status) { + this.iosKeyboard = status + } }, __renderPortal (h) { @@ -302,6 +343,7 @@ export default Vue.extend({ h('div', { staticClass: 'q-dialog__backdrop fixed-full', on: { + touchmove: this.__onTouchMove, // prevent iOS page scroll click: this.__onBackdropClick } }) @@ -324,6 +366,8 @@ export default Vue.extend({ mounted () { this.__processModelChange(this.value) + // discriminate between mac laptop and ipad with request desktop site + this.__isIos = this.$q.platform.is.ios === true || (this.$q.platform.is.mac === true && this.$q.platform.has.touch === true) }, beforeDestroy () { diff --git a/ui/src/mixins/prevent-scroll.js b/ui/src/mixins/prevent-scroll.js index 18e8016c310..c174c13d8dc 100644 --- a/ui/src/mixins/prevent-scroll.js +++ b/ui/src/mixins/prevent-scroll.js @@ -49,15 +49,26 @@ function shouldPreventScroll (e) { } function onAppleScroll (e) { - if (e.target === document) { - // required, otherwise iOS blocks further scrolling - // until the mobile scrollbar dissapears - document.scrollingElement.scrollTop = document.scrollingElement.scrollTop // eslint-disable-line + // iPad does not need this and neither does iPhone in landscape + if (e.target === document && window.innerWidth < 768) { + if (document.scrollingElement.scrollTop < 0) { + document.scrollingElement.scrollTop -= 1 + } + else if (window.innerWidth < 400 && document.scrollingElement.scrollTop > 260) { + document.scrollingElement.scrollTop = 260 + } + else if (document.scrollingElement.scrollTop > 307) { + document.scrollingElement.scrollTop = 307 + } } } function apply (action) { - const body = document.body + const + body = document.body, + // discriminate between mac laptop and ipad with request desktop site + needIosSafariHack = Platform.is.safari === true && (Platform.is.ios === true || (Platform.is.mac === true && Platform.has.touch === true)), + needMacSafariHack = Platform.is.safari === true && Platform.is.desktop === true && Platform.is.mac === true && Platform.has.touch !== true if (action === 'add') { const overflowY = window.getComputedStyle(body).overflowY @@ -74,16 +85,16 @@ function apply (action) { } body.classList.add('q-body--prevent-scroll') - Platform.is.ios === true && window.addEventListener('scroll', onAppleScroll, listenOpts.passiveCapture) + needIosSafariHack === true && window.addEventListener('scroll', onAppleScroll, listenOpts.passiveCapture) } - if (Platform.is.desktop === true && Platform.is.mac === true) { + if (needMacSafariHack === true) { // ref. https://developers.google.com/web/updates/2017/01/scrolling-intervention window[`${action}EventListener`]('wheel', onWheel, listenOpts.notPassive) } if (action === 'remove') { - Platform.is.ios === true && window.removeEventListener('scroll', onAppleScroll, listenOpts.passiveCapture) + needIosSafariHack === true && window.removeEventListener('scroll', onAppleScroll, listenOpts.passiveCapture) body.classList.remove('q-body--prevent-scroll') body.classList.remove('q-body--force-scrollbar')