mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-08 08:48:39 +09:00
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:
parent
263b719ef8
commit
56193120c5
7 changed files with 1712 additions and 98 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
903
dom/base/test/file_bug1453693.html
Normal file
903
dom/base/test/file_bug1453693.html
Normal 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>
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
32
dom/base/test/test_bug1453693.html
Normal file
32
dom/base/test/test_bug1453693.html
Normal 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>
|
||||
36
dom/base/test/test_focus_shadow_dom_root.html
Normal file
36
dom/base/test/test_focus_shadow_dom_root.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue