Native text selection UI is incorrectly suppressed in Microsoft Visio
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 5 Mar 2019 00:31:31 +0000 (00:31 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 5 Mar 2019 00:31:31 +0000 (00:31 +0000)
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.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@242401 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-empty-overflow-hidden-container.html
LayoutTests/resources/ui-helper.js
Source/WebCore/ChangeLog
Source/WebCore/rendering/RenderObject.cpp

index 3b46cd2..8e4a37d 100644 (file)
@@ -1,3 +1,33 @@
+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.
+
 2019-03-04  Daniel Bates  <dabates@apple.com>
 
         [iOS] Caret x-position in empty text area does not match text field
diff --git a/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt b/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container-expected.txt
new file mode 100644 (file)
index 0000000..807c4b7
--- /dev/null
@@ -0,0 +1,18 @@
+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
+
diff --git a/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html b/LayoutTests/editing/selection/ios/do-not-hide-selection-in-visible-container.html
new file mode 100644 (file)
index 0000000..5084ffe
--- /dev/null
@@ -0,0 +1,105 @@
+<!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>
index db3f054..3b05b56 100644 (file)
@@ -26,10 +26,9 @@ button {
 }
 </style>
 <script>
-addEventListener("load", runTest);
 jsTestIsAsync = true;
 
-async function runTest() {
+addEventListener("load", async () => {
     description("Verifies that native selection UI is suppressed when focusing a textarea that is completely hidden "
         + "underneath an empty container with <code>overflow: hidden;</code>. To manually test, tap the top button, "
         + "and then tap on the bottom button. In both cases, there should be no platform selection shown.");
@@ -64,9 +63,9 @@ async function runTest() {
 
     frame.contentWindow.document.activeElement.blur();
     await UIHelper.waitForKeyboardToHide();
-    document.querySelectorAll("button, #container").forEach(element => element.remove())
+    document.querySelectorAll("button, #container").forEach(element => element.remove());
     finishJSTest();
-}
+});
 </script>
 </head>
 <body>
@@ -76,7 +75,7 @@ async function runTest() {
 </button>
 <div id="container">
     <textarea id="editor"></textarea>
-    <iframe id="frame" srcdoc="<textarea id='editor'></textarea>" onload="runTest()"></iframe>
+    <iframe id="frame" srcdoc="<textarea id='editor'></textarea>"></iframe>
 </div>
 <div id="description"></div>
 <div id="console"></div>
index dcd4eb0..3a6180e 100644 (file)
@@ -224,6 +224,13 @@ window.UIHelper = class UIHelper {
         });
     }
 
+    static activateElementAndWaitForInputSession(element)
+    {
+        const x = element.offsetLeft + element.offsetWidth / 2;
+        const y = element.offsetTop + element.offsetHeight / 2;
+        return this.activateAndWaitForInputSessionAt(x, y);
+    }
+
     static activateFormControl(element)
     {
         if (!this.isWebKit2() || !this.isIOS())
index df501d1..47b64f7 100644 (file)
@@ -1,3 +1,27 @@
+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):
+
 2019-03-01  Ryosuke Niwa  <rniwa@webkit.org>
 
         gPictureOwnerMap is unnecessary
index 51cc458..6f35d04 100644 (file)
@@ -1527,38 +1527,40 @@ void RenderObject::destroy()
     delete this;
 }
 
+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());
+}
+
 bool RenderObject::isTransparentOrFullyClippedRespectingParentFrames() const
 {
     static const double minimumVisibleOpacity = 0.01;
 
     float currentOpacity = 1;
-    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();
+    for (auto* layer = enclosingLayer(); layer; layer = parentLayerCrossingFrameBoundaries(*layer)) {
+        currentOpacity *= layer->renderer().style().opacity();
         if (currentOpacity < minimumVisibleOpacity)
             return true;
+    }
 
-        auto* parentLayer = layer->parent();
-        if (!parentLayer) {
-            if (!is<RenderView>(layerRenderer))
-                return false;
-
-            auto& enclosingFrame = downcast<RenderView>(layerRenderer).view().frame();
-            if (enclosingFrame.isMainFrame())
-                return false;
-
-            if (auto *frameOwnerRenderer = enclosingFrame.ownerElement()->renderer())
-                parentLayer = frameOwnerRenderer->enclosingLayer();
-        }
-        layer = parentLayer;
+    for (auto* layer = enclosingLayer(); layer; layer = enclosingFrameRenderLayer(layer->renderer())) {
+        if (layer->selfClipRect().isEmpty())
+            return true;
     }
+
     return false;
 }