зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
631f5f7dbb
Коммит
11571dd0b7
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
Загрузка…
Ссылка в новой задаче