REGRESSION (r243250): Text interactions are no longer suppressed when editing in...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 29 Mar 2019 20:09:02 +0000 (20:09 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 29 Mar 2019 20:09:02 +0000 (20:09 +0000)
https://bugs.webkit.org/show_bug.cgi?id=196378
<rdar://problem/49231299>

Reviewed by Simon Fraser.

Source/WebCore:

Enabling async overflow scrolling by default in r243250 exposed an issue with hidden editable area detection
heuristics. Currently, an empty value for RenderLayer::selfClipRect is used to determine whether the layer
enclosing the editable element or form control is completely clipped by a parent (in other words, the clip rect
is empty). With async overflow scrolling, the enclosing layer of the editable element (as seen in the websites
affected by this bug) will now be a clipping root for painting, since it is composited. This means selfClipRect
returns a non-empty rect despite the layer being entirely clipped, which negates the heuristic.

To address this, we adjust the clipping heuristic to instead walk up the layer tree (crossing frame boundaries)
and look for enclosing ancestors with overflow clip. For each layer we find with an overflow clip, compute the
clip rect of the previous layer relative to the ancestor with overflow clip. If the clipping rect is empty, we
know that the layer is hidden.

This isn't a perfect strategy, since it may still report false negatives (reporting a layer as visible when it
is not) in some cases. One such edge case is a series of overflow hidden containers, nested in such a way that
each container is only partially clipped relative to its ancestor, but the deepest layer is completely clipped
relative to the topmost layer. However, this heuristic is relatively cheap (entailing a layer tree walk at
worst) and works for common use cases on the web without risking scenarios in which text selection that
shouldn't be suppressed ends up becoming suppressed.

Test: editing/selection/ios/hide-selection-in-textarea-with-transform.html

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

LayoutTests:

Add a new layout test to exercise the scenario in which a transformed textarea is hidden inside an empty
overflow: hidden container.

* editing/selection/ios/hide-selection-in-textarea-with-transform-expected.txt: Added.
* editing/selection/ios/hide-selection-in-textarea-with-transform.html: Added.

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

LayoutTests/ChangeLog
LayoutTests/editing/selection/ios/hide-selection-in-textarea-with-transform-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-textarea-with-transform.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/rendering/RenderLayer.cpp

index bf5f953..7043d71 100644 (file)
@@ -1,3 +1,17 @@
+2019-03-29  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        REGRESSION (r243250): Text interactions are no longer suppressed when editing in some websites
+        https://bugs.webkit.org/show_bug.cgi?id=196378
+        <rdar://problem/49231299>
+
+        Reviewed by Simon Fraser.
+
+        Add a new layout test to exercise the scenario in which a transformed textarea is hidden inside an empty
+        overflow: hidden container.
+
+        * editing/selection/ios/hide-selection-in-textarea-with-transform-expected.txt: Added.
+        * editing/selection/ios/hide-selection-in-textarea-with-transform.html: Added.
+
 2019-03-29  Alex Christensen  <achristensen@webkit.org>
 
         Unreviewed test gardening for imported/w3c/web-platform-tests/xhr/send-redirect-post-upload.htm
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-textarea-with-transform-expected.txt b/LayoutTests/editing/selection/ios/hide-selection-in-textarea-with-transform-expected.txt
new file mode 100644 (file)
index 0000000..3a639c8
--- /dev/null
@@ -0,0 +1,14 @@
+The caret should appear 
+The caret should not appear
+This test verifies that a textarea with a transform that is completely clipped by an overflow: hidden container can be treated as a hidden editable area. To run the test manually, tap the top button and verify that a caret is shown and the keyboard appears. Then, tap the bottom button and verify that the caret is hidden, but the keyboard is still shown.
+
+Tapping button: visible
+PASS document.activeElement is textarea
+PASS Caret is visible.
+Tapping button: hidden
+PASS document.activeElement is textarea
+PASS Caret is hidden.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-textarea-with-transform.html b/LayoutTests/editing/selection/ios/hide-selection-in-textarea-with-transform.html
new file mode 100644 (file)
index 0000000..c5a9305
--- /dev/null
@@ -0,0 +1,64 @@
+<!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>
+div.container {
+    position: relative;
+    height: 0;
+}
+
+textarea {
+    top: 100px;
+    position: absolute;
+    transform: translateZ(0);
+}
+</style>
+</head>
+<body>
+<div class="container">
+    <textarea></textarea>
+</div>
+<button id="visible" onclick="focusTextarea('visible')">The caret should appear</button>
+<br>
+<button id="hidden" onclick="focusTextarea('hidden')">The caret should not appear</button>
+<p id="description">This test verifies that a textarea with a transform that is completely clipped by an overflow: hidden container can be treated as a hidden editable area. To run the test manually, tap the top button and verify that a caret is shown and the keyboard appears. Then, tap the bottom button and verify that the caret is hidden, but the keyboard is still shown.</p>
+<p id="console"></p>
+<script>
+jsTestIsAsync = true;
+textarea = document.querySelector("textarea");
+
+async function expectCaretVisibility(caretShouldBeVisible)
+{
+    let rect = null;
+    while (!rect || (!caretShouldBeVisible && rect.width && rect.height) || (caretShouldBeVisible && (!rect.width || !rect.height)))
+        rect = await UIHelper.getUICaretViewRect();
+    testPassed(`Caret is ${caretShouldBeVisible ? "visible" : "hidden"}.`);
+}
+
+function focusTextarea(overflow)
+{
+    document.querySelector("div.container").style.overflow = overflow;
+    textarea.focus();
+}
+
+async function tapButtonAndWaitForKeyboard(button, caretShouldBeVisible)
+{
+    debug(`Tapping button: ${button.id}`);
+    await UIHelper.activateElementAndWaitForInputSession(button);
+    shouldBe("document.activeElement", "textarea");
+    await expectCaretVisibility(caretShouldBeVisible);
+    document.activeElement.blur();
+    await UIHelper.waitForKeyboardToHide();
+}
+
+addEventListener("load", async () => {
+    await tapButtonAndWaitForKeyboard(document.getElementById("visible"), true);
+    await tapButtonAndWaitForKeyboard(document.getElementById("hidden"), false);
+    finishJSTest();
+});
+</script>
+</body>
+</html>
index 4527f2f..f1b8466 100644 (file)
@@ -1,3 +1,35 @@
+2019-03-29  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        REGRESSION (r243250): Text interactions are no longer suppressed when editing in some websites
+        https://bugs.webkit.org/show_bug.cgi?id=196378
+        <rdar://problem/49231299>
+
+        Reviewed by Simon Fraser.
+
+        Enabling async overflow scrolling by default in r243250 exposed an issue with hidden editable area detection
+        heuristics. Currently, an empty value for RenderLayer::selfClipRect is used to determine whether the layer
+        enclosing the editable element or form control is completely clipped by a parent (in other words, the clip rect
+        is empty). With async overflow scrolling, the enclosing layer of the editable element (as seen in the websites
+        affected by this bug) will now be a clipping root for painting, since it is composited. This means selfClipRect
+        returns a non-empty rect despite the layer being entirely clipped, which negates the heuristic.
+
+        To address this, we adjust the clipping heuristic to instead walk up the layer tree (crossing frame boundaries)
+        and look for enclosing ancestors with overflow clip. For each layer we find with an overflow clip, compute the
+        clip rect of the previous layer relative to the ancestor with overflow clip. If the clipping rect is empty, we
+        know that the layer is hidden.
+
+        This isn't a perfect strategy, since it may still report false negatives (reporting a layer as visible when it
+        is not) in some cases. One such edge case is a series of overflow hidden containers, nested in such a way that
+        each container is only partially clipped relative to its ancestor, but the deepest layer is completely clipped
+        relative to the topmost layer. However, this heuristic is relatively cheap (entailing a layer tree walk at
+        worst) and works for common use cases on the web without risking scenarios in which text selection that
+        shouldn't be suppressed ends up becoming suppressed.
+
+        Test: editing/selection/ios/hide-selection-in-textarea-with-transform.html
+
+        * rendering/RenderLayer.cpp:
+        (WebCore::RenderLayer::isTransparentOrFullyClippedRespectingParentFrames const):
+
 2019-03-29  Takashi Komori  <Takashi.Komori@sony.com>
 
         [Curl] Add Server Trust Evaluation Support.
index fc519f8..bd11f90 100644 (file)
@@ -6648,8 +6648,17 @@ bool RenderLayer::isTransparentOrFullyClippedRespectingParentFrames() const
             return true;
     }
 
-    for (auto* layer = this; layer; layer = enclosingFrameRenderLayer(*layer)) {
-        if (layer->selfClipRect().isEmpty())
+    RenderLayer* enclosingClipLayer = nullptr;
+    for (auto* layer = this; layer; layer = enclosingClipLayer ? enclosingClipLayer->parent() : enclosingFrameRenderLayer(*layer)) {
+        enclosingClipLayer = layer->enclosingOverflowClipLayer(IncludeSelfOrNot::IncludeSelf);
+        if (!enclosingClipLayer)
+            continue;
+
+        LayoutRect layerBounds;
+        ClipRect backgroundRect;
+        ClipRect foregroundRect;
+        layer->calculateRects({ enclosingClipLayer, TemporaryClipRects }, LayoutRect::infiniteRect(), layerBounds, backgroundRect, foregroundRect, layer->offsetFromAncestor(enclosingClipLayer));
+        if (backgroundRect.isEmpty())
             return true;
     }