Commit bbc9e285 authored by JunHo Seo's avatar JunHo Seo Committed by Commit Bot

[SpatNav] Navigate only visible element

A focusable element can be invisible to user when another
element covers entire the focusable element. In most cases
giving focus to the invisible focusable element is not good
because user cannot know where is focus and what's going on.

Moreover, let's assume that a floating menu is displayed and
we want to navigate items in the menu. In this case, we want
to navigate each item regardless of any focusable element under
the floating menu.

To navigate only visible element, check visibility of candidate.
We treat an element as visible when the element is not offscreen,
and the element is included in hit test result.

Change-Id: Iafc89edd77b05cc047395440548004a937bba8c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1575314
Commit-Queue: JunHo Seo <junho0924.seo@lge.com>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#659441}
parent 4c16a199
......@@ -37,6 +37,7 @@
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
......@@ -185,6 +186,49 @@ ScrollableArea* ScrollableAreaFor(const Node* node) {
return ToLayoutBox(object)->GetScrollableArea();
}
bool IsUnobscured(const FocusCandidate& candidate) {
DCHECK(candidate.visible_node);
const LocalFrame* local_main_frame = DynamicTo<LocalFrame>(
candidate.visible_node->GetDocument().GetPage()->MainFrame());
if (!local_main_frame)
return false;
// TODO(crbug.com/955952): We cannot evaluate visibility for media element
// using hit test since attached media controls cover media element.
if (candidate.visible_node->IsMediaElement())
return true;
LayoutRect viewport_rect = LayoutRect(
local_main_frame->GetPage()->GetVisualViewport().VisibleContentRect());
LayoutRect interesting_rect =
Intersection(candidate.rect_in_root_frame, viewport_rect);
if (interesting_rect.IsEmpty())
return false;
HitTestLocation location(interesting_rect);
HitTestResult result =
local_main_frame->GetEventHandler().HitTestResultAtLocation(
location, HitTestRequest::kReadOnly | HitTestRequest::kListBased |
HitTestRequest::kIgnoreZeroOpacityObjects |
HitTestRequest::kAllowChildFrameContent);
const HitTestResult::NodeSet& nodes = result.ListBasedTestResult();
for (auto hit_node = nodes.rbegin(); hit_node != nodes.rend(); ++hit_node) {
if (candidate.visible_node->ContainsIncludingHostElements(**hit_node))
return true;
if (FrameOwnerElement(candidate) &&
FrameOwnerElement(candidate)
->contentDocument()
->ContainsIncludingHostElements(**hit_node))
return true;
}
return false;
}
bool HasRemoteFrame(const Node* node) {
auto* frame_owner_element = DynamicTo<HTMLFrameOwnerElement>(node);
if (!frame_owner_element)
......
......@@ -73,6 +73,7 @@ struct FocusCandidate {
CORE_EXPORT bool HasRemoteFrame(const Node*);
CORE_EXPORT FloatRect RectInViewport(const Node&);
CORE_EXPORT bool IsOffscreen(const Node*);
CORE_EXPORT bool IsUnobscured(const FocusCandidate&);
bool ScrollInDirection(Node* container, SpatialNavigationDirection);
CORE_EXPORT bool IsScrollableNode(const Node* node);
CORE_EXPORT bool IsScrollableAreaOrDocument(const Node*);
......
......@@ -94,44 +94,41 @@ static void ConsiderForBestCandidate(SpatialNavigationDirection direction,
if (distance == MaxDistance())
return;
if (best_candidate->IsNull()) {
*best_candidate = candidate;
*best_distance = distance;
return;
}
LayoutRect intersection_rect = Intersection(
candidate.rect_in_root_frame, best_candidate->rect_in_root_frame);
if (!intersection_rect.IsEmpty() &&
!AreElementsOnSameLine(*best_candidate, candidate) &&
intersection_rect == candidate.rect_in_root_frame) {
// If 2 nodes are intersecting, do hit test to find which node in on top.
LayoutUnit x = intersection_rect.X() + intersection_rect.Width() / 2;
LayoutUnit y = intersection_rect.Y() + intersection_rect.Height() / 2;
if (!IsA<LocalFrame>(
candidate.visible_node->GetDocument().GetPage()->MainFrame()))
return;
HitTestLocation location(IntPoint(x.ToInt(), y.ToInt()));
HitTestResult result =
candidate.visible_node->GetDocument()
.GetPage()
->DeprecatedLocalMainFrame()
->GetEventHandler()
.HitTestResultAtLocation(
location, HitTestRequest::kReadOnly | HitTestRequest::kActive |
HitTestRequest::kIgnoreClipping);
if (candidate.visible_node->ContainsIncludingHostElements(
*result.InnerNode())) {
*best_candidate = candidate;
*best_distance = distance;
return;
if (!best_candidate->IsNull()) {
LayoutRect intersection_rect = Intersection(
candidate.rect_in_root_frame, best_candidate->rect_in_root_frame);
if (!intersection_rect.IsEmpty() &&
!AreElementsOnSameLine(*best_candidate, candidate) &&
intersection_rect == candidate.rect_in_root_frame) {
// If 2 nodes are intersecting, do hit test to find which node in on top.
LayoutUnit x = intersection_rect.X() + intersection_rect.Width() / 2;
LayoutUnit y = intersection_rect.Y() + intersection_rect.Height() / 2;
if (!IsA<LocalFrame>(
candidate.visible_node->GetDocument().GetPage()->MainFrame()))
return;
HitTestLocation location(IntPoint(x.ToInt(), y.ToInt()));
HitTestResult result =
candidate.visible_node->GetDocument()
.GetPage()
->DeprecatedLocalMainFrame()
->GetEventHandler()
.HitTestResultAtLocation(location,
HitTestRequest::kReadOnly |
HitTestRequest::kActive |
HitTestRequest::kIgnoreClipping);
if (candidate.visible_node->ContainsIncludingHostElements(
*result.InnerNode())) {
*best_candidate = candidate;
*best_distance = distance;
return;
}
if (best_candidate->visible_node->ContainsIncludingHostElements(
*result.InnerNode()))
return;
}
if (best_candidate->visible_node->ContainsIncludingHostElements(
*result.InnerNode()))
return;
}
if (distance < *best_distance) {
if (distance < *best_distance && IsUnobscured(candidate)) {
*best_candidate = candidate;
*best_distance = distance;
}
......
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/snav-testharness.js"></script>
<style>
div {
width: 100px;
height: 100px;
background-color: green;
position: absolute;
}
#blocker {
top: 480px;
height: 50px;
background-color: yellow;
}
#A {
top: 490px;
}
</style>
<div id="start" tabindex="0"></div>
<div id="A" tabindex="0"></div>
<div id="blocker"></div>
<script>
const A = document.getElementById("A");
const start = document.getElementById("start");
snav.assertSnavEnabledAndTestable();
start.focus();
test(() => {
window.internals.setPageScaleFactor(1.2);
// Down should scroll the visual viewport, since there's no targets
// availabe on screen(Visible part of element 'A' is obscured by
// blocker element).
snav.triggerMove('Down');
assert_equals(document.activeElement,
start,
"Focus should not be moved.");
snav.triggerMove('Up');
window.internals.setPageScaleFactor(1);
// Now we can see visible part of element 'A'.
snav.triggerMove('Down');
assert_equals(document.activeElement,
A,
"Focus should be moved to A.");
}, "Don't navigate to elements that are off screen.");
</script>
......@@ -11,12 +11,19 @@
position: absolute;
}
#blocker {
top: 30px;
background-color: yellow;
}
#A {
top: 601px;
height: 50px;
}
</style>
<div id="start" tabindex="0"></div>
<div id="blocker"></div>
<div id="A" tabindex="0"></div>
<script>
......@@ -50,5 +57,15 @@
assert_equals(window.internals.interestedElement,
A,
"Navigate to onscreen element.");
snav.triggerMove('Up');
assert_equals(window.internals.interestedElement,
A,
"Don't navigate to obscured(in viewport) element even if it has visible part outside of viewport.");
snav.triggerMove('Up');
assert_equals(window.internals.interestedElement,
start,
"Navigate to onscreen element.");
}, "Don't navigate to elements that are off screen.");
</script>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/snav-testharness.js"></script>
<style>
body {
font: 15px Ahem;
}
a, iframe {
position: absolute;
left: 5px;
}
.blocker {
position: absolute;
left: 0px;
width: 500px;
height: 55px;
background-color: green;
}
</style>
<div style="width: 500px; height: 600px">
<a id="start" style="top: 10px;" href="a">Start</a>
<a href="a" style="top: 80px;">Fully Obscured Link</a>
<div class="blocker" style="top: 60px;"></div>
<a id="partially_obscured" style="top: 160px;" href="a">Partially Obscured Link</a>
<div class="blocker" style="width: 150px; top: 140px;"></div>
<a id="obscured_by_transparent_element" style="top: 240px" href="a">Obscured by transparent element</a>
<div class="blocker" style="top: 220px; opacity: 0"></div>
<a id="partially_obscured_by_transparent_element" style="top: 320px" href="a">Partially obscured by transparent element</a>
<div class="blocker" style="width: 150px; top: 300px; opacity: 0"></div>
<a id="unobscured_at_outside_of_viewport" style="left: -50px; top: 400px" href="a">Unobscured at outside of viewport</a>
<div class="blocker" style="top: 380px;"></div>
<iframe style="top: 480px;" width=200 height=50 frameborder="0" srcdoc="
<!DOCTYPE html>
<body id='body_in_iframe' style='font: 15px Ahem'>
<a id='link_in_iframe' href='a'>Link in iframe</a>
</bdoy>
"></iframe>
<a id="end" style="top: 560px;" href="a">End</a>
</div>
<script>
// This test checks that navigation isn't performed to an element
// that is obscured (by another element).
snav.assertSnavEnabledAndTestable();
const t = async_test("Test obscured elements during spatial navigation.");
onload = t.step_func(() => {
// Start at a known place.
document.getElementById("start").focus();
// In this step, fully obscured element will be skipped.
snav.triggerMove('Down');
assert_equals(document.activeElement,
document.getElementById("partially_obscured"),
"Fully obscured element should be skipped. Partially obscured element should be focused.");
snav.triggerMove('Down');
assert_equals(document.activeElement,
document.getElementById("obscured_by_transparent_element"),
"Obscured due to transparent element should be focused.");
snav.triggerMove('Down');
assert_equals(document.activeElement,
document.getElementById("partially_obscured_by_transparent_element"),
"(Partially) Obscured due to transparent element should be focused.");
snav.triggerMove('Down');
assert_not_equals(document.activeElement,
document.getElementById("unobscured_at_outside_of_viewport"),
"Element's visible part that located at outside of viewport should not be accounted.");
assert_equals(window.internals.interestedElement,
document.querySelector("iframe").contentDocument.getElementById("body_in_iframe"),
"Body element should be focused first, when we navigate to visible iframe.");
snav.triggerMove('Down');
assert_equals(window.internals.interestedElement,
document.querySelector("iframe").contentDocument.getElementById("link_in_iframe"),
"Focus should be moved to unobscured element in iframe.");
snav.triggerMove('Down');
assert_equals(window.internals.interestedElement,
document.getElementById("end"),
"Focus should escape from iframe.");
t.done();
});
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment