<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[242401] trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/242401">242401</a></dd>
<dt>Author</dt> <dd>wenson_hsieh@apple.com</dd>
<dt>Date</dt> <dd>2019-03-04 16:31:31 -0800 (Mon, 04 Mar 2019)</dd>
</dl>

<h3>Log Message</h3>
<pre>Native text selection UI is incorrectly suppressed in Microsoft Visio
https://bugs.webkit.org/show_bug.cgi?id=195178
<rdar://problem/48519394>

Reviewed by Darin Adler.

Source/WebCore:

Currently, our heuristics for detecting hidden editable areas attempt to search for empty parent renderers with
"overflow: hidden". It does this by ascending the layer tree in search of renderers that have an empty content
size, and whose renderers' styles indicate that they have overflow: hidden in the X or Y directions. This fails
in the case where a child renderer is positioned out of flow, relative to one of its parent layers, since the
child will be visible, but we'll incorrectly believe that it is hidden. This leads to selection UI unexpectedly
disappearing in the online version of Microsoft Visio.

To fix this, we check whether the enclosing layer around the editable element has an empty clip rect; if the
element is inside of a subframe, we additionally walk up to each enclosing frame's layer and check if that
frame's layer has an empty clip rect.

Test: editing/selection/ios/do-not-hide-selection-in-visible-container.html

* rendering/RenderObject.cpp:
(WebCore::RenderObject::isTransparentOrFullyClippedRespectingParentFrames const):

LayoutTests:

Add a new layout test that focuses several different text fields and checks whether or not editing UI is shown:

1. A text field inside an overflow: hidden container, all within an absolutely positioned iframe, such that the
text field is not visible. The caret should be hidden.

2. A text field inside an absolutely positioned iframe, inside an overflow: hidden container, such that the
text field is visible. The caret should be visible.

3. A text field inside a relatively positioned iframe in an overflow: hidden container, such that the text field
is not visible. The caret should be hidden.

4. A text field that is position: fixed inside an overflow: hidden container, such that the text field is
visible. The caret should be visible.

* editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt: Added.
* editing/selection/ios/do-not-hide-selection-in-visible-container.html: Added.
* editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html:
* resources/ui-helper.js:
(window.UIHelper.activateElementAndWaitForInputSession):

Add a convenience function in UIHelper that taps a given element and waits for the keyboard to show.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsChangeLog">trunk/LayoutTests/ChangeLog</a></li>
<li><a href="#trunkLayoutTestseditingselectionioshideselectioninemptyoverflowhiddencontainerhtml">trunk/LayoutTests/editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html</a></li>
<li><a href="#trunkLayoutTestsresourcesuihelperjs">trunk/LayoutTests/resources/ui-helper.js</a></li>
<li><a href="#trunkSourceWebCoreChangeLog">trunk/Source/WebCore/ChangeLog</a></li>
<li><a href="#trunkSourceWebCorerenderingRenderObjectcpp">trunk/Source/WebCore/rendering/RenderObject.cpp</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkLayoutTestseditingselectioniosdonothideselectioninvisiblecontainerexpectedtxt">trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt</a></li>
<li><a href="#trunkLayoutTestseditingselectioniosdonothideselectioninvisiblecontainerhtml">trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkLayoutTestsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/ChangeLog (242400 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/ChangeLog      2019-03-05 00:09:15 UTC (rev 242400)
+++ trunk/LayoutTests/ChangeLog 2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -1,3 +1,33 @@
</span><ins>+2019-03-04  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Native text selection UI is incorrectly suppressed in Microsoft Visio
+        https://bugs.webkit.org/show_bug.cgi?id=195178
+        <rdar://problem/48519394>
+
+        Reviewed by Darin Adler.
+
+        Add a new layout test that focuses several different text fields and checks whether or not editing UI is shown:
+
+        1. A text field inside an overflow: hidden container, all within an absolutely positioned iframe, such that the
+        text field is not visible. The caret should be hidden.
+
+        2. A text field inside an absolutely positioned iframe, inside an overflow: hidden container, such that the
+        text field is visible. The caret should be visible.
+
+        3. A text field inside a relatively positioned iframe in an overflow: hidden container, such that the text field
+        is not visible. The caret should be hidden.
+
+        4. A text field that is position: fixed inside an overflow: hidden container, such that the text field is
+        visible. The caret should be visible.
+
+        * editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt: Added.
+        * editing/selection/ios/do-not-hide-selection-in-visible-container.html: Added.
+        * editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html:
+        * resources/ui-helper.js:
+        (window.UIHelper.activateElementAndWaitForInputSession):
+
+        Add a convenience function in UIHelper that taps a given element and waits for the keyboard to show.
+
</ins><span class="cx"> 2019-03-04  Daniel Bates  <dabates@apple.com>
</span><span class="cx"> 
</span><span class="cx">         [iOS] Caret x-position in empty text area does not match text field
</span></span></pre></div>
<a id="trunkLayoutTestseditingselectioniosdonothideselectioninvisiblecontainerexpectedtxt"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt (0 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt                          (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt     2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -0,0 +1,18 @@
</span><ins>+First field  Second field  Third field Fourth field 
+
+Verifies that native selection UI is not suppressed when focusing an input that is inside an empty container with `overflow: hidden`, but is positioned absolutely such that it is still visible. To manually verify, click on each of the four buttons to move focus into editable areas. After tapping the first and third buttons, you should not see a caret view; after tapping the second and fourth buttons, you should see a caret view.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+Waiting for caret to hide.
+PASS Caret was hidden.
+Waiting for caret to show.
+PASS Caret was shown.
+Waiting for caret to hide.
+PASS Caret was hidden.
+Waiting for caret to show.
+PASS Caret was shown.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
</ins></span></pre></div>
<a id="trunkLayoutTestseditingselectioniosdonothideselectioninvisiblecontainerhtml"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html (0 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html                          (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html     2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -0,0 +1,105 @@
</span><ins>+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+<script src="../../../resources/js-test.js"></script>
+<script src="../../../resources/ui-helper.js"></script>
+<style>
+.hidden {
+    height: 0;
+    overflow: hidden;
+}
+
+iframe {
+    width: 100px;
+    height: 44px;
+    position: absolute;
+    border: none;
+}
+
+#frame2 {
+    top: 100px;
+}
+
+#frame3 {
+    position: relative;
+}
+
+input {
+    position: fixed;
+}
+</style>
+<script>
+jsTestIsAsync = true;
+
+async function ensureCaretIsVisible(isVisible)
+{
+    if (isVisible)
+        debug("Waiting for caret to show.");
+    else
+        debug("Waiting for caret to hide.");
+
+    let rect = null;
+    while (!rect || (!isVisible && rect.width && rect.height) || (isVisible && (!rect.width || !rect.height)))
+        rect = await UIHelper.getUICaretViewRect();
+
+    if (isVisible)
+        testPassed("Caret was shown.");
+    else
+        testPassed("Caret was hidden.");
+}
+
+addEventListener("load", async () => {
+    description("Verifies that native selection UI is not suppressed when focusing an input that is inside an empty container with `overflow: hidden`, but is positioned absolutely such that it is still visible. To manually verify, click on each of the four buttons to move focus into editable areas. After tapping the first and third buttons, you should not see a caret view; after tapping the second and fourth buttons, you should see a caret view.");
+
+    if (!window.testRunner)
+        return;
+
+    for (const testCase of [[first, false], [second, true], [third, false], [fourth, true]]) {
+        const [buttonToTap, expectedVisibility] = testCase;
+        await UIHelper.activateElementAndWaitForInputSession(buttonToTap);
+        await ensureCaretIsVisible(expectedVisibility);
+        document.activeElement.blur();
+    }
+
+    await UIHelper.waitForKeyboardToHide();
+    finishJSTest();
+});
+</script>
+</head>
+<body class="hidden">
+    <button id="first">First field</button>
+    <button id="second">Second field</button>
+    <button id="third">Third field</button>
+    <button id="fourth">Fourth field</button>
+    <iframe id="frame1" srcdoc="
+        <div style='overflow: hidden; height: 0; box-sizing: border-box; border: none; padding: 0;'>
+            <input></input>
+        </div>"></iframe>
+    <div class="hidden">
+        <iframe id="frame2" srcdoc="<input></input>"></iframe>
+        <iframe id="frame3" srcdoc="<input></input>"></iframe>
+        <input></input>
+    </div>
+    <div id="description"></div>
+    <div id="console"></div>
+</body>
+<script>
+function createFrameFocusHandler(identifier) {
+    return (event) => {
+        const frame = document.getElementById(identifier);
+        frame.focus();
+        frame.contentDocument.querySelector("input").focus();
+        event.preventDefault();
+    };
+}
+
+first.addEventListener("click", createFrameFocusHandler("frame1"));
+second.addEventListener("click", createFrameFocusHandler("frame2"));
+third.addEventListener("click", createFrameFocusHandler("frame3"));
+fourth.addEventListener("click", event => {
+    document.querySelector("input").focus();
+    event.preventDefault();
+});
+</script>
+</html>
</ins></span></pre></div>
<a id="trunkLayoutTestseditingselectionioshideselectioninemptyoverflowhiddencontainerhtml"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html (242400 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html   2019-03-05 00:09:15 UTC (rev 242400)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html      2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -26,10 +26,9 @@
</span><span class="cx"> }
</span><span class="cx"> </style>
</span><span class="cx"> <script>
</span><del>-addEventListener("load", runTest);
</del><span class="cx"> jsTestIsAsync = true;
</span><span class="cx"> 
</span><del>-async function runTest() {
</del><ins>+addEventListener("load", async () => {
</ins><span class="cx">     description("Verifies that native selection UI is suppressed when focusing a textarea that is completely hidden "
</span><span class="cx">         + "underneath an empty container with <code>overflow: hidden;</code>. To manually test, tap the top button, "
</span><span class="cx">         + "and then tap on the bottom button. In both cases, there should be no platform selection shown.");
</span><span class="lines">@@ -64,9 +63,9 @@
</span><span class="cx"> 
</span><span class="cx">     frame.contentWindow.document.activeElement.blur();
</span><span class="cx">     await UIHelper.waitForKeyboardToHide();
</span><del>-    document.querySelectorAll("button, #container").forEach(element => element.remove())
</del><ins>+    document.querySelectorAll("button, #container").forEach(element => element.remove());
</ins><span class="cx">     finishJSTest();
</span><del>-}
</del><ins>+});
</ins><span class="cx"> </script>
</span><span class="cx"> </head>
</span><span class="cx"> <body>
</span><span class="lines">@@ -76,7 +75,7 @@
</span><span class="cx"> </button>
</span><span class="cx"> <div id="container">
</span><span class="cx">     <textarea id="editor"></textarea>
</span><del>-    <iframe id="frame" srcdoc="<textarea id='editor'></textarea>" onload="runTest()"></iframe>
</del><ins>+    <iframe id="frame" srcdoc="<textarea id='editor'></textarea>"></iframe>
</ins><span class="cx"> </div>
</span><span class="cx"> <div id="description"></div>
</span><span class="cx"> <div id="console"></div>
</span></span></pre></div>
<a id="trunkLayoutTestsresourcesuihelperjs"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/resources/ui-helper.js (242400 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/resources/ui-helper.js 2019-03-05 00:09:15 UTC (rev 242400)
+++ trunk/LayoutTests/resources/ui-helper.js    2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -224,6 +224,13 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static activateElementAndWaitForInputSession(element)
+    {
+        const x = element.offsetLeft + element.offsetWidth / 2;
+        const y = element.offsetTop + element.offsetHeight / 2;
+        return this.activateAndWaitForInputSessionAt(x, y);
+    }
+
</ins><span class="cx">     static activateFormControl(element)
</span><span class="cx">     {
</span><span class="cx">         if (!this.isWebKit2() || !this.isIOS())
</span></span></pre></div>
<a id="trunkSourceWebCoreChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/ChangeLog (242400 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/ChangeLog   2019-03-05 00:09:15 UTC (rev 242400)
+++ trunk/Source/WebCore/ChangeLog      2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -1,3 +1,27 @@
</span><ins>+2019-03-04  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Native text selection UI is incorrectly suppressed in Microsoft Visio
+        https://bugs.webkit.org/show_bug.cgi?id=195178
+        <rdar://problem/48519394>
+
+        Reviewed by Darin Adler.
+
+        Currently, our heuristics for detecting hidden editable areas attempt to search for empty parent renderers with
+        "overflow: hidden". It does this by ascending the layer tree in search of renderers that have an empty content
+        size, and whose renderers' styles indicate that they have overflow: hidden in the X or Y directions. This fails
+        in the case where a child renderer is positioned out of flow, relative to one of its parent layers, since the
+        child will be visible, but we'll incorrectly believe that it is hidden. This leads to selection UI unexpectedly
+        disappearing in the online version of Microsoft Visio.
+
+        To fix this, we check whether the enclosing layer around the editable element has an empty clip rect; if the
+        element is inside of a subframe, we additionally walk up to each enclosing frame's layer and check if that
+        frame's layer has an empty clip rect.
+
+        Test: editing/selection/ios/do-not-hide-selection-in-visible-container.html
+
+        * rendering/RenderObject.cpp:
+        (WebCore::RenderObject::isTransparentOrFullyClippedRespectingParentFrames const):
+
</ins><span class="cx"> 2019-03-01  Ryosuke Niwa  <rniwa@webkit.org>
</span><span class="cx"> 
</span><span class="cx">         gPictureOwnerMap is unnecessary
</span></span></pre></div>
<a id="trunkSourceWebCorerenderingRenderObjectcpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/rendering/RenderObject.cpp (242400 => 242401)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/rendering/RenderObject.cpp  2019-03-05 00:09:15 UTC (rev 242400)
+++ trunk/Source/WebCore/rendering/RenderObject.cpp     2019-03-05 00:31:31 UTC (rev 242401)
</span><span class="lines">@@ -1527,38 +1527,40 @@
</span><span class="cx">     delete this;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+static RenderLayer* enclosingFrameRenderLayer(RenderObject& renderObject)
+{
+    auto* owner = renderObject.frame().ownerElement();
+    if (!owner)
+        return nullptr;
+
+    auto* frameOwnerRenderer = owner->renderer();
+    return frameOwnerRenderer ? frameOwnerRenderer->enclosingLayer() : nullptr;
+}
+
+static RenderLayer* parentLayerCrossingFrameBoundaries(RenderLayer& layer)
+{
+    if (auto* parentLayer = layer.parent())
+        return parentLayer;
+
+    return enclosingFrameRenderLayer(layer.renderer());
+}
+
</ins><span class="cx"> bool RenderObject::isTransparentOrFullyClippedRespectingParentFrames() const
</span><span class="cx"> {
</span><span class="cx">     static const double minimumVisibleOpacity = 0.01;
</span><span class="cx"> 
</span><span class="cx">     float currentOpacity = 1;
</span><del>-    auto* layer = enclosingLayer();
-    while (layer) {
-        auto& layerRenderer = layer->renderer();
-        auto& style = layerRenderer.style();
-        if (auto* box = layer->renderBox()) {
-            bool isOverflowHidden = style.overflowX() == Overflow::Hidden || style.overflowY() == Overflow::Hidden;
-            if (isOverflowHidden && !box->isDocumentElementRenderer() && box->contentSize().isEmpty())
-                return true;
-        }
-        currentOpacity *= style.opacity();
</del><ins>+    for (auto* layer = enclosingLayer(); layer; layer = parentLayerCrossingFrameBoundaries(*layer)) {
+        currentOpacity *= layer->renderer().style().opacity();
</ins><span class="cx">         if (currentOpacity < minimumVisibleOpacity)
</span><span class="cx">             return true;
</span><ins>+    }
</ins><span class="cx"> 
</span><del>-        auto* parentLayer = layer->parent();
-        if (!parentLayer) {
-            if (!is<RenderView>(layerRenderer))
-                return false;
</del><ins>+    for (auto* layer = enclosingLayer(); layer; layer = enclosingFrameRenderLayer(layer->renderer())) {
+        if (layer->selfClipRect().isEmpty())
+            return true;
+    }
</ins><span class="cx"> 
</span><del>-            auto& enclosingFrame = downcast<RenderView>(layerRenderer).view().frame();
-            if (enclosingFrame.isMainFrame())
-                return false;
-
-            if (auto *frameOwnerRenderer = enclosingFrame.ownerElement()->renderer())
-                parentLayer = frameOwnerRenderer->enclosingLayer();
-        }
-        layer = parentLayer;
-    }
</del><span class="cx">     return false;
</span><span class="cx"> }
</span><span class="cx"> 
</span></span></pre>
</div>
</div>

</body>
</html>