Bug 1660271 - Move the focus to the previously focused element when <dialog> is closed r=smaug

This change is based on the latest changes for the <dialog> element in
https://github.com/whatwg/html/pull/6531, such that closing
<dialog> should move the focus to the previously focused element.

Differential Revision: https://phabricator.services.mozilla.com/D109726
This commit is contained in:
Sean Feng 2021-04-29 19:35:23 +00:00
parent 631f5f7dbb
commit 11571dd0b7
3 changed files with 186 additions and 1 deletions

View File

@ -52,6 +52,18 @@ void HTMLDialogElement::Close(
RemoveFromTopLayerIfNeeded();
RefPtr<Element> previouslyFocusedElement =
do_QueryReferent(mPreviouslyFocusedElement);
if (previouslyFocusedElement) {
mPreviouslyFocusedElement = nullptr;
FocusOptions options;
options.mPreventScroll = true;
previouslyFocusedElement->Focus(options, CallerType::NonSystem,
IgnoredErrorResult());
}
RefPtr<AsyncEventDispatcher> eventDispatcher =
new AsyncEventDispatcher(this, u"close"_ns, CanBubble::eNo);
eventDispatcher->PostDOMEvent();
@ -62,6 +74,9 @@ void HTMLDialogElement::Show() {
return;
}
SetOpen(true, IgnoreErrors());
StorePreviouslyFocusedElement();
FocusDialog();
}
@ -93,6 +108,14 @@ void HTMLDialogElement::RemoveFromTopLayerIfNeeded() {
doc->UnsetBlockedByModalDialog(*this);
}
void HTMLDialogElement::StorePreviouslyFocusedElement() {
if (nsIContent* unretargetedFocus =
GetComposedDoc()->GetUnretargetedFocusedContent()) {
mPreviouslyFocusedElement =
do_GetWeakReference(unretargetedFocus->AsElement());
}
}
void HTMLDialogElement::UnbindFromTree(bool aNullParent) {
RemoveFromTopLayerIfNeeded();
nsGenericHTMLElement::UnbindFromTree(aNullParent);
@ -114,6 +137,8 @@ void HTMLDialogElement::ShowModal(ErrorResult& aError) {
SetOpen(true, aError);
StorePreviouslyFocusedElement();
FocusDialog();
aError.SuppressException();

View File

@ -19,7 +19,8 @@ class HTMLDialogElement final : public nsGenericHTMLElement {
public:
explicit HTMLDialogElement(
already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
: nsGenericHTMLElement(std::move(aNodeInfo)) {}
: nsGenericHTMLElement(std::move(aNodeInfo)),
mPreviouslyFocusedElement(nullptr) {}
NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDialogElement, dialog)
@ -58,6 +59,9 @@ class HTMLDialogElement final : public nsGenericHTMLElement {
private:
void AddToTopLayerIfNeeded();
void RemoveFromTopLayerIfNeeded();
void StorePreviouslyFocusedElement();
nsWeakPtr mPreviouslyFocusedElement;
};
} // namespace dom

View File

@ -0,0 +1,156 @@
<!DOCTYPE html>
<meta charset=urf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Test focus is moved to the previously focused element when dialog is closed</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<body>
<input />
<dialog>
<button id="button1">This is a button1</button>
<button id="button2">This is a button2</button>
<button id="button3">This is a button3</button>
</dialog>
<script>
// Test focus is moved to the previously focused element
function test_move_to_previously_focused(showModal) {
const input = document.querySelector("input");
input.focus();
const dialog = document.querySelector('dialog');
if (showModal) {
dialog.showModal();
} else {
dialog.show();
}
dialog.close();
assert_equals(document.activeElement, input);
}
// Test focus is moved to the previously focused element with some complex dialog usage
async function test_move_to_previously_focused_with_complex_dialog_usage(showModal) {
const input = document.querySelector("input");
input.focus();
const dialog = document.querySelector('dialog');
if (showModal) {
dialog.showModal();
} else {
dialog.show();
}
const button1 = document.getElementById("button1");
const button2 = document.getElementById("button2");
const button3 = document.getElementById("button3");
await test_driver.click(button1);
await test_driver.click(button2);
await test_driver.click(button3);
dialog.close();
assert_equals(document.activeElement, input);
}
// Test focus is moved to <body> if the previously focused
// element can't be focused
function test_move_to_body_if_fails(showModal) {
const input = document.querySelector("input");
input.focus();
const dialog = document.querySelector('dialog');
if (showModal) {
dialog.showModal();
} else {
dialog.show();
}
dialog.close();
input.remove();
assert_equals(document.activeElement, document.body);
document.body.appendChild(input);
}
// Test focus is moved to shadow host if the previously
// focused element is a shadow node.
function test_move_to_shadow_host(showModal) {
const shadowHost = document.createElement("div");
const shadowRoot = shadowHost.attachShadow({mode: 'open'});
shadowRoot.appendChild(document.createElement("input"));
document.body.appendChild(shadowHost);
const inputElement = shadowRoot.querySelector("input");
inputElement.focus();
assert_equals(document.activeElement, shadowHost);
assert_equals(shadowRoot.activeElement, inputElement);
const dialog = document.querySelector('dialog');
if (showModal) {
dialog.showModal();
} else {
dialog.show();
}
dialog.close();
assert_equals(document.activeElement, shadowHost);
assert_equals(shadowRoot.activeElement, inputElement);
}
// Test moving the focus doesn't scroll the viewport
function test_move_focus_dont_scroll_viewport(showModal) {
const outViewPortButton = document.createElement("button");
outViewPortButton.style.top = (window.innerHeight + 10).toString() + "px";
outViewPortButton.style.position = "absolute";
document.body.appendChild(outViewPortButton);
outViewPortButton.focus();
// Since the outViewPortButton is focused, so the viewport should be
// scrolled to it
assert_true(document.documentElement.scrollTop > 0 );
const dialog = document.querySelector('dialog');
if (showModal) {
dialog.showModal();
} else {
dialog.show();
}
window.scrollTo(0, 0);
assert_equals(document.documentElement.scrollTop, 0);
dialog.close();
assert_equals(document.documentElement.scrollTop, 0);
assert_equals(document.activeElement, outViewPortButton);
}
test(() => {
test_move_to_previously_focused(true);
test_move_to_previously_focused(false);
}, 'Focus should be moved to the previously focused element(Simple dialog usage)');
promise_test(async () => {
await test_move_to_previously_focused_with_complex_dialog_usage(true);
await test_move_to_previously_focused_with_complex_dialog_usage(false);
}, 'Focus should be moved to the previously focused element(Complex dialog usage)');
test(() => {
test_move_to_body_if_fails(true);
test_move_to_body_if_fails(false);
}, 'Focus should be moved to the body if the previously focused element is removed');
test(() => {
test_move_to_shadow_host(true);
test_move_to_shadow_host(false);
}, 'Focus should be moved to the shadow DOM host if the previouly focused element is a shadow DOM node');
test(() => {
test_move_focus_dont_scroll_viewport(true);
test_move_focus_dont_scroll_viewport(false);
}, 'Focus should not scroll if the previously focused element is outside the viewport');
</script>
</body>