Title: [238146] trunk
Revision
238146
Author
wenson_hs...@apple.com
Date
2018-11-13 14:30:27 -0800 (Tue, 13 Nov 2018)

Log Message

[iOS] Do not show selection UI for editable elements with opacity near zero
https://bugs.webkit.org/show_bug.cgi?id=191442
<rdar://problem/45958625>

Reviewed by Simon Fraser.

Source/WebCore:

Tests: editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html
       editing/selection/ios/hide-selection-after-hiding-contenteditable.html
       editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html
       editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html
       editing/selection/ios/hide-selection-in-hidden-contenteditable.html

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

Add a helper function to determine whether a RenderObject is contained within a transparent layer, taking parent
frames into account. A layer is considered transparent if its opacity is less than a small threshold (i.e. 0.01).
Opacity on ancestor elements is applied multiplicatively.

* rendering/RenderObject.h:

Source/WebKit:

Add support for suppressing native selection UI (for instance, selection highlight views, selection handles, and
selection-related gestures) when the selection is inside a transparent editable element. This helps maintain
compatibility with text editors that work by capturing key events and input events hidden contenteditable
elements, and reflect these changes in different document or different part of the document.

Since selection UI is rendered in the UI process on iOS using element geometry propagated from the web process,
selection rendering is entirely decoupled from the process of painting in the web process. This means that if
the editable root has an opacity of 0, we would correctly hide the caret and selection on macOS, but draw over
the transparent element on iOS. When these hidden editable elements are focused, this often results in unwanted
behaviors, such as double caret painting, native and custom selection UI from the page being drawn on top of one
another, and the ability to change selection via tap and loupe gestures within hidden text.

To fix this, we compute whether the focused element is transparent when an element is focused, or when the
selection changes, and send this information over to the UI process via `AssistedNodeInformation` and
`EditorState`. In the UI process, we then respect this information by suppressing the selection assistant if the
focused element is transparent; this disables showing and laying out selection views, as well as gestures
associated with selection overlays. However, this still allows for contextual autocorrection and spell checking.

* Shared/AssistedNodeInformation.cpp:
(WebKit::AssistedNodeInformation::encode const):
(WebKit::AssistedNodeInformation::decode):
* Shared/AssistedNodeInformation.h:
* Shared/EditorState.cpp:
(WebKit::EditorState::PostLayoutData::encode const):
(WebKit::EditorState::PostLayoutData::decode):
* Shared/EditorState.h:

Add `elementIsTransparent` flags, and also add boilerplate IPC code.

* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView _displayFormNodeInputView]):

Prevent zooming to the focused element if the focused element is hidden.

(-[WKContentView hasSelectablePositionAtPoint:]):
(-[WKContentView pointIsNearMarkedText:]):
(-[WKContentView textInteractionGesture:shouldBeginAtPoint:]):

Don't allow these text interaction gestures to begin while suppressing the selection assistant.

(-[WKContentView _startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:]):

When an element is focused, begin suppressing the selection assistant if the element is fully transparent.

(-[WKContentView _stopAssistingNode]):

When the focused element is blurred, reset state by ending selection assistant suppression (additionally
reactivating the selection assistant if needed). This ensures that selection in non-editable text isn't broken
after focusing a hidden editable element.

(-[WKContentView _updateChangedSelection:]):

If needed, suppress or un-suppress the selection assistant when the selection changes. On certain rich text
editors, a combination of custom selection UI and native selection UI is used. For instance, on Microsoft Office
365, caret selections are rendered using the native caret view, but as soon as the selection becomes ranged, the
editable root becomes fully transparent, and Office's selection UI takes over.

(-[WKContentView _shouldSuppressSelectionCommands]):

Override this UIKit SPI hook to suppress selection commands (e.g. the callout bar) when suppressing the
selection assistant.

* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::platformEditorState const):
(WebKit::WebPage::getAssistedNodeInformation):

Compute and set `elementIsTransparent` using the assisted node.

Tools:

Add a couple of new testing helpers to UIScriptController.

* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
* TestRunnerShared/UIScriptContext/UIScriptController.cpp:
(WTR::UIScriptController::textSelectionRangeRects const):
(WTR::UIScriptController::selectionCaretViewRect const):
(WTR::UIScriptController::selectionRangeViewRects const):
* TestRunnerShared/UIScriptContext/UIScriptController.h:
* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::textSelectionRangeRects const):

Rename `selectionRangeViewRects` to `textSelectionRangeRects`. This allows us to draw a distinction between
`textSelectionRangeRects`/`textSelectionCaretRect`, which retrieve information about selection rects known
to the text interaction assistant, and `selectionCaretViewRect`/`selectionRangeViewRects`, which retrieve the
actual frames of the selection views used to draw overlaid selection UI. This difference is important in the
new layout tests added in this patch, which only suppress caret rendering (i.e. selection views remain hidden).

Also, drive-by fix a leaked `NSMutableArray`.

(WTR::UIScriptController::selectionStartGrabberViewRect const):
(WTR::UIScriptController::selectionEndGrabberViewRect const):
(WTR::UIScriptController::selectionCaretViewRect const):
(WTR::UIScriptController::selectionRangeViewRects const):

Testing helpers to grab the frames of caret and selection views, in WKContentView's coordinate space. These
rects are also clamped to WKContentView bounds.

LayoutTests:

Add 5 new layout tests. See below for more details.

* editing/selection/character-granularity-rect.html:

Adjust for a renamed UIScriptController function.

* editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt: Added.
* editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html: Added.

Add a test to verify that we don't zoom to fit the focused element, if the focused element is completely
transparent.

* editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt: Added.
* editing/selection/ios/hide-selection-after-hiding-contenteditable.html: Added.

Add a test to verify that selection UI is hidden after making an editable root transparent, and shown again when
the editable root becomes opaque.

* editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt: Added.
* editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html: Added.

Add a test to verify that transparency applied on an editable root via nested transparent containers causes
selection UI to be suppressed.

* editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt: Added.
* editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt: Added.
* editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html: Added.

Add a test to verify that selection UI is suppressed when an editable element inside a subframe is focused. This
test checks that the caret, selection rects and selection handle views are not shown, and additionally verifies
that the selection in a hidden contenteditable area cannot be changed via tap gesture.

* editing/selection/ios/hide-selection-in-hidden-contenteditable.html: Added.

Same test as above, but in a regular editable element in the main document instead of a subframe.

* resources/ui-helper.js:
(window.UIHelper.getUISelectionRects.return.new.Promise.):
(window.UIHelper.getUISelectionRects.return.new.Promise):
(window.UIHelper.getUISelectionRects):
(window.UIHelper.getUICaretViewRect.return.new.Promise.):
(window.UIHelper.getUICaretViewRect.return.new.Promise):
(window.UIHelper.getUICaretViewRect):

Add new UIHelper wrapper methods. See Tools/ChangeLog for more detail.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (238145 => 238146)


--- trunk/LayoutTests/ChangeLog	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/LayoutTests/ChangeLog	2018-11-13 22:30:27 UTC (rev 238146)
@@ -1,3 +1,57 @@
+2018-11-13  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [iOS] Do not show selection UI for editable elements with opacity near zero
+        https://bugs.webkit.org/show_bug.cgi?id=191442
+        <rdar://problem/45958625>
+
+        Reviewed by Simon Fraser.
+
+        Add 5 new layout tests. See below for more details.
+
+        * editing/selection/character-granularity-rect.html:
+
+        Adjust for a renamed UIScriptController function.
+
+        * editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt: Added.
+        * editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html: Added.
+
+        Add a test to verify that we don't zoom to fit the focused element, if the focused element is completely
+        transparent.
+
+        * editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt: Added.
+        * editing/selection/ios/hide-selection-after-hiding-contenteditable.html: Added.
+
+        Add a test to verify that selection UI is hidden after making an editable root transparent, and shown again when
+        the editable root becomes opaque.
+
+        * editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt: Added.
+        * editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html: Added.
+
+        Add a test to verify that transparency applied on an editable root via nested transparent containers causes
+        selection UI to be suppressed.
+
+        * editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt: Added.
+        * editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt: Added.
+        * editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html: Added.
+
+        Add a test to verify that selection UI is suppressed when an editable element inside a subframe is focused. This
+        test checks that the caret, selection rects and selection handle views are not shown, and additionally verifies
+        that the selection in a hidden contenteditable area cannot be changed via tap gesture.
+
+        * editing/selection/ios/hide-selection-in-hidden-contenteditable.html: Added.
+
+        Same test as above, but in a regular editable element in the main document instead of a subframe.
+
+        * resources/ui-helper.js:
+        (window.UIHelper.getUISelectionRects.return.new.Promise.):
+        (window.UIHelper.getUISelectionRects.return.new.Promise):
+        (window.UIHelper.getUISelectionRects):
+        (window.UIHelper.getUICaretViewRect.return.new.Promise.):
+        (window.UIHelper.getUICaretViewRect.return.new.Promise):
+        (window.UIHelper.getUICaretViewRect):
+
+        Add new UIHelper wrapper methods. See Tools/ChangeLog for more detail.
+
 2018-11-13  Matt Baker  <mattba...@apple.com>
 
         Web Inspector: Table should support select all (Cmd-A)

Modified: trunk/LayoutTests/editing/selection/character-granularity-rect.html (238145 => 238146)


--- trunk/LayoutTests/editing/selection/character-granularity-rect.html	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/LayoutTests/editing/selection/character-granularity-rect.html	2018-11-13 22:30:27 UTC (rev 238146)
@@ -22,7 +22,7 @@
         return `
         (function() {
             uiController.longPressAtPoint(30, 20, function() {
-                uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
+                uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionRangeRects));
             });
         })();`
     }
@@ -35,12 +35,12 @@
         var target = document.getElementById('target');
         if (testRunner.runUIScript) {
             testRunner.runUIScript(getUIScript(), function(result) {
-                var selectionRangeViewRects = JSON.parse(result);
+                var textSelectionRangeRects = JSON.parse(result);
                 var output;
-                if (selectionRangeViewRects.length !== 1)
+                if (textSelectionRangeRects.length !== 1)
                     output = 'FAIL: Unexpected number of selection range views: ' + result;
                 else {
-                    var rect = selectionRangeViewRects[0];
+                    var rect = textSelectionRangeRects[0];
                     if (rect.left != 8 || rect.top != 8 || rect.width != 112 || rect.height != 17 )
                         output = 'FAIL: Unexpected selection range view frame: ' + result;
                     else

Added: trunk/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,7 @@
+
+The initial page scale is: 1.000
+The page scale after focusing the div is: 1.000
+The page scale after focusing the iframe is: 1.000
+Testing
+
+Verifies that we don't zoom to the focused element, if the focused element is in a hidden contenteditable area. To manually test, click the button and check that the page scale is still 1.

Added: trunk/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,94 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<script src=""
+<style>
+body {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+input {
+    width: 320px;
+    height: 160px;
+    display: block;
+}
+
+.container {
+    width: 30px;
+    height: 30px;
+    border: solid 2px silver;
+    box-sizing: border-box;
+    font-size: 6px;
+}
+
+#editor, iframe {
+    opacity: 0.001;
+    width: inherit;
+    height: inherit;
+    overflow-y: scroll;
+}
+</style>
+</head>
+<body>
+<input type="button" _onclick_="focusEditor()" value="Click to focus the div">
+<input type="button" _onclick_="focusSubframe()" value="Click to focus the frame">
+<div>The initial page scale is: <span id="initial-scale"></span></div>
+<div>The page scale after focusing the div is: <span id="div-scale"></span></div>
+<div>The page scale after focusing the iframe is: <span id="iframe-scale"></span></div>
+<div class="container">
+    <div id="editor" contenteditable>Testing</div>
+</div>
+<div class="container">
+    <iframe srcdoc="
+        <head>
+            <style>body, html { width: 100%; height: 100%; font-size: 6px; }</style>
+            <script>
+                function beginEditing() {
+                    document.body.focus();
+                }
+            </script>
+        </head>
+        <body contenteditable>Testing</body>" _onload_="runTest()"></iframe>
+</div>
+<p>Verifies that we don't zoom to the focused element, if the focused element is in a hidden contenteditable area. To manually test, click the button and check that the page scale is still 1.</p>
+<script>
+loadCount = 0;
+
+addEventListener("load", runTest);
+
+function focusEditor() {
+    document.querySelector('#editor').focus();
+}
+
+function focusSubframe() {
+    document.querySelector("iframe").contentWindow.beginEditing();
+}
+
+async function runTest() {
+    if (!window.testRunner)
+        return;
+
+    if (++loadCount < 2)
+        return;
+
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    document.querySelector("#initial-scale").textContent = internals.pageScaleFactor().toFixed(3);
+
+    await UIHelper.activateAndWaitForInputSessionAt(160, 80);
+    document.querySelector("#div-scale").textContent = internals.pageScaleFactor().toFixed(3);
+    UIHelper.resignFirstResponder();
+    await UIHelper.waitForKeyboardToHide();
+
+    await UIHelper.activateAndWaitForInputSessionAt(160, 240);
+    document.querySelector("#iframe-scale").textContent = internals.pageScaleFactor().toFixed(3);
+
+    testRunner.notifyDone();
+}
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,6 @@
+Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes. The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+Verifies that selection UI is hidden after the editable area becomes hidden, following a selection change. This test requires WebKitTestRunner.
+
+Caret rect after focus: (left = 161, top = 151, width = 2, height = 30)
+Selection rects after select all:
+Caret rect after collapsing: (left = 269, top = 271, width = 2, height = 30)

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable.html (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable.html	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable.html	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,65 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, user-scalable=no">
+<script src=""
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+.container {
+    width: 320px;
+    height: 320px;
+    border: solid 2px silver;
+    box-sizing: border-box;
+}
+
+#editor {
+    width: inherit;
+    height: inherit;
+    font-size: 24px;
+}
+</style>
+</head>
+<body>
+<div class="container">
+    <div id="editor" contenteditable>
+        Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes.
+        The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+    </div>
+</div>
+<p>Verifies that selection UI is hidden after the editable area becomes hidden, following a selection change. This test requires WebKitTestRunner.</p>
+<div>Caret rect after focus: <span id="focus-caret"></span></div>
+<div>Selection rects after select all: <span id="selection"></span></div>
+<div>Caret rect after collapsing: <span id="collapse-caret"></span></div>
+<script>
+function rectToString(rect) {
+    return `(left = ${Math.round(rect.left)}, top = ${Math.round(rect.top)}, width = ${Math.round(rect.width)}, height = ${Math.round(rect.height)})`;
+}
+
+(async () => {
+    if (!window.testRunner)
+        return;
+
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    await UIHelper.activateAndWaitForInputSessionAt(160, 160);
+    document.querySelector("#focus-caret").textContent = rectToString(await UIHelper.getUICaretViewRect());
+
+    editor.style.opacity = 0;
+    document.execCommand("selectAll");
+    document.querySelector("#selection").textContent = (await UIHelper.getUISelectionViewRects()).map(rectToString).join(", ");
+
+    editor.style.opacity = 1;
+    getSelection().collapseToEnd();
+    document.querySelector("#collapse-caret").textContent = rectToString(await UIHelper.getUICaretViewRect());
+
+    testRunner.notifyDone();
+})();
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,12 @@
+Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes. The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+Verifies that selection UI is suppressed when the editable root is transparent as a result of nested transparent containers. To manually test, focus the box above and verify that:
+
+The caret is not shown.
+Selection highlights are not shown.
+The selection cannot be changed via gesture.
+Caret rect after focus: (left = 0, top = 0, width = 0, height = 0)
+Selection rects after selecting all:
+Selection start grabber rect after selecting all: (left = 0, top = 0, width = 0, height = 0)
+Selection end grabber rect after selecting all: (left = 0, top = 0, width = 0, height = 0)
+Selection before tap: ([object Text]#357, [object Text]#357)
+Selection after tap: ([object Text]#357, [object Text]#357)

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,96 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, user-scalable=no">
+<script src=""
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+.container {
+    width: 320px;
+    height: 320px;
+    border: solid 2px silver;
+    box-sizing: border-box;
+}
+
+#editor {
+    width: inherit;
+    height: inherit;
+    font-size: 24px;
+}
+
+.transparent {
+    opacity: 0.25;
+}
+</style>
+</head>
+<body>
+<div class="container">
+    <div class="transparent">
+        <div class="transparent">
+            <div class="transparent">
+                <div class="transparent">
+                    <div id="editor" contenteditable>
+                        Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes.
+                        The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<p>Verifies that selection UI is suppressed when the editable root is transparent as a result of nested transparent containers. To manually test, focus the box above and verify that:</p>
+<ul>
+    <li>The caret is not shown.</li>
+    <li>Selection highlights are not shown.</li>
+    <li>The selection cannot be changed via gesture.</li>
+</ul>
+<div>Caret rect after focus: <span id="caret-rect"></span></div>
+<div>Selection rects after selecting all: <span id="selection-rects"></span></div>
+<div>Selection start grabber rect after selecting all: <span id="start-grabber-rect"></span></div>
+<div>Selection end grabber rect after selecting all: <span id="end-grabber-rect"></span></div>
+<div>Selection before tap: <span id="selection-before"></span></div>
+<div>Selection after tap: <span id="selection-after"></span></div>
+<script>
+function rectToString(rect) {
+    return `(left = ${Math.round(rect.left)}, top = ${Math.round(rect.top)}, width = ${Math.round(rect.width)}, height = ${Math.round(rect.height)})`;
+}
+
+function selectionToString() {
+    const selection = getSelection();
+    if (!selection.rangeCount)
+        return "(no selection)";
+
+    const range = selection.getRangeAt(0);
+    return `(${range.startContainer}#${range.startOffset}, ${range.endContainer}#${range.endOffset})`;
+}
+
+(async () => {
+    if (!window.testRunner)
+        return;
+
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    await UIHelper.activateAndWaitForInputSessionAt(160, 160);
+    document.querySelector("#caret-rect").textContent = rectToString(await UIHelper.getUICaretViewRect());
+
+    document.execCommand("selectAll");
+    document.querySelector("#selection-rects").textContent = (await UIHelper.getUISelectionViewRects()).map(rectToString).join(", ");
+    document.querySelector("#start-grabber-rect").textContent = rectToString(await UIHelper.getSelectionStartGrabberViewRect());
+    document.querySelector("#end-grabber-rect").textContent = rectToString(await UIHelper.getSelectionEndGrabberViewRect());
+
+    getSelection().collapseToEnd();
+    document.querySelector("#selection-before").textContent = selectionToString();
+    await UIHelper.tapAt(32, 32);
+    document.querySelector("#selection-after").textContent = selectionToString();
+
+    testRunner.notifyDone();
+})();
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,12 @@
+Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes. The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+Verifies that selection UI is suppressed when the editable root is transparent. To manually test, focus the box above and verify that:
+
+The caret is not shown.
+Selection highlights are not shown.
+The selection cannot be changed via gesture.
+Caret rect after focus: (left = 0, top = 0, width = 0, height = 0)
+Selection rects after selecting all:
+Selection start grabber rect after selecting all: (left = 0, top = 0, width = 0, height = 0)
+Selection end grabber rect after selecting all: (left = 0, top = 0, width = 0, height = 0)
+Selection before tap: ([object Text]#325, [object Text]#325)
+Selection after tap: ([object Text]#325, [object Text]#325)

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,12 @@
+
+Verifies that selection UI is suppressed when the editable root is transparent. To manually test, focus the box above and verify that:
+
+The caret is not shown.
+Selection highlights are not shown.
+The selection cannot be changed via gesture.
+Caret rect after focus: (left = 0, top = 0, width = 0, height = 0)
+Selection rects after selecting all:
+Selection start grabber rect after selecting all: (left = 0, top = 0, width = 0, height = 0)
+Selection end grabber rect after selecting all: (left = 0, top = 0, width = 0, height = 0)
+Selection before tap: ([object Text]#333, [object Text]#333)
+Selection after tap: ([object Text]#333, [object Text]#333)

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,122 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, user-scalable=no">
+<script src=""
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+.container {
+    width: 320px;
+    height: 320px;
+    border: solid 2px silver;
+    box-sizing: border-box;
+}
+
+iframe {
+    opacity: 0.001;
+    width: inherit;
+    height: inherit;
+    font-size: 24px;
+}
+</style>
+</head>
+<body>
+<div class="container">
+    <iframe srcdoc="
+        <head>
+            <style>body, html { width: 100%; height: 100%; }</style>
+            <script>
+            function selectionToString() {
+                const selection = getSelection();
+                if (!selection.rangeCount)
+                    return '(no selection)';
+
+                const range = selection.getRangeAt(0);
+                return `(${range.startContainer}#${range.startOffset}, ${range.endContainer}#${range.endOffset})`;
+            }
+
+            function selectAll() {
+                document.execCommand('selectAll');
+            }
+
+            function collapseToEnd() {
+                getSelection().collapseToEnd();
+            }
+
+            document.addEventListener('selectionchange', () => parent.postMessage(selectionToString(), '*'));
+            </script>
+        </head>
+        <body contenteditable>
+            Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes.
+            The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+        </body>" _onload_="runTest()"></iframe>
+</div>
+<p>Verifies that selection UI is suppressed when the editable root is transparent. To manually test, focus the box above and verify that:</p>
+<ul>
+    <li>The caret is not shown.</li>
+    <li>Selection highlights are not shown.</li>
+    <li>The selection cannot be changed via gesture.</li>
+</ul>
+<div>Caret rect after focus: <span id="caret-rect"></span></div>
+<div>Selection rects after selecting all: <span id="selection-rects"></span></div>
+<div>Selection start grabber rect after selecting all: <span id="start-grabber-rect"></span></div>
+<div>Selection end grabber rect after selecting all: <span id="end-grabber-rect"></span></div>
+<div>Selection before tap: <span id="selection-before"></span></div>
+<div>Selection after tap: <span id="selection-after"></span></div>
+<script>
+currentSelectionAsString = "";
+currentSelectionChangeCount = 0;
+loadCount = 0;
+
+addEventListener("load", runTest);
+addEventListener("message", event => {
+    window.currentSelectionAsString = event.data;
+    currentSelectionChangeCount++;
+});
+
+function rectToString(rect) {
+    return `(left = ${Math.round(rect.left)}, top = ${Math.round(rect.top)}, width = ${Math.round(rect.width)}, height = ${Math.round(rect.height)})`;
+}
+
+async function waitForSelectionChangeInSubframe(actions)
+{
+    const previousSelectionChangeCount = currentSelectionChangeCount;
+    actions();
+    while (previousSelectionChangeCount == currentSelectionChangeCount)
+        await new Promise(requestAnimationFrame);
+}
+
+async function runTest() {
+    if (++loadCount < 2)
+        return;
+
+    if (!window.testRunner)
+        return;
+
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    await waitForSelectionChangeInSubframe(async () => await UIHelper.activateAndWaitForInputSessionAt(160, 160));
+    document.querySelector("#caret-rect").textContent = rectToString(await UIHelper.getUICaretViewRect());
+
+    await waitForSelectionChangeInSubframe(() => document.querySelector("iframe").contentWindow.selectAll());
+    document.querySelector("#start-grabber-rect").textContent = rectToString(await UIHelper.getSelectionStartGrabberViewRect());
+    document.querySelector("#end-grabber-rect").textContent = rectToString(await UIHelper.getSelectionEndGrabberViewRect());
+
+    await waitForSelectionChangeInSubframe(() => document.querySelector("iframe").contentWindow.collapseToEnd());
+    document.querySelector("#selection-before").textContent = currentSelectionAsString;
+
+    await UIHelper.tapAt(32, 32);
+    await new Promise(requestAnimationFrame);
+    document.querySelector("#selection-after").textContent = currentSelectionAsString;
+
+    testRunner.notifyDone();
+}
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable.html (0 => 238146)


--- trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable.html	                        (rev 0)
+++ trunk/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable.html	2018-11-13 22:30:27 UTC (rev 238146)
@@ -0,0 +1,85 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, user-scalable=no">
+<script src=""
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+.container {
+    width: 320px;
+    height: 320px;
+    border: solid 2px silver;
+    box-sizing: border-box;
+}
+
+#editor {
+    opacity: 0.001;
+    width: inherit;
+    height: inherit;
+    font-size: 24px;
+}
+</style>
+</head>
+<body>
+<div class="container">
+    <div id="editor" contenteditable>
+        Here's to the crazy ones, the misfits, the rebels, the troublemakers, the round pegs in the square holes.
+        The ones who see things differently. They're not fond of rules. You can quote them, disagree with them, glorify or vilify them, but the only thing you can't do is ignore them because they change things.
+    </div>
+</div>
+<p>Verifies that selection UI is suppressed when the editable root is transparent. To manually test, focus the box above and verify that:</p>
+<ul>
+    <li>The caret is not shown.</li>
+    <li>Selection highlights are not shown.</li>
+    <li>The selection cannot be changed via gesture.</li>
+</ul>
+<div>Caret rect after focus: <span id="caret-rect"></span></div>
+<div>Selection rects after selecting all: <span id="selection-rects"></span></div>
+<div>Selection start grabber rect after selecting all: <span id="start-grabber-rect"></span></div>
+<div>Selection end grabber rect after selecting all: <span id="end-grabber-rect"></span></div>
+<div>Selection before tap: <span id="selection-before"></span></div>
+<div>Selection after tap: <span id="selection-after"></span></div>
+<script>
+function rectToString(rect) {
+    return `(left = ${Math.round(rect.left)}, top = ${Math.round(rect.top)}, width = ${Math.round(rect.width)}, height = ${Math.round(rect.height)})`;
+}
+
+function selectionToString() {
+    const selection = getSelection();
+    if (!selection.rangeCount)
+        return "(no selection)";
+
+    const range = selection.getRangeAt(0);
+    return `(${range.startContainer}#${range.startOffset}, ${range.endContainer}#${range.endOffset})`;
+}
+
+(async () => {
+    if (!window.testRunner)
+        return;
+
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    await UIHelper.activateAndWaitForInputSessionAt(160, 160);
+    document.querySelector("#caret-rect").textContent = rectToString(await UIHelper.getUICaretViewRect());
+
+    document.execCommand("selectAll");
+    document.querySelector("#selection-rects").textContent = (await UIHelper.getUISelectionViewRects()).map(rectToString).join(", ");
+    document.querySelector("#start-grabber-rect").textContent = rectToString(await UIHelper.getSelectionStartGrabberViewRect());
+    document.querySelector("#end-grabber-rect").textContent = rectToString(await UIHelper.getSelectionEndGrabberViewRect());
+
+    getSelection().collapseToEnd();
+    document.querySelector("#selection-before").textContent = selectionToString();
+    await UIHelper.tapAt(32, 32);
+    document.querySelector("#selection-after").textContent = selectionToString();
+
+    testRunner.notifyDone();
+})();
+</script>
+</body>
+</html>

Modified: trunk/LayoutTests/resources/ui-helper.js (238145 => 238146)


--- trunk/LayoutTests/resources/ui-helper.js	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/LayoutTests/resources/ui-helper.js	2018-11-13 22:30:27 UTC (rev 238146)
@@ -170,6 +170,38 @@
         return new Promise(resolve => {
             testRunner.runUIScript(`(function() {
                 uiController.doAfterNextStablePresentationUpdate(function() {
+                    uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionRangeRects));
+                });
+            })()`, jsonString => {
+                resolve(JSON.parse(jsonString));
+            });
+        });
+    }
+
+    static getUICaretViewRect()
+    {
+        if (!this.isWebKit2() || !this.isIOS())
+            return Promise.resolve();
+
+        return new Promise(resolve => {
+            testRunner.runUIScript(`(function() {
+                uiController.doAfterNextStablePresentationUpdate(function() {
+                    uiController.uiScriptComplete(JSON.stringify(uiController.selectionCaretViewRect));
+                });
+            })()`, jsonString => {
+                resolve(JSON.parse(jsonString));
+            });
+        });
+    }
+
+    static getUISelectionViewRects()
+    {
+        if (!this.isWebKit2() || !this.isIOS())
+            return Promise.resolve();
+
+        return new Promise(resolve => {
+            testRunner.runUIScript(`(function() {
+                uiController.doAfterNextStablePresentationUpdate(function() {
                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
                 });
             })()`, jsonString => {

Modified: trunk/Source/WebCore/ChangeLog (238145 => 238146)


--- trunk/Source/WebCore/ChangeLog	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebCore/ChangeLog	2018-11-13 22:30:27 UTC (rev 238146)
@@ -1,3 +1,26 @@
+2018-11-13  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [iOS] Do not show selection UI for editable elements with opacity near zero
+        https://bugs.webkit.org/show_bug.cgi?id=191442
+        <rdar://problem/45958625>
+
+        Reviewed by Simon Fraser.
+
+        Tests: editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html
+               editing/selection/ios/hide-selection-after-hiding-contenteditable.html
+               editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html
+               editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html
+               editing/selection/ios/hide-selection-in-hidden-contenteditable.html
+
+        * rendering/RenderObject.cpp:
+        (WebCore::RenderObject::isTransparentRespectingParentFrames const):
+
+        Add a helper function to determine whether a RenderObject is contained within a transparent layer, taking parent
+        frames into account. A layer is considered transparent if its opacity is less than a small threshold (i.e. 0.01).
+        Opacity on ancestor elements is applied multiplicatively.
+
+        * rendering/RenderObject.h:
+
 2018-11-13  Eric Carlson  <eric.carl...@apple.com>
 
         [MediaStream] Observer AVCaptureDevice "suspended" property

Modified: trunk/Source/WebCore/rendering/RenderObject.cpp (238145 => 238146)


--- trunk/Source/WebCore/rendering/RenderObject.cpp	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebCore/rendering/RenderObject.cpp	2018-11-13 22:30:27 UTC (rev 238146)
@@ -1514,6 +1514,35 @@
     delete this;
 }
 
+bool RenderObject::isTransparentRespectingParentFrames() const
+{
+    static const double minimumVisibleOpacity = 0.01;
+
+    float currentOpacity = 1;
+    auto* layer = enclosingLayer();
+    while (layer) {
+        auto& layerRenderer = layer->renderer();
+        currentOpacity *= layerRenderer.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;
+    }
+    return false;
+}
+
 Position RenderObject::positionForPoint(const LayoutPoint& point)
 {
     // FIXME: This should just create a Position object instead (webkit.org/b/168566). 

Modified: trunk/Source/WebCore/rendering/RenderObject.h (238145 => 238146)


--- trunk/Source/WebCore/rendering/RenderObject.h	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebCore/rendering/RenderObject.h	2018-11-13 22:30:27 UTC (rev 238146)
@@ -797,6 +797,8 @@
     void initializeFragmentedFlowStateOnInsertion();
     virtual void insertedIntoTree();
 
+    WEBCORE_EXPORT bool isTransparentRespectingParentFrames() const;
+
 protected:
     //////////////////////////////////////////
     // Helper functions. Dangerous to use!

Modified: trunk/Source/WebKit/ChangeLog (238145 => 238146)


--- trunk/Source/WebKit/ChangeLog	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/ChangeLog	2018-11-13 22:30:27 UTC (rev 238146)
@@ -1,3 +1,79 @@
+2018-11-13  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [iOS] Do not show selection UI for editable elements with opacity near zero
+        https://bugs.webkit.org/show_bug.cgi?id=191442
+        <rdar://problem/45958625>
+
+        Reviewed by Simon Fraser.
+
+        Add support for suppressing native selection UI (for instance, selection highlight views, selection handles, and
+        selection-related gestures) when the selection is inside a transparent editable element. This helps maintain
+        compatibility with text editors that work by capturing key events and input events hidden contenteditable
+        elements, and reflect these changes in different document or different part of the document.
+
+        Since selection UI is rendered in the UI process on iOS using element geometry propagated from the web process,
+        selection rendering is entirely decoupled from the process of painting in the web process. This means that if
+        the editable root has an opacity of 0, we would correctly hide the caret and selection on macOS, but draw over
+        the transparent element on iOS. When these hidden editable elements are focused, this often results in unwanted
+        behaviors, such as double caret painting, native and custom selection UI from the page being drawn on top of one
+        another, and the ability to change selection via tap and loupe gestures within hidden text.
+
+        To fix this, we compute whether the focused element is transparent when an element is focused, or when the
+        selection changes, and send this information over to the UI process via `AssistedNodeInformation` and
+        `EditorState`. In the UI process, we then respect this information by suppressing the selection assistant if the
+        focused element is transparent; this disables showing and laying out selection views, as well as gestures
+        associated with selection overlays. However, this still allows for contextual autocorrection and spell checking.
+
+        * Shared/AssistedNodeInformation.cpp:
+        (WebKit::AssistedNodeInformation::encode const):
+        (WebKit::AssistedNodeInformation::decode):
+        * Shared/AssistedNodeInformation.h:
+        * Shared/EditorState.cpp:
+        (WebKit::EditorState::PostLayoutData::encode const):
+        (WebKit::EditorState::PostLayoutData::decode):
+        * Shared/EditorState.h:
+
+        Add `elementIsTransparent` flags, and also add boilerplate IPC code.
+
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView _displayFormNodeInputView]):
+
+        Prevent zooming to the focused element if the focused element is hidden.
+
+        (-[WKContentView hasSelectablePositionAtPoint:]):
+        (-[WKContentView pointIsNearMarkedText:]):
+        (-[WKContentView textInteractionGesture:shouldBeginAtPoint:]):
+
+        Don't allow these text interaction gestures to begin while suppressing the selection assistant.
+
+        (-[WKContentView _startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:]):
+
+        When an element is focused, begin suppressing the selection assistant if the element is fully transparent.
+
+        (-[WKContentView _stopAssistingNode]):
+
+        When the focused element is blurred, reset state by ending selection assistant suppression (additionally
+        reactivating the selection assistant if needed). This ensures that selection in non-editable text isn't broken
+        after focusing a hidden editable element.
+
+        (-[WKContentView _updateChangedSelection:]):
+
+        If needed, suppress or un-suppress the selection assistant when the selection changes. On certain rich text
+        editors, a combination of custom selection UI and native selection UI is used. For instance, on Microsoft Office
+        365, caret selections are rendered using the native caret view, but as soon as the selection becomes ranged, the
+        editable root becomes fully transparent, and Office's selection UI takes over.
+
+        (-[WKContentView _shouldSuppressSelectionCommands]):
+
+        Override this UIKit SPI hook to suppress selection commands (e.g. the callout bar) when suppressing the
+        selection assistant.
+
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::platformEditorState const):
+        (WebKit::WebPage::getAssistedNodeInformation):
+
+        Compute and set `elementIsTransparent` using the assisted node.
+
 2018-11-13  Ryan Haddad  <ryanhad...@apple.com>
 
         Unreviewed, rolling out r238137.

Modified: trunk/Source/WebKit/Shared/AssistedNodeInformation.cpp (238145 => 238146)


--- trunk/Source/WebKit/Shared/AssistedNodeInformation.cpp	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/Shared/AssistedNodeInformation.cpp	2018-11-13 22:30:27 UTC (rev 238146)
@@ -91,6 +91,7 @@
     encoder << title;
     encoder << acceptsAutofilledLoginCredentials;
     encoder << isAutofillableUsernameField;
+    encoder << elementIsTransparent;
     encoder << representingPageURL;
     encoder.encodeEnum(autofillFieldName);
     encoder << placeholder;
@@ -191,6 +192,9 @@
     if (!decoder.decode(result.isAutofillableUsernameField))
         return false;
 
+    if (!decoder.decode(result.elementIsTransparent))
+        return false;
+
     if (!decoder.decode(result.representingPageURL))
         return false;
 

Modified: trunk/Source/WebKit/Shared/AssistedNodeInformation.h (238145 => 238146)


--- trunk/Source/WebKit/Shared/AssistedNodeInformation.h	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/Shared/AssistedNodeInformation.h	2018-11-13 22:30:27 UTC (rev 238146)
@@ -120,6 +120,7 @@
     String title;
     bool acceptsAutofilledLoginCredentials { false };
     bool isAutofillableUsernameField { false };
+    bool elementIsTransparent { false };
     WebCore::URL representingPageURL;
     WebCore::AutofillFieldName autofillFieldName { WebCore::AutofillFieldName::None };
     String placeholder;

Modified: trunk/Source/WebKit/Shared/EditorState.cpp (238145 => 238146)


--- trunk/Source/WebKit/Shared/EditorState.cpp	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/Shared/EditorState.cpp	2018-11-13 22:30:27 UTC (rev 238146)
@@ -131,6 +131,7 @@
     encoder << isStableStateUpdate;
     encoder << insideFixedPosition;
     encoder << hasPlainText;
+    encoder << elementIsTransparent;
     encoder << caretColor;
 #endif
 #if PLATFORM(MAC)
@@ -187,6 +188,8 @@
         return false;
     if (!decoder.decode(result.hasPlainText))
         return false;
+    if (!decoder.decode(result.elementIsTransparent))
+        return false;
     if (!decoder.decode(result.caretColor))
         return false;
 #endif

Modified: trunk/Source/WebKit/Shared/EditorState.h (238145 => 238146)


--- trunk/Source/WebKit/Shared/EditorState.h	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/Shared/EditorState.h	2018-11-13 22:30:27 UTC (rev 238146)
@@ -107,6 +107,7 @@
         bool isStableStateUpdate { false };
         bool insideFixedPosition { false };
         bool hasPlainText { false };
+        bool elementIsTransparent { false };
         WebCore::Color caretColor;
 #endif
 #if PLATFORM(MAC)

Modified: trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm (238145 => 238146)


--- trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm	2018-11-13 22:30:27 UTC (rev 238146)
@@ -1342,16 +1342,18 @@
 
 - (void)_displayFormNodeInputView
 {
-    // In case user scaling is force enabled, do not use that scaling when zooming in with an input field.
-    // Zooming above the page's default scale factor should only happen when the user performs it.
-    [self _zoomToFocusRect:_assistedNodeInformation.elementRect
-        selectionRect:_didAccessoryTabInitiateFocus ? IntRect() : _assistedNodeInformation.selectionRect
-        insideFixed:_assistedNodeInformation.insideFixedPosition
-        fontSize:_assistedNodeInformation.nodeFontSize
-        minimumScale:_assistedNodeInformation.minimumScaleFactor
-        maximumScale:_assistedNodeInformation.maximumScaleFactorIgnoringAlwaysScalable
-        allowScaling:_assistedNodeInformation.allowsUserScalingIgnoringAlwaysScalable && !currentUserInterfaceIdiomIsPad()
-        forceScroll:(_assistedNodeInformation.inputMode == InputMode::None) ? !currentUserInterfaceIdiomIsPad() : [self requiresAccessoryView]];
+    if (!self.suppressAssistantSelectionView) {
+        // In case user scaling is force enabled, do not use that scaling when zooming in with an input field.
+        // Zooming above the page's default scale factor should only happen when the user performs it.
+        [self _zoomToFocusRect:_assistedNodeInformation.elementRect
+            selectionRect:_didAccessoryTabInitiateFocus ? IntRect() : _assistedNodeInformation.selectionRect
+            insideFixed:_assistedNodeInformation.insideFixedPosition
+            fontSize:_assistedNodeInformation.nodeFontSize
+            minimumScale:_assistedNodeInformation.minimumScaleFactor
+            maximumScale:_assistedNodeInformation.maximumScaleFactorIgnoringAlwaysScalable
+            allowScaling:_assistedNodeInformation.allowsUserScalingIgnoringAlwaysScalable && !currentUserInterfaceIdiomIsPad()
+            forceScroll:(_assistedNodeInformation.inputMode == InputMode::None) ? !currentUserInterfaceIdiomIsPad() : [self requiresAccessoryView]];
+    }
 
     _didAccessoryTabInitiateFocus = NO;
     [self _ensureFormAccessoryView];
@@ -1746,6 +1748,9 @@
     if (!_webView.configuration._textInteractionGesturesEnabled)
         return NO;
 
+    if (self.suppressAssistantSelectionView)
+        return NO;
+
     if (_inspectorNodeSearchEnabled)
         return NO;
 
@@ -1769,6 +1774,9 @@
     if (!_webView.configuration._textInteractionGesturesEnabled)
         return NO;
 
+    if (self.suppressAssistantSelectionView)
+        return NO;
+
     InteractionInformationRequest request(roundedIntPoint(point));
     if (![self ensurePositionInformationIsUpToDate:request])
         return NO;
@@ -1780,6 +1788,9 @@
     if (!_webView.configuration._textInteractionGesturesEnabled)
         return NO;
 
+    if (self.suppressAssistantSelectionView)
+        return NO;
+
     InteractionInformationRequest request(roundedIntPoint(point));
     if (![self ensurePositionInformationIsUpToDate:request])
         return NO;
@@ -4267,6 +4278,8 @@
     if ([inputDelegate respondsToSelector:@selector(_webView:decidePolicyForFocusedElement:)])
         startInputSessionPolicy = [inputDelegate _webView:_webView decidePolicyForFocusedElement:focusedElementInfo.get()];
 
+    self.suppressAssistantSelectionView = information.elementIsTransparent;
+
     switch (startInputSessionPolicy) {
     case _WKFocusStartsInputSessionPolicyAuto:
         // The default behavior is to allow node assistance if the user is interacting.
@@ -4409,6 +4422,8 @@
         [_webView _scheduleVisibleContentRectUpdate];
 
     [_webView didEndFormControlInteraction];
+
+    self.suppressAssistantSelectionView = NO;
 }
 
 - (void)updateCurrentAssistedNodeInformation:(Function<void(bool didUpdate)>&&)callback
@@ -4741,9 +4756,14 @@
 
 - (void)_updateChangedSelection:(BOOL)force
 {
-    if (!_selectionNeedsUpdate || _page->editorState().isMissingPostLayoutData)
+    auto& state = _page->editorState();
+    if (state.isMissingPostLayoutData)
         return;
 
+    auto& postLayoutData = state.postLayoutData();
+    if (hasAssistedNode(_assistedNodeInformation))
+        self.suppressAssistantSelectionView = postLayoutData.elementIsTransparent;
+
     WKSelectionDrawingInfo selectionDrawingInfo(_page->editorState());
     if (force || selectionDrawingInfo != _lastSelectionDrawingInfo) {
         LOG_WITH_STREAM(Selection, stream << "_updateChangedSelection " << selectionDrawingInfo);
@@ -4764,8 +4784,7 @@
         }
     }
 
-    auto& state = _page->editorState();
-    if (!state.isMissingPostLayoutData && state.postLayoutData().isStableStateUpdate && _needsDeferredEndScrollingSelectionUpdate && _page->inStableState()) {
+    if (postLayoutData.isStableStateUpdate && _needsDeferredEndScrollingSelectionUpdate && _page->inStableState()) {
         [[self selectionInteractionAssistant] showSelectionCommands];
 
         if (!self.suppressAssistantSelectionView)
@@ -4777,6 +4796,11 @@
     }
 }
 
+- (BOOL)_shouldSuppressSelectionCommands
+{
+    return _suppressAssistantSelectionView;
+}
+
 - (BOOL)suppressAssistantSelectionView
 {
     return _suppressAssistantSelectionView;

Modified: trunk/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm (238145 => 238146)


--- trunk/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm	2018-11-13 22:30:27 UTC (rev 238146)
@@ -240,6 +240,7 @@
         if (m_assistedNode && m_assistedNode->renderer()) {
             postLayoutData.selectionClipRect = view->contentsToRootView(m_assistedNode->renderer()->absoluteBoundingBoxRect());
             postLayoutData.caretColor = m_assistedNode->renderer()->style().caretColor();
+            postLayoutData.elementIsTransparent = m_assistedNode->renderer()->isTransparentRespectingParentFrames();
         }
         computeEditableRootHasContentAndPlainText(selection, postLayoutData);
     }
@@ -2337,6 +2338,7 @@
         Frame& elementFrame = m_page->focusController().focusedOrMainFrame();
         information.elementRect = elementRectInRootViewCoordinates(*m_assistedNode, elementFrame);
         information.nodeFontSize = renderer->style().fontDescription().computedSize();
+        information.elementIsTransparent = renderer->isTransparentRespectingParentFrames();
 
         bool inFixed = false;
         renderer->localToContainerPoint(FloatPoint(), nullptr, UseTransforms, &inFixed);

Modified: trunk/Tools/ChangeLog (238145 => 238146)


--- trunk/Tools/ChangeLog	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Tools/ChangeLog	2018-11-13 22:30:27 UTC (rev 238146)
@@ -1,3 +1,38 @@
+2018-11-13  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [iOS] Do not show selection UI for editable elements with opacity near zero
+        https://bugs.webkit.org/show_bug.cgi?id=191442
+        <rdar://problem/45958625>
+
+        Reviewed by Simon Fraser.
+
+        Add a couple of new testing helpers to UIScriptController.
+
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+        * TestRunnerShared/UIScriptContext/UIScriptController.cpp:
+        (WTR::UIScriptController::textSelectionRangeRects const):
+        (WTR::UIScriptController::selectionCaretViewRect const):
+        (WTR::UIScriptController::selectionRangeViewRects const):
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::textSelectionRangeRects const):
+
+        Rename `selectionRangeViewRects` to `textSelectionRangeRects`. This allows us to draw a distinction between
+        `textSelectionRangeRects`/`textSelectionCaretRect`, which retrieve information about selection rects known
+        to the text interaction assistant, and `selectionCaretViewRect`/`selectionRangeViewRects`, which retrieve the
+        actual frames of the selection views used to draw overlaid selection UI. This difference is important in the
+        new layout tests added in this patch, which only suppress caret rendering (i.e. selection views remain hidden).
+
+        Also, drive-by fix a leaked `NSMutableArray`.
+
+        (WTR::UIScriptController::selectionStartGrabberViewRect const):
+        (WTR::UIScriptController::selectionEndGrabberViewRect const):
+        (WTR::UIScriptController::selectionCaretViewRect const):
+        (WTR::UIScriptController::selectionRangeViewRects const):
+
+        Testing helpers to grab the frames of caret and selection views, in WKContentView's coordinate space. These
+        rects are also clamped to WKContentView bounds.
+
 2018-11-13  Daniel Bates  <daba...@apple.com>
 
         Consolidate WebKit UIKitSPI.h and UIKitTestSPI.h

Modified: trunk/Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm (238145 => 238146)


--- trunk/Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm	2018-11-13 22:30:27 UTC (rev 238146)
@@ -309,7 +309,7 @@
 {
 }
 
-JSObjectRef UIScriptController::selectionRangeViewRects() const
+JSObjectRef UIScriptController::textSelectionRangeRects() const
 {
     return nullptr;
 }
@@ -389,6 +389,16 @@
     return nullptr;
 }
 
+JSObjectRef UIScriptController::selectionCaretViewRect() const
+{
+    return nullptr;
+}
+
+JSObjectRef UIScriptController::selectionRangeViewRects() const
+{
+    return nullptr;
+}
+
 bool UIScriptController::isShowingDataListSuggestions() const
 {
     return false;

Modified: trunk/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl (238145 => 238146)


--- trunk/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl	2018-11-13 22:30:27 UTC (rev 238146)
@@ -238,10 +238,12 @@
 
     readonly attribute object contentVisibleRect; // Returned object has 'left', 'top', 'width', 'height' properties.
 
-    readonly attribute object selectionRangeViewRects; // An array of objects with 'left', 'top', 'width', and 'height' properties.
+    readonly attribute object textSelectionRangeRects; // An array of objects with 'left', 'top', 'width', and 'height' properties.
     readonly attribute object textSelectionCaretRect; // An object with 'left', 'top', 'width', 'height' properties.
     readonly attribute object selectionStartGrabberViewRect;
     readonly attribute object selectionEndGrabberViewRect;
+    readonly attribute object selectionCaretViewRect;
+    readonly attribute object selectionRangeViewRects;
     readonly attribute object calendarType;
     void setDefaultCalendarType(DOMString calendarIdentifier);
     readonly attribute object inputViewBounds;

Modified: trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp (238145 => 238146)


--- trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp	2018-11-13 22:30:27 UTC (rev 238146)
@@ -389,7 +389,7 @@
     return nullptr;
 }
 
-JSObjectRef UIScriptController::selectionRangeViewRects() const
+JSObjectRef UIScriptController::textSelectionRangeRects() const
 {
     return nullptr;
 }
@@ -404,6 +404,16 @@
     return nullptr;
 }
 
+JSObjectRef UIScriptController::selectionCaretViewRect() const
+{
+    return nullptr;
+}
+
+JSObjectRef UIScriptController::selectionRangeViewRects() const
+{
+    return nullptr;
+}
+
 JSObjectRef UIScriptController::selectionEndGrabberViewRect() const
 {
     return nullptr;

Modified: trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h (238145 => 238146)


--- trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h	2018-11-13 22:30:27 UTC (rev 238146)
@@ -159,10 +159,12 @@
 
     JSObjectRef contentVisibleRect() const;
     
-    JSObjectRef selectionRangeViewRects() const;
+    JSObjectRef textSelectionRangeRects() const;
     JSObjectRef textSelectionCaretRect() const;
     JSObjectRef selectionStartGrabberViewRect() const;
     JSObjectRef selectionEndGrabberViewRect() const;
+    JSObjectRef selectionCaretViewRect() const;
+    JSObjectRef selectionRangeViewRects() const;
     JSObjectRef calendarType() const;
     void setDefaultCalendarType(JSStringRef calendarIdentifier);
     JSObjectRef inputViewBounds() const;

Modified: trunk/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm (238145 => 238146)


--- trunk/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm	2018-11-13 22:21:01 UTC (rev 238145)
+++ trunk/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm	2018-11-13 22:30:27 UTC (rev 238146)
@@ -507,14 +507,14 @@
     return m_context->objectFromRect(rect);
 }
 
-JSObjectRef UIScriptController::selectionRangeViewRects() const
+JSObjectRef UIScriptController::textSelectionRangeRects() const
 {
-    NSMutableArray *selectionRects = [[NSMutableArray alloc] init];
+    auto selectionRects = adoptNS([[NSMutableArray alloc] init]);
     NSArray *rects = TestController::singleton().mainWebView()->platformView()._uiTextSelectionRects;
     for (NSValue *rect in rects)
-        [selectionRects addObject:toNSDictionary([rect CGRectValue])];
+        [selectionRects addObject:toNSDictionary(rect.CGRectValue)];
 
-    return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:selectionRects inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
+    return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:selectionRects.get() inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
 }
 
 JSObjectRef UIScriptController::textSelectionCaretRect() const
@@ -528,6 +528,7 @@
     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
     UIView *selectionRangeView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView"];
     auto frameInContentCoordinates = [selectionRangeView convertRect:[[selectionRangeView valueForKeyPath:@"startGrabber"] frame] toView:contentView];
+    frameInContentCoordinates = CGRectIntersection(contentView.bounds, frameInContentCoordinates);
     auto jsContext = m_context->jsContext();
     return JSValueToObject(jsContext, [JSValue valueWithObject:toNSDictionary(frameInContentCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
 }
@@ -538,10 +539,35 @@
     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
     UIView *selectionRangeView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView"];
     auto frameInContentCoordinates = [selectionRangeView convertRect:[[selectionRangeView valueForKeyPath:@"endGrabber"] frame] toView:contentView];
+    frameInContentCoordinates = CGRectIntersection(contentView.bounds, frameInContentCoordinates);
     auto jsContext = m_context->jsContext();
     return JSValueToObject(jsContext, [JSValue valueWithObject:toNSDictionary(frameInContentCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
 }
 
+JSObjectRef UIScriptController::selectionCaretViewRect() const
+{
+    WKWebView *webView = TestController::singleton().mainWebView()->platformView();
+    UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
+    UIView *caretView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.caretView"];
+    auto rectInContentViewCoordinates = CGRectIntersection([caretView convertRect:caretView.bounds toView:contentView], contentView.bounds);
+    return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(rectInContentViewCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
+}
+
+JSObjectRef UIScriptController::selectionRangeViewRects() const
+{
+    WKWebView *webView = TestController::singleton().mainWebView()->platformView();
+    UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
+    UIView *rangeView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView"];
+    auto rectsAsDictionaries = adoptNS([[NSMutableArray alloc] init]);
+    NSArray *textRectInfoArray = [rangeView valueForKeyPath:@"rects"];
+    for (id textRectInfo in textRectInfoArray) {
+        NSValue *rectValue = [textRectInfo valueForKeyPath:@"rect"];
+        auto rangeRectInContentViewCoordinates = [rangeView convertRect:rectValue.CGRectValue toView:contentView];
+        [rectsAsDictionaries addObject:toNSDictionary(CGRectIntersection(rangeRectInContentViewCoordinates, contentView.bounds))];
+    }
+    return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:rectsAsDictionaries.get() inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
+}
+
 JSObjectRef UIScriptController::inputViewBounds() const
 {
     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(TestController::singleton().mainWebView()->platformView()._inputViewBounds) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to