Issue #2135 - Implement sequential focus navigation for shadow DOM

This covers the following:
Bug 1413834	Implement sequential focus navigation regarding shadow DOM
> Bug 1430020	Let sequential focus navigation in shadow DOM enter iframes
> Bug 1430701	Handle focus navigation on frameless shadow hosts in light DOM
> Bug 1430692	Handle focus navigation on NAC in shadow DOM
> Bug 1453693	Ensure sequential focus navigation works in Shadow DOM and add some tests
> Bug 1466581	Handle sequential focus also in nested shadow DOM
> Bug 1481079	Shadow DOM hosts should be focusable
> Bug 1507101	Use StyleChildrenIterator instead of custom frame tree walking code to handle NAC inside shadow dom
> Bug 1512043	Ensure traverse all nodes owned by the top level shadow host
> Bug 1512457	Fix various cases that focus navigation doesn't work well with frameless shadow host
> Bug 1513141	Really minor nsFocusManager cleanup
> Bug 1519090	Keyboard focus is trapped inside <slot>
> Bug 1528034	Make IsHostOrSlot null-safe
> Bug 1544826	Wrong focus navigation behavior when the root element is a shadow root
>> Bug 1500273	Ensure backward focus navigation works in Shadow DOM
This commit is contained in:
FranklinDM 2023-03-04 02:18:40 +08:00 committed by roytam1
commit 56193120c5
7 changed files with 1712 additions and 98 deletions

View file

@ -6436,9 +6436,7 @@ nsContentUtils::IsFocusedContent(const nsIContent* aContent)
bool
nsContentUtils::IsSubDocumentTabbable(nsIContent* aContent)
{
//XXXsmaug Shadow DOM spec issue!
// We may need to change this to GetComposedDoc().
nsIDocument* doc = aContent->GetUncomposedDoc();
nsIDocument* doc = aContent->GetComposedDoc();
if (!doc) {
return false;
}

View file

@ -8,10 +8,12 @@
#include "nsFocusManager.h"
#include "AccessibleCaretEventHub.h"
#include "ChildIterator.h"
#include "nsAttrValueInlines.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsGkAtoms.h"
#include "nsContentUtils.h"
#include "nsIDocument.h"
#include "nsDocument.h"
#include "nsIEditor.h"
#include "nsPIDOMWindow.h"
#include "nsIDOMChromeWindow.h"
@ -46,7 +48,9 @@
#include "mozilla/ContentEvents.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLSlotElement.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/EventStates.h"
@ -2978,6 +2982,378 @@ nsFocusManager::DetermineElementToMoveFocus(nsPIDOMWindowOuter* aWindow,
return NS_OK;
}
static bool
IsHostOrSlot(nsIContent* aContent)
{
return aContent && (aContent->GetShadowRoot() ||
aContent->IsHTMLElement(nsGkAtoms::slot));
}
// Helper class to iterate contents in scope by traversing flattened tree
// in tree order
class MOZ_STACK_CLASS ScopedContentTraversal
{
public:
ScopedContentTraversal(nsIContent* aStartContent, nsIContent* aOwner)
: mCurrent(aStartContent)
, mOwner(aOwner)
{
MOZ_ASSERT(aStartContent);
}
void Next();
void Prev();
void Reset()
{
SetCurrent(mOwner);
}
nsIContent* GetCurrent()
{
return mCurrent;
}
private:
void SetCurrent(nsIContent* aContent)
{
mCurrent = aContent;
}
nsIContent* mCurrent;
nsIContent* mOwner;
};
void
ScopedContentTraversal::Next()
{
MOZ_ASSERT(mCurrent);
// Get mCurrent's first child if it's in the same scope.
if (!IsHostOrSlot(mCurrent) || mCurrent == mOwner) {
StyleChildrenIterator iter(mCurrent);
nsIContent* child = iter.GetNextChild();
if (child) {
SetCurrent(child);
return;
}
}
// If mOwner has no children, END traversal
if (mCurrent == mOwner) {
SetCurrent(nullptr);
return;
}
nsIContent* current = mCurrent;
while (1) {
// Create parent's iterator and move to current
nsIContent* parent = current->GetFlattenedTreeParent();
StyleChildrenIterator parentIter(parent);
parentIter.Seek(current);
// Get next sibling of current
if (nsIContent* next = parentIter.GetNextChild()) {
SetCurrent(next);
return;
}
// If no next sibling and parent is mOwner, END traversal
if (parent == mOwner) {
SetCurrent(nullptr);
return;
}
current = parent;
}
}
void
ScopedContentTraversal::Prev()
{
MOZ_ASSERT(mCurrent);
nsIContent* parent;
nsIContent* last;
if (mCurrent == mOwner) {
// Get last child of mOwner
StyleChildrenIterator ownerIter(mOwner, false /* aStartAtBeginning */);
last = ownerIter.GetPreviousChild();
parent = last;
} else {
// Create parent's iterator and move to mCurrent
parent = mCurrent->GetFlattenedTreeParent();
StyleChildrenIterator parentIter(parent);
parentIter.Seek(mCurrent);
// Get previous sibling
last = parentIter.GetPreviousChild();
}
while (last) {
parent = last;
if (parent->GetShadowRoot() ||
parent->IsHTMLElement(nsGkAtoms::slot)) {
// Skip contents in other scopes
break;
}
// Find last child
StyleChildrenIterator iter(parent, false /* aStartAtBeginning */);
last = iter.GetPreviousChild();
}
// If parent is mOwner and no previous sibling remains, END traversal
SetCurrent(parent == mOwner ? nullptr : parent);
}
nsIContent*
nsFocusManager::FindOwner(nsIContent* aContent)
{
nsIContent* currentContent = aContent;
while (currentContent) {
nsIContent* parent = currentContent->GetFlattenedTreeParent();
// Shadow host / Slot
if (IsHostOrSlot(parent)) {
return parent;
}
currentContent = parent;
}
return nullptr;
}
/**
* Host and Slot elements need to be handled as if they had tabindex 0 even
* when they don't have the attribute. This is a helper method to get the
* right value for focus navigation. If aIsFocusable is passed, it is set to
* true if the element itself is focusable.
*/
static int32_t
HostOrSlotTabIndexValue(nsIContent* aContent,
bool* aIsFocusable = nullptr)
{
MOZ_ASSERT(IsHostOrSlot(aContent));
if (aIsFocusable) {
*aIsFocusable = false;
nsIFrame* frame = aContent->GetPrimaryFrame();
if (frame) {
int32_t tabIndex;
frame->IsFocusable(&tabIndex, 0);
*aIsFocusable = tabIndex >= 0;
}
}
const nsAttrValue* attrVal =
aContent->AsElement()->GetParsedAttr(nsGkAtoms::tabindex);
if (!attrVal) {
return 0;
}
if (attrVal->Type() == nsAttrValue::eInteger) {
return attrVal->GetIntegerValue();
}
return -1;
}
nsIContent*
nsFocusManager::GetNextTabbableContentInScope(nsIContent* aOwner,
nsIContent* aStartContent,
nsIContent* aOriginalStartContent,
bool aForward,
int32_t aCurrentTabIndex,
bool aIgnoreTabIndex,
bool aForDocumentNavigation,
bool aSkipOwner)
{
MOZ_ASSERT(IsHostOrSlot(aOwner), "Scope owner should be host or slot");
if (!aSkipOwner && (aForward && aOwner == aStartContent)) {
int32_t tabIndex = -1;
nsIFrame* frame = aOwner->GetPrimaryFrame();
if (frame && frame->IsFocusable(&tabIndex, false) && tabIndex >= 0) {
return aOwner;
}
}
//
// Iterate contents in scope
//
ScopedContentTraversal contentTraversal(aStartContent, aOwner);
nsCOMPtr<nsIContent> iterContent;
nsIContent* firstNonChromeOnly = aStartContent->IsInNativeAnonymousSubtree() ?
aStartContent->FindFirstNonChromeOnlyAccessContent() : nullptr;
while (1) {
// Iterate tab index to find corresponding contents in scope
while (1) {
// Iterate remaining contents in scope to find next content to focus
// Get next content
aForward ? contentTraversal.Next() : contentTraversal.Prev();
iterContent = contentTraversal.GetCurrent();
if (firstNonChromeOnly && firstNonChromeOnly == iterContent) {
// We just broke out from the native anonymous content, so move
// to the previous/next node of the native anonymous owner.
if (aForward) {
contentTraversal.Next();
} else {
contentTraversal.Prev();
}
iterContent = contentTraversal.GetCurrent();
}
if (!iterContent) {
// Reach the end
break;
}
// Get the tab index of the next element. For NAC we rely on frames.
//XXXsmaug we should probably use frames also for Shadow DOM and special
// case only display:contents elements.
int32_t tabIndex = 0;
if (iterContent->IsInNativeAnonymousSubtree() &&
iterContent->GetPrimaryFrame()) {
iterContent->GetPrimaryFrame()->IsFocusable(&tabIndex);
} else if (IsHostOrSlot(iterContent)) {
tabIndex = HostOrSlotTabIndexValue(iterContent);
} else {
iterContent->IsFocusable(&tabIndex);
}
if (tabIndex < 0 || !(aIgnoreTabIndex || tabIndex == aCurrentTabIndex)) {
continue;
}
if (!IsHostOrSlot(iterContent)) {
nsCOMPtr<nsIContent> elementInFrame;
bool checkSubDocument = true;
if (aForDocumentNavigation &&
TryDocumentNavigation(iterContent, &checkSubDocument,
getter_AddRefs(elementInFrame))) {
return elementInFrame;
}
if (!checkSubDocument) {
continue;
}
if (TryToMoveFocusToSubDocument(iterContent, aOriginalStartContent,
aForward, aForDocumentNavigation,
getter_AddRefs(elementInFrame))) {
return elementInFrame;
}
// Found content to focus
return iterContent;
}
// Search in scope owned by iterContent
nsIContent* contentToFocus =
GetNextTabbableContentInScope(iterContent, iterContent,
aOriginalStartContent, aForward,
aForward ? 1 : 0, aIgnoreTabIndex,
aForDocumentNavigation,
false /* aSkipOwner */);
if (contentToFocus) {
return contentToFocus;
}
};
// If already at lowest priority tab (0), end search completely.
// A bit counterintuitive but true, tabindex order goes 1, 2, ... 32767, 0
if (aCurrentTabIndex == (aForward ? 0 : 1)) {
break;
}
// Continue looking for next highest priority tabindex
aCurrentTabIndex = GetNextTabIndex(aOwner, aCurrentTabIndex, aForward);
contentTraversal.Reset();
}
// Return scope owner at last for backward navigation if its tabindex
// is non-negative
if (!aSkipOwner && !aForward) {
int32_t tabIndex = -1;
nsIFrame* frame = aOwner->GetPrimaryFrame();
if (frame && frame->IsFocusable(&tabIndex, false) && tabIndex >= 0) {
return aOwner;
}
}
return nullptr;
}
nsIContent*
nsFocusManager::GetNextTabbableContentInAncestorScopes(
nsIContent* aStartOwner,
nsIContent** aStartContent,
nsIContent* aOriginalStartContent,
bool aForward,
int32_t* aCurrentTabIndex,
bool aIgnoreTabIndex,
bool aForDocumentNavigation)
{
MOZ_ASSERT(aStartOwner == FindOwner(*aStartContent),
"aStartOwner should be the scope owner of aStartContent");
MOZ_ASSERT(IsHostOrSlot(aStartOwner), "scope owner should be host or slot");
nsIContent* owner = aStartOwner;
nsIContent* startContent = *aStartContent;
while (IsHostOrSlot(owner)) {
int32_t tabIndex = 0;
if (IsHostOrSlot(startContent)) {
tabIndex = HostOrSlotTabIndexValue(startContent);
} else {
startContent->IsFocusable(&tabIndex);
}
nsIContent* contentToFocus =
GetNextTabbableContentInScope(owner, startContent, aOriginalStartContent,
aForward, tabIndex, aIgnoreTabIndex,
aForDocumentNavigation,
false /* aSkipOwner */);
if (contentToFocus) {
return contentToFocus;
}
startContent = owner;
owner = FindOwner(startContent);
}
// If not found in shadow DOM, search from the top level shadow host in light DOM
*aStartContent = startContent;
*aCurrentTabIndex = HostOrSlotTabIndexValue(startContent);
return nullptr;
}
static nsIContent*
GetTopLevelScopeOwner(nsIContent* aContent)
{
nsIContent* topLevelScopeOwner = nullptr;
while (aContent) {
if (HTMLSlotElement* slot = aContent->GetAssignedSlot()) {
aContent = slot;
} else if (ShadowRoot* shadowRoot = aContent->GetContainingShadow()) {
aContent = shadowRoot->Host();
topLevelScopeOwner = aContent;
} else {
// TODO: replaced with FromNode.
if (HTMLSlotElement::FromContentOrNull(aContent)) {
topLevelScopeOwner = aContent;
}
aContent = aContent->GetParent();
}
}
return topLevelScopeOwner;
}
nsresult
nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
nsIContent* aRootContent,
@ -2995,70 +3371,163 @@ nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
if (!startContent)
return NS_OK;
nsIContent* currentTopLevelScopeOwner = GetTopLevelScopeOwner(aStartContent);
LOGCONTENTNAVIGATION("GetNextTabbable: %s", aStartContent);
LOGFOCUSNAVIGATION((" tabindex: %d", aCurrentTabIndex));
if (nsDocument::IsWebComponentsEnabled(aRootContent)) {
// If aStartContent is a shadow host or slot in forward navigation,
// search in scope owned by aStartContent.
if (aForward && IsHostOrSlot(aStartContent)) {
nsIContent* contentToFocus =
GetNextTabbableContentInScope(aStartContent, aStartContent,
aOriginalStartContent, aForward,
aForward ? 1 : 0, aIgnoreTabIndex,
aForDocumentNavigation,
true /* aSkipOwner */);
if (contentToFocus) {
NS_ADDREF(*aResultContent = contentToFocus);
return NS_OK;
}
}
// If aStartContent is in a scope owned by Shadow DOM search from scope
// including aStartContent.
if (nsIContent* owner = FindOwner(aStartContent)) {
nsIContent* contentToFocus =
GetNextTabbableContentInAncestorScopes(owner,
&aStartContent,
aOriginalStartContent,
aForward,
&aCurrentTabIndex,
aIgnoreTabIndex,
aForDocumentNavigation);
if (contentToFocus) {
NS_ADDREF(*aResultContent = contentToFocus);
return NS_OK;
}
}
// If we reach here, it means no next tabbable content in shadow DOM.
// We need to continue searching in light DOM, starting at the top level
// shadow host in light DOM (updated aStartContent) and its tabindex
// (updated aCurrentTabIndex).
MOZ_ASSERT(!FindOwner(aStartContent),
"aStartContent should not be owned by Shadow DOM at this point");
}
nsPresContext* presContext = aPresShell->GetPresContext();
bool getNextFrame = true;
nsCOMPtr<nsIContent> iterStartContent = aStartContent;
// Iterate tab index to find corresponding contents
while (1) {
nsIFrame* startFrame = iterStartContent->GetPrimaryFrame();
nsIFrame* frame = iterStartContent->GetPrimaryFrame();
// if there is no frame, look for another content node that has a frame
if (!startFrame) {
while (!frame) {
// if the root content doesn't have a frame, just return
if (iterStartContent == aRootContent)
if (iterStartContent == aRootContent) {
return NS_OK;
}
// look for the next or previous content node in tree order
iterStartContent = aForward ? iterStartContent->GetNextNode() : iterStartContent->GetPreviousContent();
iterStartContent = aForward ? iterStartContent->GetNextNode()
: iterStartContent->GetPreviousContent();
if (!iterStartContent) {
break;
}
frame = iterStartContent->GetPrimaryFrame();
// Host without frame, enter its scope.
if (nsDocument::IsWebComponentsEnabled(aRootContent) &&
(!frame && iterStartContent->GetShadowRoot())) {
int32_t tabIndex = HostOrSlotTabIndexValue(iterStartContent);
if (tabIndex >= 0 &&
(aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) {
nsIContent* contentToFocus = GetNextTabbableContentInScope(
iterStartContent, iterStartContent, aOriginalStartContent,
aForward, aForward ? 1 : 0, aIgnoreTabIndex,
aForDocumentNavigation, true /* aSkipOwner */);
if (contentToFocus) {
NS_ADDREF(*aResultContent = contentToFocus);
return NS_OK;
}
}
}
// we've already skipped over the initial focused content, so we
// don't want to traverse frames.
getNextFrame = false;
if (iterStartContent)
continue;
// otherwise, as a last attempt, just look at the root content
iterStartContent = aRootContent;
continue;
}
// For tab navigation, pass false for aSkipPopupChecks so that we don't
// iterate into or out of a popup. For document naviation pass true to
// ignore these boundaries.
nsCOMPtr<nsIFrameEnumerator> frameTraversal;
nsresult rv = NS_NewFrameTraversal(getter_AddRefs(frameTraversal),
presContext, startFrame,
ePreOrder,
false, // aVisual
false, // aLockInScrollView
true, // aFollowOOFs
aForDocumentNavigation // aSkipPopupChecks
);
NS_ENSURE_SUCCESS(rv, rv);
if (frame) {
// For tab navigation, pass false for aSkipPopupChecks so that we don't
// iterate into or out of a popup. For document navigation, pass true to
// ignore these boundaries.
nsresult rv = NS_NewFrameTraversal(getter_AddRefs(frameTraversal),
presContext,
frame,
ePreOrder,
false, // aVisual
false, // aLockInScrollView
true, // aFollowOOFs
aForDocumentNavigation // aSkipPopupChecks
);
NS_ENSURE_SUCCESS(rv, rv);
if (iterStartContent == aRootContent) {
if (!aForward) {
frameTraversal->Last();
} else if (aRootContent->IsFocusable()) {
frameTraversal->Next();
if (iterStartContent == aRootContent) {
if (!aForward) {
frameTraversal->Last();
} else if (aRootContent->IsFocusable()) {
frameTraversal->Next();
}
frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
} else if (getNextFrame &&
(!iterStartContent ||
!iterStartContent->IsHTMLElement(nsGkAtoms::area))) {
// Need to do special check in case we're in an imagemap which has multiple
// content nodes per frame, so don't skip over the starting frame.
if (aForward) {
frameTraversal->Next();
} else {
frameTraversal->Prev();
}
frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
}
}
else if (getNextFrame &&
(!iterStartContent ||
!iterStartContent->IsHTMLElement(nsGkAtoms::area))) {
// Need to do special check in case we're in an imagemap which has multiple
// content nodes per frame, so don't skip over the starting frame.
if (aForward)
frameTraversal->Next();
else
frameTraversal->Prev();
}
// Walk frames to find something tabbable matching mCurrentTabIndex
nsIFrame* frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
while (frame) {
nsIContent* currentContent = frame->GetContent();
if (nsDocument::IsWebComponentsEnabled(currentContent)) {
// Try to find the topmost scope owner, since we want to skip the node
// that is not owned by document in frame traversal.
nsIContent* oldTopLevelScopeOwner = currentTopLevelScopeOwner;
if (!aForward || oldTopLevelScopeOwner != currentContent) {
currentTopLevelScopeOwner = GetTopLevelScopeOwner(currentContent);
} else {
currentTopLevelScopeOwner = currentContent;
}
if (currentTopLevelScopeOwner) {
if (currentTopLevelScopeOwner == oldTopLevelScopeOwner) {
// We're within non-document scope, continue.
do {
if (aForward) {
frameTraversal->Next();
} else {
frameTraversal->Prev();
}
frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
// For the usage of GetPrevContinuation, see the comment
// at the end of while (frame) loop.
} while (frame && frame->GetPrevContinuation());
continue;
}
currentContent = currentTopLevelScopeOwner;
}
}
// For document navigation, check if this element is an open panel. Since
// panels aren't focusable (tabIndex would be -1), we'll just assume that
@ -3092,7 +3561,7 @@ nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
// and root content, so that we only find content within the panel.
// Note also that we pass false for aForDocumentNavigation since we
// want to locate the first content, not the first document.
rv = GetNextTabbableContent(aPresShell, currentContent,
nsresult rv = GetNextTabbableContent(aPresShell, currentContent,
nullptr, currentContent,
true, 1, false, false,
aResultContent);
@ -3103,6 +3572,34 @@ nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
}
}
// As of now, 2018/04/12, sequential focus navigation is still
// in the obsolete Shadow DOM specification.
// http://w3c.github.io/webcomponents/spec/shadow/#sequential-focus-navigation
// "if ELEMENT is focusable, a shadow host, or a slot element,
// append ELEMENT to NAVIGATION-ORDER."
// and later in "For each element ELEMENT in NAVIGATION-ORDER: "
// hosts and slots are handled before other elements.
if (nsDocument::IsWebComponentsEnabled(currentContent) &&
IsHostOrSlot(currentContent)) {
bool focusableHostSlot;
int32_t tabIndex =
HostOrSlotTabIndexValue(currentContent, &focusableHostSlot);
// Host or slot itself isn't focusable or going backwards, enter its scope.
if ((!aForward || !focusableHostSlot) && tabIndex >= 0 &&
(aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) {
nsIContent* contentToFocus =
GetNextTabbableContentInScope(currentContent, currentContent,
aOriginalStartContent, aForward,
aForward ? 1 : 0, aIgnoreTabIndex,
aForDocumentNavigation,
true /* aSkipOwner */);
if (contentToFocus) {
NS_ADDREF(*aResultContent = contentToFocus);
return NS_OK;
}
}
}
// TabIndex not set defaults to 0 for form elements, anchors and other
// elements that are normally focusable. Tabindex defaults to -1
// for elements that are not normally focusable.
@ -3151,54 +3648,22 @@ nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
// Next, for document navigation, check if this a non-remote child document.
bool checkSubDocument = true;
if (aForDocumentNavigation) {
nsIContent* docRoot = GetRootForChildDocument(currentContent);
if (docRoot) {
// If GetRootForChildDocument returned something then call
// FocusFirst to find the root or first element to focus within
// the child document. If this is a frameset though, skip this and
// fall through to the checkSubDocument block below to iterate into
// the frameset's frames and locate the first focusable frame.
if (!docRoot->IsHTMLElement(nsGkAtoms::frameset)) {
return FocusFirst(docRoot, aResultContent);
}
} else {
// Set checkSubDocument to false, as this was neither a frame
// type element or a child document that was focusable.
checkSubDocument = false;
}
if (aForDocumentNavigation &&
TryDocumentNavigation(currentContent, &checkSubDocument,
aResultContent)) {
return NS_OK;
}
if (checkSubDocument) {
// found a node with a matching tab index. Check if it is a child
// frame. If so, navigate into the child frame instead.
nsIDocument* doc = currentContent->GetComposedDoc();
NS_ASSERTION(doc, "content not in document");
nsIDocument* subdoc = doc->GetSubDocumentFor(currentContent);
if (subdoc && !subdoc->EventHandlingSuppressed()) {
if (aForward) {
// when tabbing forward into a frame, return the root
// frame so that the canvas becomes focused.
nsCOMPtr<nsPIDOMWindowOuter> subframe = subdoc->GetWindow();
if (subframe) {
*aResultContent = GetRootForFocus(subframe, subdoc, false, true);
if (*aResultContent) {
NS_ADDREF(*aResultContent);
return NS_OK;
}
}
}
Element* rootElement = subdoc->GetRootElement();
nsIPresShell* subShell = subdoc->GetShell();
if (rootElement && subShell) {
rv = GetNextTabbableContent(subShell, rootElement,
aOriginalStartContent, rootElement,
aForward, (aForward ? 1 : 0),
false, aForDocumentNavigation, aResultContent);
NS_ENSURE_SUCCESS(rv, rv);
if (*aResultContent)
return NS_OK;
}
if (TryToMoveFocusToSubDocument(currentContent,
aOriginalStartContent,
aForward,
aForDocumentNavigation,
aResultContent)) {
MOZ_ASSERT(*aResultContent);
return NS_OK;
}
// otherwise, use this as the next content node to tab to, unless
// this was the element we started on. This would happen for
@ -3269,6 +3734,77 @@ nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
return NS_OK;
}
bool
nsFocusManager::TryDocumentNavigation(nsIContent* aCurrentContent,
bool* aCheckSubDocument,
nsIContent** aResultContent)
{
*aCheckSubDocument = true;
nsIContent* docRoot = GetRootForChildDocument(aCurrentContent);
if (docRoot) {
// If GetRootForChildDocument returned something then call
// FocusFirst to find the root or first element to focus within
// the child document. If this is a frameset though, skip this and
// fall through to normal tab navigation to iterate into
// the frameset's frames and locate the first focusable frame.
if (!docRoot->IsHTMLElement(nsGkAtoms::frameset)) {
*aCheckSubDocument = false;
Unused << FocusFirst(docRoot, aResultContent);
return *aResultContent != nullptr;
}
} else {
// Set aCheckSubDocument to false, as this was neither a frame
// type element or a child document that was focusable.
*aCheckSubDocument = false;
}
return false;
}
bool
nsFocusManager::TryToMoveFocusToSubDocument(nsIContent* aCurrentContent,
nsIContent* aOriginalStartContent,
bool aForward,
bool aForDocumentNavigation,
nsIContent** aResultContent)
{
nsIDocument* doc = aCurrentContent->GetComposedDoc();
NS_ASSERTION(doc, "content not in document");
nsIDocument* subdoc = doc->GetSubDocumentFor(aCurrentContent);
if (subdoc && !subdoc->EventHandlingSuppressed()) {
if (aForward) {
// when tabbing forward into a frame, return the root
// frame so that the canvas becomes focused.
nsCOMPtr<nsPIDOMWindowOuter> subframe = subdoc->GetWindow();
if (subframe) {
*aResultContent = GetRootForFocus(subframe, subdoc, false, true);
if (*aResultContent) {
NS_ADDREF(*aResultContent);
return true;
}
}
}
Element* rootElement = subdoc->GetRootElement();
nsIPresShell* subShell = subdoc->GetShell();
if (rootElement && subShell) {
nsresult rv = GetNextTabbableContent(subShell,
rootElement,
aOriginalStartContent,
rootElement,
aForward,
(aForward ? 1 : 0),
false,
aForDocumentNavigation,
aResultContent);
NS_ENSURE_SUCCESS(rv, false);
if (*aResultContent) {
return true;
}
}
}
return false;
}
nsIContent*
nsFocusManager::GetNextTabbableMapArea(bool aForward,
int32_t aCurrentTabIndex,
@ -3315,15 +3851,20 @@ nsFocusManager::GetNextTabIndex(nsIContent* aParent,
bool aForward)
{
int32_t tabIndex, childTabIndex;
StyleChildrenIterator iter(aParent);
if (aForward) {
tabIndex = 0;
for (nsIContent* child = aParent->GetFirstChild();
for (nsIContent* child = iter.GetNextChild();
child;
child = child->GetNextSibling()) {
childTabIndex = GetNextTabIndex(child, aCurrentTabIndex, aForward);
if (childTabIndex > aCurrentTabIndex && childTabIndex != tabIndex) {
tabIndex = (tabIndex == 0 || childTabIndex < tabIndex) ? childTabIndex : tabIndex;
child = iter.GetNextChild()) {
// Skip child's descendants if child is a shadow host, as they are
// in the focus navigation scope owned by child's shadow root
if (!(nsDocument::IsWebComponentsEnabled(aParent) && IsHostOrSlot(child))) {
childTabIndex = GetNextTabIndex(child, aCurrentTabIndex, aForward);
if (childTabIndex > aCurrentTabIndex && childTabIndex != tabIndex) {
tabIndex = (tabIndex == 0 || childTabIndex < tabIndex) ? childTabIndex : tabIndex;
}
}
nsAutoString tabIndexStr;
@ -3337,13 +3878,17 @@ nsFocusManager::GetNextTabIndex(nsIContent* aParent,
}
else { /* !aForward */
tabIndex = 1;
for (nsIContent* child = aParent->GetFirstChild();
for (nsIContent* child = iter.GetNextChild();
child;
child = child->GetNextSibling()) {
childTabIndex = GetNextTabIndex(child, aCurrentTabIndex, aForward);
if ((aCurrentTabIndex == 0 && childTabIndex > tabIndex) ||
(childTabIndex < aCurrentTabIndex && childTabIndex > tabIndex)) {
tabIndex = childTabIndex;
child = iter.GetNextChild()) {
// Skip child's descendants if child is a shadow host, as they are
// in the focus navigation scope owned by child's shadow root
if (!(nsDocument::IsWebComponentsEnabled(aParent) && IsHostOrSlot(child))) {
childTabIndex = GetNextTabIndex(child, aCurrentTabIndex, aForward);
if ((aCurrentTabIndex == 0 && childTabIndex > tabIndex) ||
(childTabIndex < aCurrentTabIndex && childTabIndex > tabIndex)) {
tabIndex = childTabIndex;
}
}
nsAutoString tabIndexStr;

View file

@ -384,6 +384,91 @@ protected:
int32_t aType, bool aNoParentTraversal,
nsIContent** aNextContent);
/**
* Returns scope owner of aContent.
* A scope owner is either a document root, shadow host, or slot.
*/
nsIContent* FindOwner(nsIContent* aContent);
/**
* Retrieve the next tabbable element in scope owned by aOwner, using
* focusability and tabindex to determine the tab order.
*
* aOwner is the owner of scope to search in.
*
* aStartContent is the starting point for this call of this method.
*
* aOriginalStartContent is the initial starting point for sequential
* navigation.
*
* aForward should be true for forward navigation or false for backward
* navigation.
*
* aCurrentTabIndex is the current tabindex.
*
* aIgnoreTabIndex to ignore the current tabindex and find the element
* irrespective or the tab index.
*
* aForDocumentNavigation informs whether we're navigating only through
* documents.
*
* aSkipOwner to skip owner while searching. The flag is set when caller is
* |GetNextTabbableContent| in order to let caller handle owner.
*
* NOTE:
* Consider the method searches downwards in flattened subtree
* rooted at aOwner.
*/
nsIContent* GetNextTabbableContentInScope(nsIContent* aOwner,
nsIContent* aStartContent,
nsIContent* aOriginalStartContent,
bool aForward,
int32_t aCurrentTabIndex,
bool aIgnoreTabIndex,
bool aForDocumentNavigation,
bool aSkipOwner);
/**
* Retrieve the next tabbable element in scope including aStartContent
* and the scope's ancestor scopes, using focusability and tabindex to
* determine the tab order.
*
* aStartOwner is the scope owner of the aStartContent.
*
* aStartContent an in/out paremeter. It as input is the starting point
* for this call of this method; as output it is the shadow host in
* light DOM if the next tabbable element is not found in shadow DOM,
* in order to continue searching in light DOM.
*
* aOriginalStartContent is the initial starting point for sequential
* navigation.
*
* aForward should be true for forward navigation or false for backward
* navigation.
*
* aCurrentTabIndex returns tab index of shadow host in light DOM if the
* next tabbable element is not found in shadow DOM, in order to continue
* searching in light DOM.
*
* aIgnoreTabIndex to ignore the current tabindex and find the element
* irrespective or the tab index.
*
* aForDocumentNavigation informs whether we're navigating only through
* documents.
*
* NOTE:
* Consider the method searches upwards in all shadow host- or slot-rooted
* flattened subtrees that contains aStartContent as non-root, except
* the flattened subtree rooted at shadow host in light DOM.
*/
nsIContent* GetNextTabbableContentInAncestorScopes(nsIContent* aStartOwner,
nsIContent** aStartContent,
nsIContent* aOriginalStartContent,
bool aForward,
int32_t* aCurrentTabIndex,
bool aIgnoreTabIndex,
bool aForDocumentNavigation);
/**
* Retrieve the next tabbable element within a document, using focusability
* and tabindex to determine the tab order. The element is returned in
@ -503,6 +588,16 @@ private:
void SetFocusedWindowInternal(nsPIDOMWindowOuter* aWindow);
bool TryDocumentNavigation(nsIContent* aCurrentContent,
bool* aCheckSubDocument,
nsIContent** aResultContent);
bool TryToMoveFocusToSubDocument(nsIContent* aCurrentContent,
nsIContent* aOriginalStartContent,
bool aForward,
bool aForDocumentNavigation,
nsIContent** aResultContent);
// the currently active and front-most top-most window
nsCOMPtr<nsPIDOMWindowOuter> mActiveWindow;

View file

@ -0,0 +1,903 @@
<html>
<head>
<title>Test for Bug 1453693</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script>
class TestNode extends HTMLElement {
constructor() {
super();
const styles = "<style>:focus{background-color:yellow;}</style>";
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML =
`${styles}<div tabindex='-1'>test node</div> <slot></slot>`;
}}
window.customElements.define('test-node', TestNode);
var lastFocusTarget;
function focusLogger(event) {
lastFocusTarget = event.target;
console.log(event.target + " under " + event.target.parentNode);
event.stopPropagation();
}
function testTabbingThroughShadowDOMWithTabIndexes() {
var anchor = document.createElement("a");
anchor.onfocus = focusLogger;
anchor.href = "#";
anchor.textContent = "in light DOM";
document.body.appendChild(anchor);
var host = document.createElement("div");
document.body.appendChild(host);
var sr = host.attachShadow({mode: "open"});
var shadowAnchor = anchor.cloneNode(false);
shadowAnchor.onfocus = focusLogger;
shadowAnchor.textContent = "in shadow DOM";
sr.appendChild(shadowAnchor);
var shadowInput = document.createElement("input");
shadowInput.onfocus = focusLogger;
shadowInput.tabIndex = 1;
sr.appendChild(shadowInput);
var shadowDate = document.createElement("input");
shadowDate.type = "date";
shadowDate.onfocus = focusLogger;
shadowDate.tabIndex = 1;
sr.appendChild(shadowDate);
var shadowIframe = document.createElement("iframe");
shadowIframe.tabIndex = 1;
sr.appendChild(shadowIframe);
shadowIframe.contentDocument.body.innerHTML = "<input>";
var input = document.createElement("input");
input.onfocus = focusLogger;
input.tabIndex = 1;
document.body.appendChild(input);
var input2 = document.createElement("input");
input2.onfocus = focusLogger;
document.body.appendChild(input2);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input, "Should have focused input element. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, anchor, "Should have focused anchor element. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(shadowIframe.contentDocument.activeElement,
shadowIframe.contentDocument.documentElement,
"Should have focused document element in shadow iframe. (3)");
synthesizeKey("KEY_Tab");
opener.is(shadowIframe.contentDocument.activeElement,
shadowIframe.contentDocument.body.firstChild,
"Should have focused input element in shadow iframe. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input2, "Should have focused input[2] element. (3)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(shadowIframe.contentDocument.activeElement,
shadowIframe.contentDocument.body.firstChild,
"Should have focused input element in shadow iframe. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(shadowIframe.contentDocument.activeElement,
shadowIframe.contentDocument.documentElement,
"Should have focused document element in shadow iframe. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, anchor, "Should have focused anchor element. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input, "Should have focused input element. (4)");
document.body.innerHTML = null;
}
function testTabbingThroughSimpleShadowDOM() {
var anchor = document.createElement("a");
anchor.onfocus = focusLogger;
anchor.href = "#";
anchor.textContent = "in light DOM";
document.body.appendChild(anchor);
anchor.focus();
var host = document.createElement("div");
document.body.appendChild(host);
var sr = host.attachShadow({mode: "open"});
var shadowAnchor = anchor.cloneNode(false);
shadowAnchor.onfocus = focusLogger;
shadowAnchor.textContent = "in shadow DOM";
sr.appendChild(shadowAnchor);
var shadowInput = document.createElement("input");
shadowInput.onfocus = focusLogger;
sr.appendChild(shadowInput);
var hiddenShadowButton = document.createElement("button");
hiddenShadowButton.setAttribute("style", "display: none;");
sr.appendChild(hiddenShadowButton);
var input = document.createElement("input");
input.onfocus = focusLogger;
document.body.appendChild(input);
var input2 = document.createElement("input");
input2.onfocus = focusLogger;
document.body.appendChild(input2);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input, "Should have focused input element.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input2, "Should have focused input[2] element.");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input, "Should have focused input element. (2)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (2)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM. (2)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, anchor, "Should have focused anchor element. (2)");
host.remove();
input.remove();
input2.remove();
}
function testTabbingThroughNestedShadowDOM() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (1)");
var host = document.createElement("div");
host.id = "host";
document.body.appendChild(host);
var sr0 = host.attachShadow({mode: "open"});
sr0.innerHTML = "<button id='button'>X</button><br id='br'><div id='h1'></div><div id='h2'></div>";
var button = sr0.getElementById("button");
button.onfocus = focusLogger;
var h1 = sr0.getElementById("h1");
var sr1 = h1.attachShadow({mode: "open"});
sr1.innerHTML = "h1 <input id='h11' placeholder='click me and press tab'><input id='h12' placeholder='and then tab again'>";
var input11 = sr1.getElementById("h11");
input11.onfocus = focusLogger;
var input12 = sr1.getElementById("h12");
input12.onfocus = focusLogger;
var h2 = sr0.getElementById("h2");
var sr2 = h2.attachShadow({mode: "open"});
sr2.innerHTML = "h2 <input id='h21'><input id='h22'>";
var input21 = sr2.getElementById("h21");
input21.onfocus = focusLogger;
var input22 = sr2.getElementById("h22");
input22.onfocus = focusLogger;
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, button, "[nested shadow] Should have focused button element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input11, "[nested shadow] Should have focused input element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input12, "[nested shadow] Should have focused input element. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input21, "[nested shadow] Should have focused input element. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input22, "[nested shadow] Should have focused input element. (4)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input21, "[nested shadow] Should have focused input element. (5)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input12, "[nested shadow] Should have focused input element. (6)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input11, "[nested shadow] Should have focused input element. (7)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, button, "[nested shadow] Should have focused button element. (8)");
// Back to beginning, outside of Shadow DOM.
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (2)");
host.remove();
}
function testTabbingThroughDisplayContentsHost() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (1)");
var host = document.createElement("div");
host.id = "host";
host.setAttribute("style", "display: contents; border: 1px solid black;");
document.body.appendChild(host);
var sr0 = host.attachShadow({mode: "open"});
sr0.innerHTML = "<input id='shadowInput1'><input id='shadowInput2'>";
var shadowInput1 = sr0.getElementById("shadowInput1");
shadowInput1.onfocus = focusLogger;
var shadowInput2 = sr0.getElementById("shadowInput2");
shadowInput2.onfocus = focusLogger;
var host1 = document.createElement("div");
host1.id = "host";
host1.tabIndex = 0;
host1.setAttribute("style", "display: contents; border: 1px solid black;");
document.body.appendChild(host1);
var sr1 = host1.attachShadow({mode: "open"});
sr1.innerHTML = "<input id='shadowInput1'><input id='shadowInput2'>";
var shadowInput3 = sr1.getElementById("shadowInput1");
shadowInput3.onfocus = focusLogger;
var shadowInput4 = sr1.getElementById("shadowInput2");
shadowInput4.onfocus = focusLogger;
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput1, "Should have focused input element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput2, "Should have focused input element. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput3, "Should have focused input element. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput4, "Should have focused input element. (4)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowInput3, "Should have focused input element. (5)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowInput2, "Should have focused input element. (6)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowInput1, "Should have focused input element. (7)");
// Back to beginning, outside of Shadow DOM.
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (2)");
host.remove();
host1.remove();
}
function testTabbingThroughLightDOMShadowDOMLightDOM() {
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
var host = document.createElement("span");
host.innerHTML = "\n";
host.id = "host";
document.body.appendChild(host);
var sr0 = host.attachShadow({mode: "open"});
sr0.innerHTML = document.getElementById("template").innerHTML;
var p1 = sr0.getElementById("p1");
p1.onfocus = focusLogger;
var p2 = sr0.getElementById("p2");
p2.onfocus = focusLogger;
var p = document.createElement("p");
p.innerHTML = " <a href='#p'>link 1</a> ";
var a = p.firstElementChild;
a.onfocus = focusLogger;
document.body.appendChild(p);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, p1, "Should have focused p1.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, p2, "Should have focused p2.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, a, "Should have focused a.");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, p2, "Should have focused p2.");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, p1, "Should have focused p1.");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
host.remove();
p.remove();
}
function testFocusableHost() {
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
var host = document.createElement("div");
host.id = "host";
host.tabIndex = 0;
host.onfocus = focusLogger;
document.body.appendChild(host);
var slotted = document.createElement("div");
slotted.tabIndex = 0;
slotted.onfocus = focusLogger;
host.appendChild(slotted);
var sr0 = host.attachShadow({mode: "open"});
sr0.appendChild(document.createElement("slot"));
var p = document.createElement("p");
p.innerHTML = " <a href='#p'>link 1</a> ";
var a = p.firstElementChild;
a.onfocus = focusLogger;
document.body.appendChild(p);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, host, "Should have focused host.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, slotted, "Should have focused slotted.");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, a, "Should have focused a.");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, slotted, "Should have focused slotted.");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, host, "Should have focused host.");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
host.remove();
p.remove();
}
function testShiftTabbingThroughFocusableHost() {
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
var host = document.createElement("div");
host.id = "host";
host.tabIndex = 0;
host.onfocus = focusLogger;
document.body.appendChild(host);
var sr = host.attachShadow({mode: "open"});
var shadowButton = document.createElement("button");
shadowButton.innerText = "X";
shadowButton.onfocus = focusLogger;
sr.appendChild(shadowButton);
var shadowInput = document.createElement("input");
shadowInput.onfocus = focusLogger;
sr.appendChild(shadowInput);
sr.appendChild(document.createElement("br"));
var input = document.createElement("input");
input.onfocus = focusLogger;
document.body.appendChild(input);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, host, "Should have focused host element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowButton, "Should have focused button element in shadow DOM. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input, "Should have focused input element. (4)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (5)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowButton, "Should have focused button element in shadow DOM. (6)");
synthesizeKey("KEY_Tab", {shiftKey: true});
// focus is already on host
opener.is(sr.activeElement, null,
"Focus should have left button element in shadow DOM. (7)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
host.remove();
input.remove();
}
function testTabbingThroughNestedSlot() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
var host0 = document.createElement("div");
var sr0 = host0.attachShadow({mode: "open"});
sr0.innerHTML = "<slot></slot>";
document.body.appendChild(host0);
// focusable
var host00 = document.createElement("div");
var sr00 = host00.attachShadow({mode: "open"});
var div00 = document.createElement("div");
div00.tabIndex = 0;
div00.onfocus = focusLogger;
sr00.appendChild(div00);
host0.appendChild(host00);
// not focusable
var host01 = document.createElement("div");
var sr01 = host01.attachShadow({mode: "open"});
sr01.innerHTML = "<div></div>";
host0.appendChild(host01);
// focusable
var host02 = document.createElement("div");
var sr02 = host02.attachShadow({mode: "open"});
var div02 = document.createElement("div");
div02.tabIndex = 0;
div02.onfocus = focusLogger;
sr02.appendChild(div02);
host0.appendChild(host02);
var host1 = document.createElement("div");
var sr1 = host1.attachShadow({mode: "open"});
sr1.innerHTML = "<slot></slot>";
document.body.appendChild(host1);
var host10 = document.createElement("div");
var sr10 = host10.attachShadow({mode: "open"});
sr10.innerHTML = "<slot></slot>";
host1.appendChild(host10);
var input10 = document.createElement("input");
input10.onfocus = focusLogger;
host10.appendChild(input10);
var host11 = document.createElement("div");
var sr11 = host11.attachShadow({mode: "open"});
sr11.innerHTML = "<slot></slot>";
host1.appendChild(host11);
var input11 = document.createElement("input");
input11.onfocus = focusLogger;
host11.appendChild(input11);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, div00, "Should have focused div element in shadow DOM. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, div02, "Should have focused div element in shadow DOM. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input10, "Should have focused input element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input11, "Should have focused button element in shadow DOM. (4)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input10, "Should have focused input element in shadow DOM. (5)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, div02, "Should have focused input element in shadow DOM. (6)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, div00, "Should have focused input element in shadow DOM. (7)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
host0.remove();
host1.remove();
}
function testTabbingThroughSlotInLightDOM() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
var input0 = document.createElement("input");
input0.onfocus = focusLogger;
document.body.appendChild(input0);
var slot1 = document.createElement("slot");
document.body.appendChild(slot1);
var input10 = document.createElement("input");
input10.onfocus = focusLogger;
slot1.appendChild(input10);
var input11 = document.createElement("input");
input11.onfocus = focusLogger;
slot1.appendChild(input11);
var input2 = document.createElement("input");
input2.onfocus = focusLogger;
document.body.appendChild(input2);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input0, "Should have focused input element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input10, "Should have focused input element in slot. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input11, "Should have focused input element in slot. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input2, "Should have focused input element. (4)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input11, "Should have focused input element in slot. (5)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input10, "Should have focused input element in slot. (6)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input0, "Should have focused input element. (7)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
input0.remove();
slot1.remove();
input2.remove();
}
function testTabbingThroughFocusableSlotInLightDOM() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
var slot0 = document.createElement("slot");
slot0.tabIndex = 0;
slot0.setAttribute("style", "display: inline;");
slot0.onfocus = focusLogger;
document.body.appendChild(slot0);
var slot00 = document.createElement("slot");
slot00.tabIndex = 0;
slot00.setAttribute("style", "display: inline;");
slot00.onfocus = focusLogger;
slot0.appendChild(slot00);
var input000 = document.createElement("input");
input000.onfocus = focusLogger;
slot00.appendChild(input000);
var input01 = document.createElement("input");
input01.onfocus = focusLogger;
slot0.appendChild(input01);
var input1 = document.createElement("input");
input1.onfocus = focusLogger;
document.body.appendChild(input1);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, slot0, "Should have focused slot element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, slot00, "Should have focused slot element. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input000, "Should have focused input element in slot. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input01, "Should have focused input element in slot. (4)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input1, "Should have focused input element. (5)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input01, "Should have focused input element in slot. (6)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input000, "Should have focused input element in slot. (7)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, slot00, "Should have focused slot element. (8)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, slot0, "Should have focused slot element. (9)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
slot0.remove();
input1.remove();
}
function testTabbingThroughScrollableShadowDOM() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
var host0 = document.createElement("div");
host0.setAttribute("style", "height: 50px; overflow: auto;");
host0.onfocus = focusLogger;
document.body.appendChild(host0);
var sr0 = host0.attachShadow({mode: "open"});
sr0.innerHTML = `
<style>
div,slot {
height: 30px;
display: block;
overflow: auto;
}
input {
display: block;
}
</style>
`;
var input00 = document.createElement("input");
input00.setAttribute("style", "background-color: red;");
input00.onfocus = focusLogger;
sr0.appendChild(input00);
var container01 = document.createElement("div");
container01.onfocus = focusLogger;
sr0.appendChild(container01);
var input010 = document.createElement("input");
input010.onfocus = focusLogger;
container01.appendChild(input010);
var input011 = document.createElement("input");
input011.onfocus = focusLogger;
container01.appendChild(input011);
var slot02 = document.createElement("slot");
slot02.onfocus = focusLogger;
sr0.appendChild(slot02);
var input020 = document.createElement("input");
input020.setAttribute("style", "display: block;");
input020.onfocus = focusLogger;
host0.appendChild(input020);
var input021 = document.createElement("input");
input021.setAttribute("style", "display: block;");
input021.onfocus = focusLogger;
host0.appendChild(input021);
var input1 = document.createElement("input");
input1.onfocus = focusLogger;
document.body.appendChild(input1);
document.body.offsetLeft;
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, host0, "Should have focused shadow host element. (1)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input00, "Should have focused input element in shadow dom. (2)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, container01, "Should have focused scrollable element in shadow dom. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input010, "Should have focused input element in shadow dom. (4)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input011, "Should have focused input element in shadow dom. (5)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, slot02, "Should have focused slot element in shadow dom. (6)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input020, "Should have focused input element in slot. (7)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input021, "Should have focused input element in slot. (8)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, input1, "Should have focused input element in light dom. (9)");
// Backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input021, "Should have focused input element in slot. (10)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input020, "Should have focused input element in slot. (11)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, slot02, "Should have focused slot element in shadow dom. (12)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input011, "Should have focused input element in shadow dom. (13)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input010, "Should have focused input element in shadow dom. (14)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, container01, "Should have focused scrollable element in shadow dom. (15)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, input00, "Should have focused input element in shadow dom. (16)");
synthesizeKey("KEY_Tab", {shiftKey: true});
// focus is already on host
opener.is(sr0.activeElement, null,
"Focus should have left input element in shadow DOM. (7)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild,
"body's first child should have focus.");
host0.remove();
input1.remove();
}
function testDeeplyNestedShadowTree() {
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
var host1 = document.createElement("test-node");
var lastHost = host1;
for (var i = 0; i < 20; ++i) {
lastHost.appendChild(document.createElement("test-node"));
lastHost = lastHost.firstChild;
}
var input = document.createElement("input");
document.body.appendChild(host1);
document.body.appendChild(input);
document.body.offsetLeft;
// Test shadow tree which doesn't have anything tab-focusable.
host1.shadowRoot.querySelector("div").focus();
synthesizeKey("KEY_Tab");
is(document.activeElement, input, "Should have focused input element.");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
// Same test but with focusable elements in the tree...
var input2 = document.createElement("input");
var host2 = host1.firstChild;
var host3 = host2.firstChild;
host2.insertBefore(input2, host3);
var input3 = document.createElement("input");
lastHost.appendChild(input3);
document.body.offsetLeft;
host3.shadowRoot.querySelector("div").focus();
synthesizeKey("KEY_Tab");
is(document.activeElement, input3, "Should have focused input3 element.");
// ...and backwards
host3.shadowRoot.querySelector("div").focus();
synthesizeKey("KEY_Tab", {shiftKey: true});
is(document.activeElement, input2, "Should have focused input2 element.");
// Remove elements added to body element.
host1.remove();
input.remove();
// Tests expect body.firstChild to have focus.
document.body.firstChild.focus();
}
// Bug 1558393
function testBackwardsTabbingWithSlotsWithoutFocusableContent() {
let first = document.createElement("div");
first.tabIndex = 0;
let host = document.createElement("div");
host.tabIndex = 0;
let second = document.createElement("div");
second.tabIndex = 0;
host.appendChild(document.createTextNode("foo"));
host.attachShadow({ mode: "open" }).innerHTML = `<slot></slot>`;
document.body.appendChild(first);
document.body.appendChild(host);
document.body.appendChild(second);
document.body.offsetLeft;
first.focus();
opener.is(document.activeElement, first, "First light div should have focus");
synthesizeKey("KEY_Tab");
opener.is(document.activeElement, host, "Host should be focused");
synthesizeKey("KEY_Tab");
opener.is(document.activeElement, second, "Second light div should be focused");
// Now backwards
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, host, "Focus should return to host");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(document.activeElement, first, "Focus should return to first light div");
second.remove();
host.remove();
first.remove();
}
function runTest() {
testTabbingThroughShadowDOMWithTabIndexes();
testTabbingThroughSimpleShadowDOM();
testTabbingThroughNestedShadowDOM();
testTabbingThroughDisplayContentsHost();
testTabbingThroughLightDOMShadowDOMLightDOM();
testFocusableHost();
testShiftTabbingThroughFocusableHost();
testTabbingThroughNestedSlot();
testTabbingThroughSlotInLightDOM();
testTabbingThroughFocusableSlotInLightDOM();
testTabbingThroughScrollableShadowDOM();
testDeeplyNestedShadowTree();
testBackwardsTabbingWithSlotsWithoutFocusableContent();
opener.didRunTests();
window.close();
}
function init() {
SimpleTest.waitForFocus(runTest);
}
</script>
<style>
</style>
<template id="template">
<div style="overflow: hidden">
<p tabindex="0" id="p1">component</p>
<p tabindex="0" id="p2">/component</p>
</div>
</template>
</head>
<body onload="init()">
</body>
</html>

View file

@ -129,6 +129,7 @@ support-files =
file_bug1263696_frame_pass.html
file_bug1263696_frame_fail.html
file_bug1274806.html
file_bug1453693.html
file_general_document.html
file_htmlserializer_1.html
file_htmlserializer_1_bodyonly.html
@ -609,6 +610,8 @@ skip-if = toolkit == 'android'
[test_bug1308069.html]
[test_bug1314032.html]
[test_bug1375050.html]
[test_bug1453693.html]
skip-if = os == "mac" # Different tab focus behavior on mac
[test_caretPositionFromPoint.html]
[test_change_policy.html]
[test_classList.html]
@ -648,6 +651,8 @@ skip-if = toolkit == 'android' #bug 904183
[test_fileapi.html]
[test_fileapi_slice.html]
skip-if = (toolkit == 'android') # Android: Bug 775227
[test_focus_shadow_dom_root.html]
skip-if = os == "mac" # Different tab focus behavior on mac
[test_getAttribute_after_createAttribute.html]
[test_getElementById.html]
[test_getTranslationNodes.html]

View file

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1453693
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1453693</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
/** Test for Bug 1453693 **/
SimpleTest.waitForExplicitFinish();
function runTests() {
win = window.open("file_bug1453693.html", "", "width=300, height=300");
}
function didRunTests() {
setTimeout("SimpleTest.finish()");
}
;
</script>
</head>
<body onload="SimpleTest.waitForFocus(runTests);">
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1453693">Mozilla Bug 1453693</a>
</body>
</html>

View file

@ -0,0 +1,36 @@
<!doctype html>
<title>Test for bug 1544826</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<div id="host"><a href="#" id="slotted">This is focusable too</a></div>
<script>
const host = document.getElementById("host");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `
<a id="shadow-1" href="#">This is focusable</a>
<slot></slot>
<a id="shadow-2" href="#">So is this</a>
`;
document.documentElement.remove();
document.appendChild(host);
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
is(document.documentElement, host, "Host is the document element");
host.offsetTop;
synthesizeKey("KEY_Tab");
is(shadow.activeElement.id, "shadow-1", "First link in Shadow DOM is focused");
synthesizeKey("KEY_Tab");
is(document.activeElement.id, "slotted", "Slotted link is focused");
synthesizeKey("KEY_Tab");
is(shadow.activeElement.id, "shadow-2", "Second link in Shadow DOM is focused");
// Now backwards.
synthesizeKey("KEY_Tab", {shiftKey: true});
is(document.activeElement.id, "slotted", "Backwards: Slotted link is focused");
synthesizeKey("KEY_Tab", {shiftKey: true});
is(shadow.activeElement.id, "shadow-1", "Backwards: First slotted link is focused");
SimpleTest.finish();
});
</script>