overengineer.dev:feed:/blog/feeds/mozilla.xmlDennis Schubert - Blog - Mozilla2024-02-27T09:57:05+00:00overengineer.dev:item:/blog/2021/08/20/webcompat-finger-radius2021-08-20T18:31:52+00:002021-08-25T15:38:19+00:00WebCompat Tale: Touching Clickable ThingsDennis Schubert
<p>Did you know your finger is larger than one pixel? I mean, sure, your <em>physical</em> finger should always be larger than one pixel, unless your screen has a <em>really</em> low resolution. But did you know that when using Firefox for Android, your finger is actually 6x7 millimeters large? Now you do!</p>
<p>Unlike a pixel-perfect input device like a mouse or even a laptop’s trackpad, your finger is weird. Not only is it all soft and squishy, it also actively obstructs your view when touching things on the screen. When you use a web browser and want to click on a link, it is surprisingly difficult to hit it accurately with the <em>center</em> of your fingertip, which is what your touchscreen driver sends to the browser. To help you out, your friendly Firefox for Android helps you out by slightly enlarging the “touch point”.</p>
<p>Usually, this works fine and is completely transparent to users. Sometimes, however, it breaks things.</p>
<p>Here is an example of a clever CSS-only implementation of a menu with collapsible sub-navigation that I extracted <a href="https://github.com/webcompat/web-bugs/issues/75903">from an actual Web Compatibility bug report</a> I looked at earlier. Please do not actually use this, this is broken by design to make a point. :) Purely visual CSS declarations have been omitted for brevity.</p>
<div class="multicol">
<div class="multicol-code-view">
<p><strong>Source</strong>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><style></span>
<span class="nf">#menu-demo</span> <span class="nt">li</span> <span class="nt">ul</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#menu-demo</span> <span class="nt">li</span><span class="nd">:hover</span> <span class="nt">ul</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"><section</span> <span class="na">id=</span><span class="s">"menu-demo"</span><span class="nt">></span>
<span class="nt"><ul></span>
<span class="nt"><li><a</span> <span class="na">href=</span><span class="s">"#menu-demo"</span><span class="nt">></span>One<span class="nt"></a></li></span>
<span class="nt"><li></span>
<span class="nt"><span></span>Two with Subnav<span class="nt"></span></span>
<span class="nt"><ul></span>
<span class="nt"><li><a</span> <span class="na">href=</span><span class="s">"#menu-demo"</span><span class="nt">></span>Two <span class="ni">&gt;</span> One<span class="nt"></a></li></span>
<span class="nt"><li><a</span> <span class="na">href=</span><span class="s">"#menu-demo"</span><span class="nt">></span>Two <span class="ni">&gt;</span> Two<span class="nt"></a></li></span>
<span class="nt"></ul></span>
<span class="nt"></li></span>
<span class="nt"><li><a</span> <span class="na">href=</span><span class="s">"#menu-demo"</span><span class="nt">></span>Three<span class="nt"></a></li></span>
<span class="nt"></ul></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<p><strong>Result</strong>:</p>
<style>
#menu-demo ul {
list-style-type: none;
margin: 0;
padding: 0;
}
#menu-demo li {
border-bottom: 1px solid RGB(var(--main-highlight-color));
}
#menu-demo li a, #menu-demo li span {
box-sizing: border-box;
display: block;
padding: 5px 3px 7px;
text-decoration: none;
}
#menu-demo li ul {
margin-left: 2em;
}
</style>
<style>
#menu-demo li ul {
display: none;
}
#menu-demo li:hover ul {
display: block;
}
</style>
<section id="menu-demo">
<ul>
<li><a href="#menu-demo">One</a></li>
<li>
<span>Two with Subnav</span>
<ul>
<li><a href="#menu-demo">Two > One</a></li>
<li><a href="#menu-demo">Two > Two</a></li>
</ul>
</li>
<li><a href="#menu-demo">Three</a></li>
</ul>
</section>
</div>
</div>
<p>Now, just imagine that on Desktop, this is a horizontal menu and not a vertical list, but I’m too lazy to write media queries right now. It works fine on Desktop. However, if you try this in Firefox for Android, you will find that it’s pretty much impossible to select the second entry, and you will just hit “One” or “Three” most of the time.</p>
<p>To understand what’s going on here, we have to talk about two things: the larger “finger radius” I explained earlier, and the rules by which Firefox detects the element the user <em>probably</em> wanted to click on.</p>
<h2 id="touch-point-expansion">Touch Point expansion</h2>
<p>The current touch point expansion settings, as set by the <code class="language-plaintext highlighter-rouge">ui.mouse.radius.*</code> preferences in <code class="language-plaintext highlighter-rouge">about:config</code>, are: 5mm to the top; 3mm to the left; 3mm to the right; 2mm to the bottom. There probably is a good reason why the top/bottom expansion is asymmetric, and I assume this has something to do with viewing angles or how your finger is shaped, but I actually don’t know.</p>
<p>To visualize this, I prepared a little annotated screenshot of how this “looks like” on my testing Android device:</p>
<p><img src="/statics/blog/20210820/menu-enlarged-box.png" alt="A screenshot of the live menu demo from above. A red dot in the middle of "Two with Subnav" marks the position where the user placed the middle of their finger, a blue border marks the outline of the area Firefox considers "touched". The blue outline spans well into the "One" menu item." class="center" /></p>
<p>The red dot marks the center of the touch point, the blue outline marks the area as expanded by Firefox for Android. As you can see, the expanded touch area covers part of the previous menu item, “One”. If you’d try to touch lower on the item, then the bottom expansion will start to cover parts of the “Three” item. In this example, you have a 9px window to actually hit “Two with Subnav”. On my device, that’s roughly 0.9mm. Good luck with that!</p>
<p>With this expansion in mind, you might wonder why you’re not hitting the wrong items all the time. Fair question.</p>
<h2 id="clickable-elements">“Clickable” elements</h2>
<p>Firefox doesn’t just click on every element inside this expanded area. Instead, Firefox tries to find the “clickable element closest to the original touch point”. If all three <code class="language-plaintext highlighter-rouge"><li></code>s contained links, then this wouldn’t be an issue: links are clickable elements, and “Two with Subnav” would, without a doubt, be the closest. However, in this example, it’s not a link, and then the rules are a little bit more complicated.</p>
<p>Things Firefox considers “clickable” for the purpose of finding the right element:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge"><a></code>s.</li>
<li><code class="language-plaintext highlighter-rouge"><button></code>, <code class="language-plaintext highlighter-rouge"><input></code>, <code class="language-plaintext highlighter-rouge"><select></code>, <code class="language-plaintext highlighter-rouge"><textarea></code>, <code class="language-plaintext highlighter-rouge"><label></code>, and <code class="language-plaintext highlighter-rouge"><iframe></code>.</li>
<li>Elements with JavaScript listeners for:
<ul>
<li><code class="language-plaintext highlighter-rouge">click</code>, <code class="language-plaintext highlighter-rouge">mousedown</code>, <code class="language-plaintext highlighter-rouge">mouseup</code></li>
<li><code class="language-plaintext highlighter-rouge">touchstart</code>, <code class="language-plaintext highlighter-rouge">touchend</code></li>
<li><code class="language-plaintext highlighter-rouge">pointerdown</code>, <code class="language-plaintext highlighter-rouge">pointerup</code></li>
</ul>
</li>
<li>Elements with <code class="language-plaintext highlighter-rouge">contenteditable="true"</code>.</li>
<li>Elements with <code class="language-plaintext highlighter-rouge">role="button"</code>.</li>
<li>Elements with <code class="language-plaintext highlighter-rouge">cursor: pointer</code> assigned via CSS.</li>
</ul>
<p>Unfortunately, none of the rules above are true for the “Two with Subnav” element in the example above. And this means that the “closest clickable element” to the touch point here is, well, “One”. And so, Firefox dispatches the <code class="language-plaintext highlighter-rouge">click</code> event to that one.</p>
<p>Matching any of the conditions, even simply changing the cursor via CSS, would provide the browser with enough context to do “the right thing” here.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This issue, once again, is one of those cases where I do not yet have a satisfying outcome. I wrote a message to the site’s authors, but given the site is based on a Joomla template from 2013, I do not have high hopes here. As for changes inside Firefox, we could treat elements with <code class="language-plaintext highlighter-rouge">:hover</code> styling and <code class="language-plaintext highlighter-rouge">mouseover</code> listeners as “clickable”, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1726786">and I filed a bug to suggest as much</a>, but I’m not yet convinced this is the right thing to do. From what I can tell, neither Chrome nor Safari do a similar expansion, so just dropping it from Firefox is another idea. But I kinda like the way it makes things better 99.9% of the time.</p>
<p>In any case, this serves as yet another reminder of why having semantically correct markup is important. Not only do attributes like <code class="language-plaintext highlighter-rouge">role="button"</code> on clickable elements help out anyone relying on accessibility features and tooling, browsers also depend on these kinds of hints. Use the tools you have, there’s a reason why the <code class="language-plaintext highlighter-rouge">role</code> attribute is part of the web. :)</p>
<h2 id="update-from-2021-08-25">Update from 2021-08-25</h2>
<p>Good news! The developer responsible for the site <a href="https://github.com/webcompat/web-bugs/issues/75903#issuecomment-904881294">has responded</a>, and they did fix the issue by adding <code class="language-plaintext highlighter-rouge">role="button"</code> to the links. Huge success!</p>
overengineer.dev:item:/blog/2021/06/08/webcompat-flex-order2021-06-08T18:40:02+00:002021-06-08T18:40:02+00:00WebCompat Tale: CSS Flexbox and the order of thingsDennis Schubert
<p>Have you thought about the order of things recently? Purely from a web development perspective, I mean.</p>
<p>The chances are that you, just like me, usually don’t spend too much time thinking about the drawing order of elements on your site when writing HTML and CSS. And that’s generally fine because things usually just <em>feel</em> right. Consider the following little example:</p>
<div class="multicol">
<div class="multicol-code-view">
<p><strong>Source</strong>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><style></span>
<span class="nf">#order-demo-one</span> <span class="p">{</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-one</span> <span class="nc">.box</span> <span class="p">{</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">bottom</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">right</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-one</span> <span class="nc">.first</span> <span class="p">{</span>
<span class="nl">background-color</span><span class="p">:</span> <span class="no">blue</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-one</span> <span class="nc">.second</span> <span class="p">{</span>
<span class="nl">background-color</span><span class="p">:</span> <span class="no">red</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"><section</span> <span class="na">id=</span><span class="s">"order-demo-one"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box first"</span><span class="nt">></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box second"</span><span class="nt">></div></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<p><strong>Result</strong>:</p>
<style>
#order-demo-one {
margin: 1em;
}
</style>
<style>
#order-demo-one {
position: relative;
height: 100px;
width: 100px;
}
#order-demo-one .box {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
}
#order-demo-one .first {
background-color: blue;
}
#order-demo-one .second {
background-color: red;
}
</style>
<section id="order-demo-one">
<div class="box first"></div>
<div class="box second"></div>
</section>
</div>
</div>
<p>You could probably tell, without even looking at the result, that the second box - the red one - should be “on top”, completely covering up the blue box. After all, both boxes have the same size and the same position, but since the second box is placed <em>after</em> the first box, it’s drawn <em>on top</em> of the first one. To me, this feels pretty intuitive.</p>
<h2 id="lets-add-some-flex-to-it">Let’s add some Flex to it</h2>
<p>Now, let’s make things a bit more complicated. If you’re reading this article, I hope you’re at least slightly familiar with CSS Flexbox. And as you might know, flex-items have an <code class="language-plaintext highlighter-rouge">order</code> property, which you can use to reorder the items inside a flex container. Here’s the same example as before, but this time inside a flexbox container, with the items reordered. Note that this demo uses the same source as above, but I’m only showing relevant changes here.</p>
<div class="multicol">
<div class="multicol-code-view">
<p><strong>Source</strong>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><style></span>
<span class="nf">#order-demo-two</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-two</span> <span class="nc">.first</span> <span class="p">{</span>
<span class="nl">order</span><span class="p">:</span> <span class="m">2</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-two</span> <span class="nc">.second</span> <span class="p">{</span>
<span class="nl">order</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"><section</span> <span class="na">id=</span><span class="s">"order-demo-two"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box first"</span><span class="nt">></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box second"</span><span class="nt">></div></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<p><strong>Result</strong>:</p>
<style>
#order-demo-two {
margin: 1em;
position: relative;
height: 100px;
width: 100px;
}
#order-demo-two .box {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
}
#order-demo-two .first {
background-color: blue;
}
#order-demo-two .second {
background-color: red;
}
</style>
<style>
#order-demo-two {
display: flex;
}
#order-demo-two .first {
order: 2;
}
#order-demo-two .second {
order: 1;
}
</style>
<section id="order-demo-two">
<div class="box first"></div>
<div class="box second"></div>
</section>
</div>
</div>
<p>Okay, now we used <code class="language-plaintext highlighter-rouge">order</code> to swap positions of the first and second boxes. And as you can see in the demo … nothing changed. What? This is where things start becoming a bit counter-intuitive because this test case is actually a bit of a trick question.</p>
<p>Here is what <a href="https://www.w3.org/TR/css-flexbox-1/#order-property">the CSS Flexbox spec says about the <code class="language-plaintext highlighter-rouge">order</code> property</a>:</p>
<blockquote>
<ol>
<li>A flex container lays out its content in order-modified document order, starting from the lowest numbered ordinal group and going up. Items with the same ordinal group are laid out in the order they appear in the source document.</li>
<li>This also affects the painting order, exactly as if the flex items were reordered in the source document.</li>
<li>Absolutely-positioned children of a flex container are treated as having <code class="language-plaintext highlighter-rouge">order: 0</code> for the purpose of determining their painting order relative to flex items.</li>
</ol>
</blockquote>
<p>(List points added by me; the original is a single block of text.)</p>
<p>Point 1 is what we <em>intuitively</em> know. An element with <code class="language-plaintext highlighter-rouge">order: 2</code> is shown after <code class="language-plaintext highlighter-rouge">order: 1</code>. So far, so good. Point 2, however, says that if you specify <code class="language-plaintext highlighter-rouge">order</code>, the elements should behave as if they have been reordered in the HTML. For our example above, this should mean that both of these HTML snippets should behave the same:</p>
<div class="multicol">
<div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><section</span> <span class="na">id=</span><span class="s">"order-demo-two"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box first"</span><span class="nt">></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box second"</span><span class="nt">></div></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><section</span> <span class="na">id=</span><span class="s">"order-demo-two"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box second"</span><span class="nt">></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box first"</span><span class="nt">></div></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
</div>
<p>But we can clearly see that that’s not how it works. That is because in the spec text above, point 3 says that if a flex item is absolutely-positioned, it is always treated as having <code class="language-plaintext highlighter-rouge">order: 0</code>, so what we define in our CSS doesn’t actually matter.</p>
<h2 id="flex-order-for-real-this-time">Flex order, for real this time.</h2>
<p>So instead of having the absolutely positioned element as the flex item, let’s build another demo that has the absolute element <em>inside</em> the flex item.</p>
<div class="multicol">
<div class="multicol-code-view">
<p><strong>Source</strong>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><style></span>
<span class="nf">#order-demo-three</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-three</span> <span class="nc">.first</span> <span class="p">{</span>
<span class="nl">order</span><span class="p">:</span> <span class="m">2</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-three</span> <span class="nc">.second</span> <span class="p">{</span>
<span class="nl">order</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-three</span> <span class="nc">.inner</span> <span class="p">{</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">bottom</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">right</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-three</span> <span class="nc">.first</span> <span class="nc">.inner</span> <span class="p">{</span>
<span class="nl">background-color</span><span class="p">:</span> <span class="no">blue</span><span class="p">;</span>
<span class="p">}</span>
<span class="nf">#order-demo-three</span> <span class="nc">.second</span> <span class="nc">.inner</span> <span class="p">{</span>
<span class="nl">background-color</span><span class="p">:</span> <span class="no">red</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"><section</span> <span class="na">id=</span><span class="s">"order-demo-three"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box first"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"inner"</span><span class="nt">></div></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box second"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"inner"</span><span class="nt">></div></span>
<span class="nt"></div></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<p><strong>Result</strong>:</p>
<style>
#order-demo-three {
margin: 1em;
}
</style>
<style>
#order-demo-three {
display: flex;
position: relative;
height: 100px;
width: 100px;
}
#order-demo-three .first {
order: 2;
}
#order-demo-three .second {
order: 1;
}
#order-demo-three .inner {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
}
#order-demo-three .first .inner {
background-color: blue;
}
#order-demo-three .second .inner {
background-color: red;
}
</style>
<section id="order-demo-three">
<div class="box first">
<div class="inner"></div>
</div>
<div class="box second">
<div class="inner"></div>
</div>
</section>
</div>
</div>
<p>And now, I might have lost you. Because, as of the time of writing this, what you see as the result depends on which browser you read this blog post in. In Firefox, you’ll see the blue box on top; but pretty much everywhere else, the red box will still be on top.</p>
<p>The question now is: who is right? And instead of just telling you the answer, let’s work it out together. Rule 3 from above does not apply here: The flex items are not absolutely positioned, so the order should be taken into consideration. To check if that’s the case, we can look at rule 2: the code should behave the same if we reorder the elements in the HTML. We can build a test for that:</p>
<div class="multicol">
<div class="multicol-code-view">
<p><strong>Source</strong>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><section</span> <span class="na">id=</span><span class="s">"order-demo-four"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box second"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"inner"</span><span class="nt">></div></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"box first"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"inner"</span><span class="nt">></div></span>
<span class="nt"></div></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<p><strong>Result</strong>:</p>
<style>
#order-demo-four {
margin: 1em;
display: flex;
position: relative;
height: 100px;
width: 100px;
}
#order-demo-four .first {
order: 2;
}
#order-demo-four .second {
order: 1;
}
#order-demo-four .inner {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
}
#order-demo-four .first .inner {
background-color: blue;
}
#order-demo-four .second .inner {
background-color: red;
}
</style>
<section id="order-demo-four">
<div class="box second">
<div class="inner"></div>
</div>
<div class="box first">
<div class="inner"></div>
</div>
</section>
</div>
</div>
<p>(again, the code is the same as the one in <code class="language-plaintext highlighter-rouge">#order-demo-three</code>, but I’m just showing the HTML to keep it easier to read)</p>
<p>If you’re reading this in Firefox, then the last two test cases behave the same: they’ll show the blue box. However, if you’re in Chrome, Safari, or Edge, there will be a difference: the first case will show the red box, the second case shows the blue box. If you now think that this is a bug in Blink and WebKit: you are right, <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=606208">and that bug has been known for a while</a>.</p>
<h2 id="uh-what-now">Uh, what now?</h2>
<p>This might sound like a super weird edge-case, and that’s probably right. It <em>is</em> a weird edge case. But unfortunately, as with pretty much all things that end up on my desk, we discovered this edge-case by investigating real-world breakage. Here, <a href="https://github.com/webcompat/web-bugs/issues/73426">we received a report about the flight date picker on flydubai.com being broken</a>, where in Firefox, there is an advertising banner on top of the picker. That’s caused by what I described here.</p>
<p>The Blink issue I linked earlier was opened in 2016, and there hasn’t been much progress on there since. I’m not saying this to blame Google folks; that’s just highlighting that changing things like this is a bit tricky sometimes. While there appears to be a consensus that Firefox is right, changing Chrome to match Firefox could result in an undefined number of broken sites. So you have to be careful when pushing such a change.</p>
<p>For now, I decided to go ahead and <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1715302">add a web-platform-test for this, because there is none yet</a>. Currently, there’s also a cross-browser compat effort, “<a href="https://web.dev/compat2021/">Compat2021</a>”, going on, and CSS Flexbox is one of the key areas everyone wants to work on to make it a bit less of a pain for web developers. Maybe we can get some progress done on this issue as well. I will certainly try!</p>
<p>And with that, I have to end this post. There is no happy end, there isn’t even a certainty on what - if anything - will happen next. Sometimes, that’s the nature of our work. And I think that’s worth sharing, too.</p>
overengineer.dev:item:/blog/2021/05/26/webcompat-text-indent2021-05-26T18:03:32+00:002021-05-26T18:03:32+00:00WebCompat PSA: Please don't use negative `text-indent`s for hidden labels.Dennis Schubert
<p>During my work on Web Compatibility at Mozilla, I see many things that break in exciting ways. Sometimes, it’s obvious stuff like flexbox compat issues<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, but sometimes, the breakages are a bit surprising. Today, the star of the show is a single CSS instruction:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>text-indent: -9999px
</code></pre></div></div>
<p>When we talk about web compatibility issues, most people think about an elite subset of “well-known” breakages or massive layout issues. They rarely think about innocent-looking things like text-indent. And to be fair, most of the time, neither do we browser people.</p>
<p>This large negative <code class="language-plaintext highlighter-rouge">text-indent</code> appears to be a hack, frequently used to “move away” labels next to icons, probably to hide them from view but keep them in the markup for screen readers and similar user agents. <strong>Please don’t do that</strong>, there are <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute">better alternatives</a> for screenreaders. Even though having a negative indentation seems like a good solution, the unfortunate reality is that <code class="language-plaintext highlighter-rouge">text-indent</code> has some weird cross-browser quirks. Two examples that I stumbled across in the last month:</p>
<ul>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1477110">Some disagreement between browsers around using relative units in <code class="language-plaintext highlighter-rouge">text-indent</code> inside form elements</a>.</li>
<li><a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1159311">A weird issue in Blink where it ignored <code class="language-plaintext highlighter-rouge">text-indent</code> on anonymous flex items</a>.</li>
</ul>
<p>… and there are a <em>lot</em> more.</p>
<p><code class="language-plaintext highlighter-rouge">text-indent</code> does extend the size of an element, but not in a fixed direction, but depending on the direction of text flow. Here’s a quick example:</p>
<div class="multicol">
<div class="multicol-code-view">
<p><strong>Source</strong>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nt"><style></span>
<span class="nf">#text-indent-demo</span> <span class="nt">p</span> <span class="p">{</span>
<span class="nl">text-indent</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"><section</span> <span class="na">id=</span><span class="s">"text-indent-demo"</span><span class="nt">></span>
<span class="nt"><p</span> <span class="na">style=</span><span class="s">"direction: ltr;"</span><span class="nt">></span>one<span class="nt"></p></span>
<span class="nt"><p</span> <span class="na">style=</span><span class="s">"direction: rtl;"</span><span class="nt">></span>two<span class="nt"></p></span>
<span class="nt"><p</span> <span class="na">style=</span><span class="s">"direction: ltr; writing-mode: vertical-lr;"</span><span class="nt">></span>three<span class="nt"></p></span>
<span class="nt"><p</span> <span class="na">style=</span><span class="s">"direction: rtl; writing-mode: vertical-lr;"</span><span class="nt">></span>five<span class="nt"></p></span>
<span class="nt"></section></span>
</code></pre></div> </div>
</div>
<div>
<p><strong>Result</strong>:</p>
<style>
#text-indent-demo p {
border: 3px solid gray;
float: left;
margin: 10px;
padding: 10px;
}
</style>
<style>
#text-indent-demo p {
text-indent: 100px;
}
</style>
<section id="text-indent-demo">
<p style="direction: ltr;">one</p>
<p style="direction: rtl;">two</p>
<p style="direction: ltr; writing-mode: vertical-lr;">three</p>
<p style="direction: rtl; writing-mode: vertical-lr;">five</p>
</section>
</div>
</div>
<p>As you can see, we have the same <code class="language-plaintext highlighter-rouge">text-indent: 100px;</code>, but in four different directions depending on the text direction and writing mode. This makes perfect sense if you think about it, but developers can get caught off-guard here, especially if working on a site that later gets translated. Or, well, if browsers misbehave.</p>
<p>On an <a href="https://github.com/webcompat/web-bugs/issues/72189#issuecomment-830575592">Israeli site I recently looked at</a>, a large negative <code class="language-plaintext highlighter-rouge">text-indent</code> caused the site to be extended <em>to the right</em>, which caused some viewport issues in Firefox for Android because we try to fit everything into your view. Another example is <a href="https://github.com/webcompat/web-bugs/issues/71565#issuecomment-829278135">a report about a Romanian news site</a>, where clicking on the social links left a dotted border all across the screen because they extended their buttons <code class="language-plaintext highlighter-rouge">9999px</code> to the left without <code class="language-plaintext highlighter-rouge">overflow: hidden</code>‘ing it. In Chrome, this particular case is not noticeable because Chrome does not show focus borders the same way Firefox does, but the issue is still there. There are more examples of things going wrong in unexpected ways, but you get the gist.</p>
<p>While I am only talking about <code class="language-plaintext highlighter-rouge">text-indent</code> here, mainly because the text direction dependency adds an interesting twist, note that all methods of “moving something out of the screen to make it invisible” have similar issues. Even if you move things really far away, they still exist inside the document, and they can have unexpected side effects.</p>
<p>So… please don’t. The web is broken enough already. :)</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Spoiler: there soon will be another blog post, about a flexbox issue! Wohoo! <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
overengineer.dev:item:/blog/2018/11/10/observing-broken-image-intersections2018-11-10T02:39:42+00:002018-11-10T02:39:42+00:00Observing Broken Image IntersectionsDennis Schubert
<p>So, I thought I am going to share this little story about a broken website I looked at recently, just to make an example of how <em>weird</em> the web can be, and how Gray implementing specifications can sometimes be.</p>
<p>Imagine a site<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> with a list of news articles and lots of thumbnails next to them, and imagine the served code looks something like:</p>
<div class="multicol">
<div>
<p>CSS:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.thumbnail</span> <span class="p">{</span>
<span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.thumbnail</span> <span class="nt">img</span> <span class="p">{</span>
<span class="nl">height</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">margin-left</span><span class="p">:</span> <span class="m">-42px</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">213px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div> </div>
</div>
<div>
<p>HTML:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"thumbnail"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">data-lazyload-src=</span><span class="s">"foo.jpg"</span> <span class="nt">/></span>
<span class="nt"></div></span>
</code></pre></div> </div>
</div>
</div>
<p>That does not look too fancy, does it? The <code class="language-plaintext highlighter-rouge">data-lazyload-src</code> attribute on the image suggests they are doing some kind of image lazyloading, but that is probably a good thing given the site is mobile optimized as well, and you do not want to load a lot of thumbnails at once. The site is actually pretty smart about it, and is using a library that implements an <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API"><code class="language-plaintext highlighter-rouge">IntersectionObserver</code></a> to be notified whenever an <code class="language-plaintext highlighter-rouge"><img></code> scrolls into view, to then trigger loading the image. Pretty cool stuff.</p>
<p>Now, the fun part. We received a report that the site is working fine in Chrome, but for some reason, in Firefox, the thumbnails never load. Pretty bad.</p>
<p>After evenly distributing breakpoints in code I deemed relevant, it turned out the <code class="language-plaintext highlighter-rouge">IntersectionObserver</code> never triggers the image loading. As my knowledge about the <code class="language-plaintext highlighter-rouge">IntersectionObserver</code> was still stuck in 2014 (which is pretty much nothing, given the work on it started in 2015), I took the time to read the spec, because clearly, Firefox has a compat issue breaking that website. And well, I <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1505471">actually found a compat issue</a>, but that one was completely irrelevant to the issue I was originally debugging.</p>
<p>So, back to the beginning. Looking again at their <code class="language-plaintext highlighter-rouge">IntersectionObserver</code>, I realized that Firefox is calling the callback for a lot of images, but in Firefox, <code class="language-plaintext highlighter-rouge">IntersectionObserverEntry.isIntersecting</code> is <code class="language-plaintext highlighter-rouge">false</code>, even for the images that <em>should</em> be <code class="language-plaintext highlighter-rouge">true</code> as they are scrolled into view. In Chrome, everything is fine, and some of the thumbnails are reported to be intersecting.</p>
<p>Before you scroll up to check the source code again, let me remind you that images should be rendered as <code class="language-plaintext highlighter-rouge">display: inline;</code> per default, as you surely remember<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. Now, what do you expect the CSS to do in the default case where no image is loaded:</p>
<ol>
<li>Scale the image to 213px width with some magic height.</li>
<li>No dimensions applied to the image, because it is <code class="language-plaintext highlighter-rouge">display: inline;</code>, d’oh!</li>
<li>Render a “broken image” icon, but it is replaced with a picture of a cute kitten.</li>
</ol>
<p>If you guessed 1, 2, or 3: Congratulations, you are wrong! As we all know, CSS is easy, and this is one of those cases where CSS is super easy. So, let me explain this simple CSS behavior by talking spec for a second here. <code class="language-plaintext highlighter-rouge"><img></code> is, amongst some others<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>, a so-called <em>replaced element</em>. <a href="https://drafts.csswg.org/css2/conform.html#replaced-element">The spec</a> accurately describes those as</p>
<blockquote>
<p>An element whose content is outside the scope of the CSS formatting model, such as an image, embedded document, or applet. For example, the content of the HTML IMG element is often replaced by the image that its “src” attribute designates</p>
</blockquote>
<p>which is basically the spec authors telling you “yeah, we also do not know how it looks like”. For images, there are <a href="https://html.spec.whatwg.org/multipage/rendering.html#images-3">some rules</a> on how the browser should render things:</p>
<blockquote>
<ul>
<li>
<p>If the element does not represent an image, but the element already has intrinsic dimensions (e.g. from the dimension attributes or CSS rules), and either:</p>
<ul>
<li>the user agent has reason to believe that the image will become available and be rendered in due course, or</li>
<li>the element has no alt attribute, or</li>
<li>the Document is in quirks mode</li>
</ul>
<p>The user agent is expected to treat the element as a replaced element whose content is the text that the element represents, if any, optionally alongside an icon indicating that the image is being obtained (if applicable).</p>
</li>
<li>
<p>If the element is an img element that represents some text and the user agent does not expect this to change</p>
<p>The user agent is expected to treat the element as a non-replaced phrasing element whose content is the text, optionally with an icon indicating that an image is missing, so that the user can request the image be displayed or investigate why it is not rendering. In non-graphical contexts, such an icon should be omitted.</p>
</li>
<li>
<p>If the element is an img element that represents nothing and the user agent does not expect this to change</p>
<p>The user agent is expected to treat the element as an empty inline element. (In the absence of further styles, this will cause the element to essentially not be rendered.)</p>
</li>
</ul>
</blockquote>
<p>There are some nasty spec language bits in there, but in order to not bother you more than I need, I will skip those, but you get the idea. If you scroll back up to the source, you will notice two things: the image tag in question does not have a <code class="language-plaintext highlighter-rouge">src</code> attribute, and to add more fun to the mix, it also does not have an <code class="language-plaintext highlighter-rouge">alt</code> attribute, but it <em>does</em> have intrinsic dimensions, as they are defined via CSS.</p>
<p>So, technically, the first case is true: the element is not an image, and it also does not have an <code class="language-plaintext highlighter-rouge">alt</code> attribute. But what does “treat the element as a replaced element whose content is the text that the element represents” even mean? How are we supposed to replace nothing with text? Because there is no text, the last case is <em>also</em> true, because there is nothing there, and because there is no <code class="language-plaintext highlighter-rouge">src</code> attribute to be loaded, the browser also does not expect this to change.</p>
<p>To my understanding, this means the browser can replace the element with either <em>something</em> or with <em>nothing</em>. Well, let’s see what different browsers do:</p>
<div class="multicol">
<div>
<p>HTML:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><img><hr></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"broken image!"</span><span class="nt">><hr></span>
<span class="nt"><img</span> <span class="na">alt=</span><span class="s">""</span><span class="nt">><hr></span>
<span class="nt"><img</span> <span class="na">alt=</span><span class="s">"poetic alt text"</span><span class="nt">><hr></span>
</code></pre></div> </div>
</div>
<div>
<p><img src="/statics/blog/20181110/rendering_comparison.png" alt="Comparison of the code example's rendering in Firefox, Chrome, Edge, and Safari" style="width: 100%; max-width: 382px;" /></p>
</div>
</div>
<p>As it turns out, browsers disagree in our relevant case. In Firefox, we render <em>nothing</em> as an <code class="language-plaintext highlighter-rouge">inline</code> element, and Chrome decides to render <em>something empty</em> as <code class="language-plaintext highlighter-rouge">inline-block</code>.</p>
<p>Even worse, I am having a hard time figuring out who is <em>right</em> and who is <em>wrong</em> here. There are two Chrome issues (<a href="https://bugs.chromium.org/p/chromium/issues/detail?id=671871">one</a>, <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=753868">two</a>) about this specific scenario, and a <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196668">Firefox patch</a> landing just as I write this that brings Firefox closer to Chrome, at least in the no-<code class="language-plaintext highlighter-rouge">src</code> scenario. But still, there seems to be a general disagreement on what the <em>right</em> thing is.</p>
<p>To end this whole post: if you paid attention<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>, you have figured out the original issue by now.</p>
<p>Because Firefox renders <em>nothing</em> (that is actually not entirely true, but let us act like it is, because the reality would turn this post into a proper scientific paper), there is <em>nothing</em> that can ever intersect the viewport, so the <code class="language-plaintext highlighter-rouge">IntersectinObserver</code> returns, rightfully so, <code class="language-plaintext highlighter-rouge">false</code>. On Chrome, however, there is <em>something</em> that is <code class="language-plaintext highlighter-rouge">213px</code> wide, so there is <em>something</em> that intersects the viewport, so there is something for the observer to report on.</p>
<p>And there is our issue. Quite simple, eh?</p>
<p>The sad thing out of all is there is a very, very simple solution to all of this.</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.thumbnail</span> <span class="nt">img</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And they <em>would</em> live happily ever after.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>This is <em>totally not</em> <a href="https://webcompat.com/issues/18554">webcompat.com bug #18554</a>, and I am <em>totally not</em> trying to write a <a href="https://miketaylr.com/">miketaylr.com style blog post</a> here. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>Yeah, me neither. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:3" role="doc-endnote">
<p><code class="language-plaintext highlighter-rouge">audio</code>, <code class="language-plaintext highlighter-rouge">canvas</code>, <code class="language-plaintext highlighter-rouge">embed</code>, <code class="language-plaintext highlighter-rouge">iframe</code>, <code class="language-plaintext highlighter-rouge">input</code>, <code class="language-plaintext highlighter-rouge">object</code>, and <code class="language-plaintext highlighter-rouge">video</code>, if you really want to know. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:4" role="doc-endnote">
<p>Yeah, me neither. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>