iOS 13 Safari: Bug(s) in window.innerHeight - css

As I understand it, the window.innerHeight should return the size of the viewport without the browser chrome (address bar, navigation, tabs, etc.). But this doesn't seem to be the case in the latest version of iOS13. Instead there are two problems:
(Sometimes* too small in portrait) If you rotate from portrait mode to landscape mode with no tabs open and then back to portrait mode, the window.innerHeight value ends up being too small (by about the size of the bottom navigation bar) giving this horrible white bar at the bottom of the screen. See this discussion on macrumors for more details: https://forums.macrumors.com/threads/is-this-a-mobile-safari-bug-white-space-appears-at-bottom-after-rotating-iphone.2209551/
(Sometimes* too big in landscape) If you have a bunch of tabs open, "Show tab bar" turned on and then rotate from portrait mode into landscape mode, then window.innerHeight is too big and the bottom of the screen gets cut off.
Even after turning on every conceivable viewport tag and all permutations thereof, it doesn't seem to work. I've also looked at several "tutorials" on how to handle this problem in iOS Safari, and to date every one that I've checked is broken.
I've also tried all variations of the window.innerHeight, with more or less the same result:
The new visual viewport API returns the same results, no different than window.innerHeight. Bottom is still truncated in landscape with tab bar and portrait mode still has the white bar at the bottom.
document.documentElement.clientHeight with various permutations of CSS (using 100vh, 100%, etc.) gives the same result. Ditto for getBoundingClientRect on various divs and combinations of div elements.
window.outerHeight and screen.height give the size of the full screen without browser chrome, which is generally too big and causes an overflow.
Also tried a bunch of other random things that I've forgotten by now (should have taken notes).
You can manually fudge it on a per-device basis if you can guess the size of the top and bottom browser chrome, but this is extremely fragile. I'm looking for a solution that doesn't involve building a giant look up table of every iOS device and software configuration.
I'm trying to make a fullscreen canvas element for a web game and this issue is blocking my ability to ship. As far as I know this issue is only present in iOS13. After looking around for weeks I still haven't found a good fix.

I have had the same issue recently and I was able to solve it like this:
CSS (only relevant parts shown):
html {
height: 100%;
min-height: 100%;
max-height: 100%;
background: #99f; /* Safari for iOS and Opera for Android in fullscreen mode?!?! */
}
body {
padding: 0;
margin: 0;
color: #000;
width: 100%; /* I was desperate! This was a wild guess... And worked! */
height: 100%;
min-height: 100%;
max-height: 100%;
overflow: hidden;
background: #99f;
}
TypeScript (only relevant parts shown):
// Assume everything here is in the global scope
function detectIOSOrSafari(): boolean {
// https://stackoverflow.com/q/9038625/3569421
if ((navigator.userAgent.indexOf("Chrome") <= 0 && navigator.userAgent.indexOf("Safari") >= 0) ||
(navigator.userAgent.indexOf("Mac") >= 0 && ("ontouchend" in document)))
return true;
switch (navigator.platform) {
case "iPad Simulator":
case "iPhone Simulator":
case "iPod Simulator":
case "iPad":
case "iPhone":
case "iPod":
return true;
}
return false;
}
const isIOSOrSafari = detectIOSOrSafari();
function adjustWindowSize(): void {
let widthCss = window.innerWidth,
heightCss = window.innerHeight;
if (document.documentElement && ("clientWidth" in document.documentElement)) {
widthCss = document.documentElement.clientWidth;
heightCss = document.documentElement.clientHeight;
}
if (isIOSOrSafari) {
let bodyRect: DOMRect = null;
// Another act out of desperation...
if (document.documentElement && ("getBoundingClientRect" in document.documentElement))
bodyRect = document.documentElement.getBoundingClientRect();
else if (("getBoundingClientRect" in document.body))
bodyRect = document.body.getBoundingClientRect();
if (bodyRect) {
widthCss = bodyRect.right - bodyRect.left;
heightCss = bodyRect.bottom - bodyRect.top;
}
}
// Rest of the code, where I use widthCss and heightCss to compute my canvas' size
}
window.onresize = adjustWindowSize;
You can check out the entire source code in the project's repository: https://github.com/carlosrafaelgn/pixel

Related

position: sticky doesn't work when virtual keyboard is open in Safari

Position sticky doesn't work when virtual keyboard is open in Safari
I've tried using position: -webkit-sticky.
Expected behavior in non-Safari webkit browsers (Chrome, Firefox, Opera)
.sticky {
background-color: red;
position: sticky;
position: -webkit-sticky;
bottom: 0;
}
Steps to reproduce:
Open https://codepen.io/wmsmacdonald/pen/vYBVVRL in Safari on iOS
Scroll to bring white screen into viewport
Click text input to focus
Expected:
Red footer should stick to bottom of screen even when virtual keyboard is open
Actual:
User must scroll down with keyboard open in order to see the red footer
let pendingUpdate = false;
const viewportHandler = (event) => {
if (pendingUpdate) {
return;
}
pendingUpdate = true;
requestAnimationFrame(() => {
pendingUpdate = false;
const layoutViewport = document.querySelector('.sticky');
layoutViewport.style.transform = "none";
if (layoutViewport.getBoundingClientRect().top < 0) {
layoutViewport.style.transform = `translate(0, ${-layoutViewport.getBoundingClientRect().top}px)`;
}
});
};
window.visualViewport.addEventListener("scroll", viewportHandler);
window.visualViewport.addEventListener("resize", viewportHandler);
The pendingUpdate flag serves to prevent multiple invocations of the transfrom that can occur when onresize and onscroll fire at the same time. Using requestAnimationFrame() ensures that the transform occurs before the next render.
This is expected behavior in Safari as of October 2019:
https://bugs.webkit.org/show_bug.cgi?id=202120
A workaround to make it consistent with other rendering engines would be to use the Visual Viewport API to get the visual viewport height and fix the element to the bottom using position: absolute. However, the Visual Viewport API only has support in Safari 13.

Is there a way to position page content UNDERNEATH or ON TOP OF a scrollbar?

I'm trying to emulate the CTRL+F functionality from Chrome that highlights matches on the page in the scrollbar, but for certain fields in a form. Using page offsets and percentages, I have blocks of color which correspond to the relative locations of those fields on the page.
In my prototype, the blocks of color sit to the left of the scrollbar. Ideally, they'd sit UNDERNEATH the scrollbar, and the scrollbar's track would be transparent so that it looks like they're part of the scrollbar track.
Can the default scrollbar be set to allow for overflow content to show underneath it (or allow page content to go over it)? I know this could be accomplished if I just rolled my own scroll, but I'd like to use the default ones provided by the browser if at all possible.
It's clearest if you just look at this Prototype.
CSS:
::-webkit-scrollbar {
width: 14px;
height: 18px;
background-color:transparent;
}
::-webkit-scrollbar-track,
::-webkit-scrollbar-track-piece {
background:none;
}
::-webkit-scrollbar-thumb {
height: 6px;
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
-webkit-border-radius: 7px;
background-color: #333
}
::-webkit-scrollbar-button {
width: 0;
height: 0;
display: none;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
I thought of rendering the matches on the trackbar similarly to what browsers do today before. The idea is simple by using linear-gradient background for the ::-webkit-scrollbar-track. However I did not try implementing this. Right after reading your question, I've tried it and looks like it's not such easy.
You can use the linear-gradient background OK, but if you try rendering more than 1 match (a line), it sometimes can't be rendered (especially when the window's size is changed) and the line is not rendered smoothly. Such as this seems to be OK:
//render 2 lines, one is at 50px and the other is at 100px
background: linear-gradient(transparent 50px, red 50px, red 51px, transparent 51px,
transparent 100px, red 100px, red 101px, transparent 101px);
but it's not stable, as I said when you try resizing the window, at some size, some line won't be rendered (at least I tried on Opera). When the window's height is large, the line even becomes blurred (not sharp) and thicker. I don't really understand this, because the color stops are set fixedly (by px, not by %). This issue is even worse when the number of lines is larger. You have a linear-gradient with many corresponding color stops. That seems to be a neat way to solve the problem. Just because of the undesired issue, we can't use that approach.
The new approach: So I tried using multi-backgrounds feature instead. Each background just renders 1 line, the background-size is the same for all the background is just about 2px height and the background-position should be different. Here is the equivalent code (to the above clean code) using this approach:
background: linear-gradient(red, red), linear-gradient(red, red);
background-repeat: no-repeat;
background-size: 100% 2px;
background-position: 0 50px, 0 100px;
The new approach of course requires that the browser has to support multi-backgrounds features (looks like just IE8- do not support this cool feature).
So that's almost what you need to solve this problem. Now we need to find how to apply that style using script. We can't select a pseudo-element (or something like that) via script. We can just use the window.getComputedStyle() method to get the read-only style of a pseudo-element. However we always have a way to modify the CSS directly. That's is by using pure JS with the help of document.styleSheets and cssRules. They allow us to insert/remove/modify a rule.
That looks great. But there is still another issue. When changing the style using that method, the style is not applied right (at least it happens to the ::-webkit-scrollbar-track, it may not happen to other elements). Only when you move the mouse over the scrollbar, the new style is applied. I've just found a simple way to invalidate that scrollbar by setting the overflow of document.documentElement (the html) to hidden and set it back to auto. That works almost well.
Here is the code:
var requiredTb = $(".required input");
var invalids = requiredTb;
var offsets = [];
//init offsets to highlight on the trackbar later
requiredTb.each(function() {
offsets.push(($(this).offset().top)/document.body.scrollHeight * 100);
});
//find the rule styling the -webkit-scrollbar-track
//we added in the CSS stylesheet, this is done just 1 time
var sheets = document.styleSheets;
var trackRule;
for(var i = 0; i < sheets.length; i++){
var rules = sheets[i].cssRules || sheets[i].rules;
for(var j = 0; j < rules.length; j++){
var rule = rules[j];
if(rule.selectorText == "::-webkit-scrollbar-track:vertical"){
trackRule = rule; break;
}
}
}
//define an invalidate() method, we need to use this method
//to refresh the scrollbars, otherwise the newly applied style is not affected
window.invalidate = function(){
$(document.documentElement).css('overflow','hidden');
setTimeout(function(e){
$(document.documentElement).css('overflow','auto');
},1);
};
//this is the main function to set style for the scrollbar track.
function setTrackHighlights(positions, color){
positions.sort();//ensure that the input array should be ascendingly sorted.
trackRule.style.cssText = "";
var gradient = "background: ", backPos = "background-position: ";
var winHeight = $(window).height();
$.each(positions, function(i,pos){
gradient += "linear-gradient(" + color + ", " + color + "),";
backPos += "0 " + pos + "%,"
});
gradient = gradient.substr(0,gradient.length-1) + ";";
backPos = backPos.substr(0,backPos.length -1) + ";";
trackRule.style.cssText += gradient + backPos + "background-repeat:no-repeat; background-size:100% 2px";
invalidate();
}
//initially set the highlights on the trackbar
setTrackHighlights(offsets,'red');
//handle the oninput event to update the highlights accordingly
requiredTb.on('input', function(e){
var required = $(this).closest('.required');
var refreshHighlights = false;
if(this.value && !required.is('.ok')) {
required.addClass('ok');
refreshHighlights = true;
invalids = invalids.not(this);
}
if(!this.value && required.is('.ok')) {
required.removeClass('ok');
refreshHighlights = true;
invalids = invalids.add(this);
}
if(refreshHighlights){
offsets.splice(0);
invalids.each(function() {
offsets.push(($(this).offset().top)/document.body.scrollHeight * 100);
});
setTrackHighlights(offsets,'red');
}
});
You have to add an empty ::-webkit-scrollbar-track:vertical rule (we need to deal only with the vertical scrollbar) in the CSS code, it should be appended at the last to override any similar rule before. We can in fact use the insertRule() method (of a CSSRuleList which can be accessed via cssRules property) to add a new rule instead of looping through the styleSheets, and through the cssRules (of each sheet) to find the empty rule ::-webkit-scrollbar-track:vertical.
The code I posted here can be improved, such as you can add another method setTrackHighlights to allow to add more lines (instead of rendering all the lines each time we need to add/remove just 1 line)...
Note that by using the term line, I mean the rendering representation of a match on the trackbar.
Demo

misunderstood how webkit rendering translate3d(with bug)

Firstly I wanna say, that I'm not looking for the solution of this 'problem', I just wanna understand how it works(or right say - does not work).
So for example I have a block:
#block {
display: block;
position: fixed;
top: 0;
left: 0;
width: 200px;
height: 200px;
background-color: #0000ff;
-webkit-transform: translateZ(0);
}
.hidden {
-webkit-transform: translate3d(-500px, 0, 0)!important;
/*display: none!important;*/
}
and random another block for handle click event with toggle class function for #block and resizing for get scrollbar on screen
And again - YES, I know, I can't use fixed position with translate3d, because coordinate system(actually I'm not fully understand how that works.. :( ), I read specification and bug report on chromium/webkit, but I just wanna understand why, I mean how browser see it and why rendering so strange...
here some observation:
change translate position(fully hidden -> -500px):
- with scrollbar on screen :
render hide/visible through time,
like(one action = one click): hide-visibility(hidden-state) -> hide(visible) -> hide(hidden) -> visible(visible) -> hide(hidden) -||-
- without scrollbar on screen :
nothing rendering; only then something redraw on screen(like scrolling or add textcontent to block(not in layer))
change translate position(visible -> +50px):
- with scrollbar on screen :
works well, changing position on each time
- without scrollbar on screen :
nothing rendering, only then something redraw on screen
change display none/block:
- with scrollbar on screen :
hide block, after nothing rendering; only then something redraw on screen
- without scrollbar on screen :
hide block, after nothing rendering; only then something redraw on screen
with 'position:absolute' changing translate(position) works fine in all cases, 'display: none' same does not render(only then redraw something)
testing only in Chrome 33.0.1726.0, so guess it's works different in another browsers

Twitter Bootstrap - Giving thumbnail caption a minimum number of lines

I have a carousel in Bootstrap that displays 4 columns of thumbnails. Here's the carousel in question. If you move to the third page, you can see that the container increases in height in order to accommodate the contents of the thumbnail captions. I've been trying many things such as setting bottom margins, min heights, etc. to get the position of the "View Details" button constant across the entire carousel.
My question is what is the best way to approach this issue? I was thinking somehow making the thumbnail caption height a minimum of 4 or so lines, but I tried that(probably the wrong way) to no avail.
When I add
.caption h4 {
min-height: 2.2em; /* 2 lines as line-height is 1.1 */
}
I get all "View details" at the same level. However, that obviously doesn't treat the problem of captions being even higher. It only works if no caption is higher in fact. (But it IS ok, if you know for sure nothing is going to be higher than your multiple.)
So, instead I apply this little bit of CSS to put a limit from the other side.
.caption h4 {
max-height: 4.4em; /* 4 lines as line-height is 1.1 */
height: 4.4em; /* making it a multiple allows usage of overflow */
overflow: hidden; /* without cutting a line in the middle */
}
If you want to set a max-height equal to the height of the highest of captions dynamically, than you would have to use a little bit of JS:
(function(d) {
var captions = d.querySelectorAll('.caption h4'),
height = 0;
for(var i = 0; i < captions.length; i++) {
height = Math.max(height, captions[i].offsetHeight); // or clientHeight depends on you box model
}
var style = d.createElement('style');
style.type = 'text/css';
style.innerHTML = '.caption h4 { max-height: '+ height +'px; height: '+ height +'px; }'; // they don't need overflow as none of them can overflow;
d.getElementsByTagName('head')[0].appendChild(style);
})(document);
You add this script at the end of body, so that the DOM is already loaded (or somehow trigger it onload).
Important: this snippet is not supported by older browsers because of the querySelectorAll.
And that does the trick when I run it on your site.

WP8 IE10 viewport issue

Did any of you noticed that when using -ms-viewport (with specific width of 320px or device-width) then web browser content can be moved outside available space? It seems like document size is wrong so i can scroll it's content to the left but there is nothing then white empty space. I can also zoom it out(but i should not) and it's size after that is not always the same. I'm aware of http://mattstow.com/responsive-design-in-ie10-on-windows-phone-8.html but it does not help. It happens after second or third navigate to the same content and disappears for example when device is rotated.
Windows Phone 8 does not properly recognize the meta viewport tag that is standard for webkit and mobile web.
Try this in your CSS
#-ms-viewport{width:device-width}
And then add this JS
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
var msViewportStyle = document.createElement("style");
msViewportStyle.appendChild(
document.createTextNode(
"#-ms-viewport{width:auto!important}"
)
);
document.getElementsByTagName("head")[0].
appendChild(msViewportStyle);
}
More here (credit)
try adding the following
#-ms-viewport {
user-zoom: fixed;
max-zoom: 1;
min-zoom: 1;
}

Resources