[iOS] Do not show selection UI for editable elements with opacity near zero
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 13 Nov 2018 22:30:27 +0000 (22:30 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 13 Nov 2018 22:30:27 +0000 (22:30 +0000)
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.

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

29 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/selection/character-granularity-rect.html
LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable.html [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html [new file with mode: 0644]
LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable.html [new file with mode: 0644]
LayoutTests/resources/ui-helper.js
Source/WebCore/ChangeLog
Source/WebCore/rendering/RenderObject.cpp
Source/WebCore/rendering/RenderObject.h
Source/WebKit/ChangeLog
Source/WebKit/Shared/AssistedNodeInformation.cpp
Source/WebKit/Shared/AssistedNodeInformation.h
Source/WebKit/Shared/EditorState.cpp
Source/WebKit/Shared/EditorState.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Tools/ChangeLog
Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm
Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm

index 476d1ba..b4eed0d 100644 (file)
@@ -1,3 +1,57 @@
+2018-11-13  Wenson Hsieh  <wenson_hsieh@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  <mattbaker@apple.com>
 
         Web Inspector: Table should support select all (Cmd-A)
index fc5bce4..11aba33 100644 (file)
@@ -22,7 +22,7 @@
         return `
         (function() {
             uiController.longPressAtPoint(30, 20, function() {
-                uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
+                uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionRangeRects));
             });
         })();`
     }
         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
diff --git a/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt b/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable-expected.txt
new file mode 100644 (file)
index 0000000..9a38bef
--- /dev/null
@@ -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.
diff --git a/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html b/LayoutTests/editing/selection/ios/do-not-zoom-to-focused-hidden-contenteditable.html
new file mode 100644 (file)
index 0000000..05722d0
--- /dev/null
@@ -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="../../../resources/ui-helper.js"></script>
+<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>
diff --git a/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt b/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable-expected.txt
new file mode 100644 (file)
index 0000000..f409c25
--- /dev/null
@@ -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)
diff --git a/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable.html b/LayoutTests/editing/selection/ios/hide-selection-after-hiding-contenteditable.html
new file mode 100644 (file)
index 0000000..99abacb
--- /dev/null
@@ -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="../../../resources/ui-helper.js"></script>
+<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>
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt b/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency-expected.txt
new file mode 100644 (file)
index 0000000..e70e692
--- /dev/null
@@ -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)
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html b/LayoutTests/editing/selection/ios/hide-selection-in-contenteditable-nested-transparency.html
new file mode 100644 (file)
index 0000000..59abdc2
--- /dev/null
@@ -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="../../../resources/ui-helper.js"></script>
+<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>
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt b/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-expected.txt
new file mode 100644 (file)
index 0000000..7fce9c1
--- /dev/null
@@ -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)
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt b/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame-expected.txt
new file mode 100644 (file)
index 0000000..dc74dfe
--- /dev/null
@@ -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)
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html b/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable-frame.html
new file mode 100644 (file)
index 0000000..41355c7
--- /dev/null
@@ -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="../../../resources/ui-helper.js"></script>
+<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>
diff --git a/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable.html b/LayoutTests/editing/selection/ios/hide-selection-in-hidden-contenteditable.html
new file mode 100644 (file)
index 0000000..e1b0936
--- /dev/null
@@ -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="../../../resources/ui-helper.js"></script>
+<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>
index ae70f03..f096591 100644 (file)
@@ -170,6 +170,38 @@ window.UIHelper = class UIHelper {
         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 => {
index 7e8afec..07ab1eb 100644 (file)
@@ -1,3 +1,26 @@
+2018-11-13  Wenson Hsieh  <wenson_hsieh@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.carlson@apple.com>
 
         [MediaStream] Observer AVCaptureDevice "suspended" property
index 4255105..d5f8085 100644 (file)
@@ -1514,6 +1514,35 @@ void RenderObject::destroy()
     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). 
index 2dce9dc..6745c07 100644 (file)
@@ -797,6 +797,8 @@ public:
     void initializeFragmentedFlowStateOnInsertion();
     virtual void insertedIntoTree();
 
+    WEBCORE_EXPORT bool isTransparentRespectingParentFrames() const;
+
 protected:
     //////////////////////////////////////////
     // Helper functions. Dangerous to use!
index 0669904..b734f95 100644 (file)
@@ -1,3 +1,79 @@
+2018-11-13  Wenson Hsieh  <wenson_hsieh@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  <ryanhaddad@apple.com>
 
         Unreviewed, rolling out r238137.
index 9ea5d11..a8bd7a7 100644 (file)
@@ -91,6 +91,7 @@ void AssistedNodeInformation::encode(IPC::Encoder& encoder) const
     encoder << title;
     encoder << acceptsAutofilledLoginCredentials;
     encoder << isAutofillableUsernameField;
+    encoder << elementIsTransparent;
     encoder << representingPageURL;
     encoder.encodeEnum(autofillFieldName);
     encoder << placeholder;
@@ -191,6 +192,9 @@ bool AssistedNodeInformation::decode(IPC::Decoder& decoder, AssistedNodeInformat
     if (!decoder.decode(result.isAutofillableUsernameField))
         return false;
 
+    if (!decoder.decode(result.elementIsTransparent))
+        return false;
+
     if (!decoder.decode(result.representingPageURL))
         return false;
 
index 7979efb..04844a2 100644 (file)
@@ -120,6 +120,7 @@ struct AssistedNodeInformation {
     String title;
     bool acceptsAutofilledLoginCredentials { false };
     bool isAutofillableUsernameField { false };
+    bool elementIsTransparent { false };
     WebCore::URL representingPageURL;
     WebCore::AutofillFieldName autofillFieldName { WebCore::AutofillFieldName::None };
     String placeholder;
index c073d30..0a2ba7f 100644 (file)
@@ -131,6 +131,7 @@ void EditorState::PostLayoutData::encode(IPC::Encoder& encoder) const
     encoder << isStableStateUpdate;
     encoder << insideFixedPosition;
     encoder << hasPlainText;
+    encoder << elementIsTransparent;
     encoder << caretColor;
 #endif
 #if PLATFORM(MAC)
@@ -187,6 +188,8 @@ bool EditorState::PostLayoutData::decode(IPC::Decoder& decoder, PostLayoutData&
         return false;
     if (!decoder.decode(result.hasPlainText))
         return false;
+    if (!decoder.decode(result.elementIsTransparent))
+        return false;
     if (!decoder.decode(result.caretColor))
         return false;
 #endif
index 0fa2383..9e33906 100644 (file)
@@ -107,6 +107,7 @@ struct EditorState {
         bool isStableStateUpdate { false };
         bool insideFixedPosition { false };
         bool hasPlainText { false };
+        bool elementIsTransparent { false };
         WebCore::Color caretColor;
 #endif
 #if PLATFORM(MAC)
index 70c8836..b887b77 100644 (file)
@@ -1342,16 +1342,18 @@ static NSValue *nsSizeForTapHighlightBorderRadius(WebCore::IntSize borderRadius,
 
 - (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 @@ static inline bool isSamePair(UIGestureRecognizer *a, UIGestureRecognizer *b, UI
     if (!_webView.configuration._textInteractionGesturesEnabled)
         return NO;
 
+    if (self.suppressAssistantSelectionView)
+        return NO;
+
     if (_inspectorNodeSearchEnabled)
         return NO;
 
@@ -1769,6 +1774,9 @@ static inline bool isSamePair(UIGestureRecognizer *a, UIGestureRecognizer *b, UI
     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 @@ static inline bool isSamePair(UIGestureRecognizer *a, UIGestureRecognizer *b, UI
     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 @@ static bool isAssistableInputType(InputType type)
     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 @@ static bool isAssistableInputType(InputType type)
         [_webView _scheduleVisibleContentRectUpdate];
 
     [_webView didEndFormControlInteraction];
+
+    self.suppressAssistantSelectionView = NO;
 }
 
 - (void)updateCurrentAssistedNodeInformation:(Function<void(bool didUpdate)>&&)callback
@@ -4741,9 +4756,14 @@ static bool isAssistableInputType(InputType type)
 
 - (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 @@ static bool isAssistableInputType(InputType type)
         }
     }
 
-    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 @@ static bool isAssistableInputType(InputType type)
     }
 }
 
+- (BOOL)_shouldSuppressSelectionCommands
+{
+    return _suppressAssistantSelectionView;
+}
+
 - (BOOL)suppressAssistantSelectionView
 {
     return _suppressAssistantSelectionView;
index ef3a5f3..ee082c4 100644 (file)
@@ -240,6 +240,7 @@ void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePost
         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 @@ void WebPage::getAssistedNodeInformation(AssistedNodeInformation& information)
         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);
index 03cf9cf..9ab042e 100644 (file)
@@ -1,3 +1,38 @@
+2018-11-13  Wenson Hsieh  <wenson_hsieh@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  <dabates@apple.com>
 
         Consolidate WebKit UIKitSPI.h and UIKitTestSPI.h
index 760aba0..d41686d 100644 (file)
@@ -309,7 +309,7 @@ void UIScriptController::platformClearAllCallbacks()
 {
 }
 
-JSObjectRef UIScriptController::selectionRangeViewRects() const
+JSObjectRef UIScriptController::textSelectionRangeRects() const
 {
     return nullptr;
 }
@@ -389,6 +389,16 @@ JSObjectRef UIScriptController::selectionEndGrabberViewRect() const
     return nullptr;
 }
 
+JSObjectRef UIScriptController::selectionCaretViewRect() const
+{
+    return nullptr;
+}
+
+JSObjectRef UIScriptController::selectionRangeViewRects() const
+{
+    return nullptr;
+}
+
 bool UIScriptController::isShowingDataListSuggestions() const
 {
     return false;
index 4e3fd49..52d330a 100644 (file)
@@ -238,10 +238,12 @@ interface UIScriptController {
 
     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;
index f25ec12..4dcb4a6 100644 (file)
@@ -389,7 +389,7 @@ JSObjectRef UIScriptController::contentVisibleRect() const
     return nullptr;
 }
 
-JSObjectRef UIScriptController::selectionRangeViewRects() const
+JSObjectRef UIScriptController::textSelectionRangeRects() const
 {
     return nullptr;
 }
@@ -404,6 +404,16 @@ JSObjectRef UIScriptController::selectionStartGrabberViewRect() const
     return nullptr;
 }
 
+JSObjectRef UIScriptController::selectionCaretViewRect() const
+{
+    return nullptr;
+}
+
+JSObjectRef UIScriptController::selectionRangeViewRects() const
+{
+    return nullptr;
+}
+
 JSObjectRef UIScriptController::selectionEndGrabberViewRect() const
 {
     return nullptr;
index 0e95219..ebbf548 100644 (file)
@@ -159,10 +159,12 @@ public:
 
     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;
index da8e73a..ffea7a0 100644 (file)
@@ -507,14 +507,14 @@ JSObjectRef UIScriptController::contentVisibleRect() const
     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 @@ JSObjectRef UIScriptController::selectionStartGrabberViewRect() const
     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 @@ JSObjectRef UIScriptController::selectionEndGrabberViewRect() const
     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);