Reputation: 7114
I have an inelegant workaround for this issue, and am hoping that others may already have more robust solutions.
On a touchscreen, tapping on an editable text field will bring up an on-screen keyboard, and this will change the amount of screen space available. Left untreated, this may hide key elements, or push a footer out of place.
On a laptop or desktop computer, opening an editable text field creates no such layout changes.
In my current project, I want to ensure that certain key items are visible even when a virtual keyboard is open, so I need to detect when such a change occurs. I can then add a class to the body
element, to change the layout to suit the presence of the keyboard.
When searching for existing solutions online, I discovered that:
contentEditable
elements will open the on-screen keyboardI have posted the solution that I have come up with below. It relies on detecting a change in height of the window within a second of the keyboard focus changing. I am hoping that you might have a better solution to propose that has been tested cross-platform, cross-browser and across devices.
I've created a repository on GitHub.
You can test my solution here.
In my tests, this may give a false positive if the user is using a computer with a touchscreen and a keyboard and mouse, and uses the mouse first to (de-)select an editable element and then immediately changes the window height. If you find other false positives or negatives, either on a computer or a mobile device, please let me know.
;(function (){
class Keyboard {
constructor () {
this.screenWidth = screen.width // detect orientation
this.windowHeight = window.innerHeight // detect keyboard change
this.listeners = {
resize: []
, keyboardchange: []
, focuschange: []
}
this.isTouchScreen = 'ontouchstart' in document.documentElement
this.focusElement = null
this.changeFocusTime = new Date().getTime()
this.focusDelay = 1000 // at least 600 ms is required
let focuschange = this.focuschange.bind(this)
document.addEventListener("focus", focuschange, true)
document.addEventListener("blur", focuschange, true)
window.onresize = this.resizeWindow.bind(this)
}
focuschange(event) {
let target = event.target
let elementType = null
let checkType = false
let checkEnabled = false
let checkEditable = true
if (event.type === "focus") {
elementType = target.nodeName
this.focusElement = target
switch (elementType) {
case "INPUT":
checkType = true
case "TEXTAREA":
checkEditable = false
checkEnabled = true
break
}
if (checkType) {
let type = target.type
switch (type) {
case "color":
case "checkbox":
case "radio":
case "date":
case "file":
case "month":
case "time":
this.focusElement = null
checkEnabled = false
default:
elementType += "[type=" + type +"]"
}
}
if (checkEnabled) {
if (target.disabled) {
elementType += " (disabled)"
this.focusElement = null
}
}
if (checkEditable) {
if (!target.contentEditable) {
elementType = null
this.focusElement = null
}
}
} else {
this.focusElement = null
}
this.changeFocusTime = new Date().getTime()
this.listeners.focuschange.forEach(listener => {
listener(this.focusElement, elementType)
})
}
resizeWindow() {
let screenWidth = screen.width;
let windowHeight = window.innerHeight
let dimensions = {
width: innerWidth
, height: windowHeight
}
let orientation = (screenWidth > screen.height)
? "landscape"
: "portrait"
let focusAge = new Date().getTime() - this.changeFocusTime
let closed = !this.focusElement
&& (focusAge < this.focusDelay)
&& (this.windowHeight < windowHeight)
let opened = this.focusElement
&& (focusAge < this.focusDelay)
&& (this.windowHeight > windowHeight)
if ((this.screenWidth === screenWidth) && this.isTouchScreen) {
// No change of orientation
// opened or closed can only be true if height has changed.
//
// Edge case
// * Will give a false positive for keyboard change.
// * The user has a tablet computer with both screen and
// keyboard, and has just clicked into or out of an
// editable area, and also changed the window height in
// the appropriate direction, all with the mouse.
if (opened) {
this.keyboardchange("shown", dimensions)
} else if (closed) {
this.keyboardchange("hidden", dimensions)
} else {
// Assume this is a desktop touchscreen computer with
// resizable windows
this.resize(dimensions, orientation)
}
} else {
// Orientation has changed
this.resize(dimensions, orientation)
}
this.windowHeight = windowHeight
this.screenWidth = screenWidth
}
keyboardchange(change, dimensions) {
this.listeners.keyboardchange.forEach(listener => {
listener(change, dimensions)
})
}
resize(dimensions, orientation) {
this.listeners.resize.forEach(listener => {
listener(dimensions, orientation)
})
}
addEventListener(eventName, listener) {
// log("*addEventListener " + eventName)
let listeners = this.listeners[eventName] || []
if (listeners.indexOf(listener) < 0) {
listeners.push(listener)
}
}
removeEventListener(eventName, listener) {
let listeners = this.listeners[eventName] || []
let index = listeners.indexOf(listener)
if (index < 0) {
} else {
listeners.slice(index, 1)
}
}
}
window.keyboard = new Keyboard()
})()
Upvotes: 12
Views: 16601
Reputation: 12507
This was inspired by on-screen-keyboard-detector. It works on Android and iOS.
if ('visualViewport' in window) {
const VIEWPORT_VS_CLIENT_HEIGHT_RATIO = 0.75;
window.visualViewport.addEventListener('resize', function (event) {
if (
(event.target.height * event.target.scale) / window.screen.height <
VIEWPORT_VS_CLIENT_HEIGHT_RATIO
)
console.log('keyboard is shown');
else console.log('keyboard is hidden');
});
}
This worked, but isn't supported in iOS yet.
if ('virtualKeyboard' in navigator) {
// Tell the browser you are taking care of virtual keyboard occlusions yourself.
navigator.virtualKeyboard.overlaysContent = true;
navigator.virtualKeyboard.addEventListener('geometrychange', (event) => {
const { x, y, width, height } = event.target.boundingRect;
if (height > 0) console.log('keyboard is shown');
else console.log('keyboard is hidden');
});
Source: https://developer.chrome.com/docs/web-platform/virtual-keyboard/
Upvotes: 5
Reputation: 6501
I'm detecting the visibility of a virtual keyboard as follows:
window.addEventListener('resize', (event) => {
// if current/available height ratio is small enough, virtual keyboard is probably visible
const isKeyboardHidden = ((window.innerHeight / window.screen.availHeight) > 0.6);
});
Upvotes: 1
Reputation: 281
There is a new experimental API that is meant exactly to track size changes due to the keyboard appearing and other mobile weirdness like that.
window.visualViewport
https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API
By listening to resize events and comparing the height to the height to the so called "layout viewport". See that it changed by a significant amount, like maybe 30 pixels. You might deduce something like "the keyboard is showing".
if('visualViewport' in window) {
window.visualViewport.addEventListener('resize', function(event) {
if(event.target.height + 30 < document.scrollElement.clientHeight) {
console.log("keyboard up?");
} else {
console.log("keyboard down?");
}
});
}
(code above is untested and I suspect zooming might trigger false positive, might have to check for scaling changes as well)
Upvotes: 6
Reputation: 1325
This is a difficult problem to get 'right'. You can try and hide the footer on input element focus, and show on blur, but that isn't always reliable on iOS. Every so often (one time in ten, say, on my iPhone 4S) the focus event seems to fail to fire (or maybe there is a race condition with JQuery Mobile), and the footer does not get hidden.
After much trial and error, I came up with this interesting solution:
<head>
...various JS and CSS imports...
<script type="text/javascript">
document.write( '<style>#footer{visibility:hidden}@media(min-height:' + ($( window ).height() - 10) + 'px){#footer{visibility:visible}}</style>' );
</script>
</head>
Essentially: use JavaScript to determine the window height of the device, then dynamically create a CSS media query to hide the footer when the height of the window shrinks by 10 pixels. Because opening the keyboard resizes the browser display, this never fails on iOS. Because it's using the CSS engine rather than JavaScript, it's much faster and smoother too!
Note: I found using 'visibility:hidden' less glitchy than 'display:none' or 'position:static', but your mileage may vary.
Upvotes: 0
Reputation: 630
As no direct way to detect the keyboard opening, you can only detect by the height and width. See more
In javascript screen.availHeight
and screen.availWidth
maybe help.
Upvotes: 2