Bug 712130 - Defer autofocus until after frame construction. r?bz
The autofocus attribute on form elements forces layout in CheckIfFocusable.
To avoid unpleasant FOUCs, defer autofocus processing until frames are
constructed in PresShell::Initialize.
Resolve the race between nsAutoFocusEvent running and page load by checking the
readystate at time of event posting. Skip autofocus if the element moved to a
different window in the meantime.
MozReview-Commit-ID: 90jiJYJWmRg
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1577,16 +1577,17 @@ nsDocument::nsDocument(const char* aCont
, mNeedsReleaseAfterStackRefCntRelease(false)
, mMaybeServiceWorkerControlled(false)
#ifdef DEBUG
, mWillReparent(false)
#endif
, mDOMLoadingSet(false)
, mDOMInteractiveSet(false)
, mDOMCompleteSet(false)
+ , mAutoFocusFired(false)
{
SetContentTypeInternal(nsDependentCString(aContentType));
MOZ_LOG(gDocumentLeakPRLog, LogLevel::Debug, ("DOCUMENT %p created", this));
// Start out mLastStyleSheetSet as null, per spec
SetDOMStringToNull(mLastStyleSheetSet);
@@ -9969,16 +9970,143 @@ nsDocument::GetTemplateContentsOwner()
// |doc| is the template contents owner of template elements created
// by |doc|.
doc->mTemplateContentsOwner = doc;
}
return mTemplateContentsOwner;
}
+static already_AddRefed<nsPIDOMWindowOuter>
+FindTopWindowForElement(Element* element)
+{
+ nsIDocument* document = element->OwnerDoc();
+ if (!document) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> window = document->GetWindow();
+ if (!window) {
+ return nullptr;
+ }
+
+ // Trying to find the top window (equivalent to window.top).
+ if (nsCOMPtr<nsPIDOMWindowOuter> top = window->GetTop()) {
+ window = top.forget();
+ }
+ return window.forget();
+}
+
+/**
+ * nsAutoFocusEvent is used to dispatch a focus event for an
+ * nsGenericHTMLFormElement with the autofocus attribute enabled.
+ */
+class nsAutoFocusEvent : public Runnable
+{
+public:
+ explicit nsAutoFocusEvent(already_AddRefed<Element>&& aElement,
+ already_AddRefed<nsPIDOMWindowOuter>&& aTopWindow)
+ : mozilla::Runnable("nsAutoFocusEvent")
+ , mElement(aElement)
+ , mTopWindow(aTopWindow)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ nsCOMPtr<nsPIDOMWindowOuter> currentTopWindow =
+ FindTopWindowForElement(mElement);
+ if (currentTopWindow != mTopWindow) {
+ // The element's top window changed from when the event was queued.
+ // Don't take away focus from an unrelated window.
+ return NS_OK;
+ }
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (!fm) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsIDocument* document = mElement->OwnerDoc();
+
+ // Don't steal focus from the user.
+ if (mTopWindow->GetFocusedNode()) {
+ return NS_OK;
+ }
+
+ // If something is focused in the same document, ignore autofocus.
+ if (!fm->GetFocusedContent() ||
+ fm->GetFocusedContent()->OwnerDoc() != document) {
+ mozilla::ErrorResult rv;
+ mElement->Focus(rv);
+ return rv.StealNSResult();
+ }
+
+ return NS_OK;
+ }
+private:
+ nsCOMPtr<Element> mElement;
+ nsCOMPtr<nsPIDOMWindowOuter> mTopWindow;
+};
+
+void
+nsDocument::SetAutoFocusElement(Element* aAutoFocusElement)
+{
+ if (mAutoFocusFired) {
+ // Too late.
+ return;
+ }
+
+ if (mAutoFocusElement) {
+ // The spec disallows multiple autofocus elements, so we consider only the
+ // first one to preserve the old behavior.
+ return;
+ }
+
+ mAutoFocusElement = do_GetWeakReference(aAutoFocusElement);
+ TriggerAutoFocus();
+}
+
+void
+nsDocument::TriggerAutoFocus()
+{
+ if (mAutoFocusFired) {
+ return;
+ }
+
+ if (!mPresShell || !mPresShell->DidInitialize()) {
+ // Delay autofocus until frames are constructed so that we don't thrash
+ // style and layout calculations.
+ return;
+ }
+
+ nsCOMPtr<Element> autoFocusElement = do_QueryReferent(mAutoFocusElement);
+ if (autoFocusElement && autoFocusElement->OwnerDoc() == this) {
+ mAutoFocusFired = true;
+
+ nsCOMPtr<nsPIDOMWindowOuter> topWindow =
+ FindTopWindowForElement(autoFocusElement);
+ if (!topWindow) {
+ return;
+ }
+
+ // NOTE: This may be removed in the future since the spec technically
+ // allows autofocus after load.
+ nsCOMPtr<nsIDocument> topDoc = topWindow->GetExtantDoc();
+ if (topDoc && topDoc->GetReadyStateEnum() == nsIDocument::READYSTATE_COMPLETE) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> event =
+ new nsAutoFocusEvent(autoFocusElement.forget(), topWindow.forget());
+ nsresult rv = NS_DispatchToCurrentThread(event.forget());
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+}
+
void
nsDocument::SetScrollToRef(nsIURI *aDocumentURI)
{
if (!aDocumentURI) {
return;
}
nsAutoCString ref;
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -808,16 +808,19 @@ public:
virtual nsresult LoadChromeSheetSync(nsIURI* uri, bool isAgentSheet,
RefPtr<mozilla::StyleSheet>* aSheet) override;
virtual nsISupports* GetCurrentContentSink() override;
// Only BlockOnload should call this!
void AsyncBlockOnload();
+ virtual void SetAutoFocusElement(Element* aAutoFocusElement) override;
+ virtual void TriggerAutoFocus() override;
+
virtual void SetScrollToRef(nsIURI *aDocumentURI) override;
virtual void ScrollToRef() override;
virtual void ResetScrolledToRefAlready() override;
virtual void SetChangeScrollPosWhenScrollingToRef(bool aValue) override;
virtual Element* LookupImageElement(const nsAString& aElementId) override;
virtual void MozSetImageElement(const nsAString& aImageElementId,
Element* aElement) override;
@@ -1313,16 +1316,18 @@ private:
// Set if we've found a URL for the current picture
nsString mPreloadPictureFoundSource;
RefPtr<mozilla::dom::DOMImplementation> mDOMImplementation;
RefPtr<nsContentList> mImageMaps;
+ nsWeakPtr mAutoFocusElement;
+
nsCString mScrollToRef;
uint8_t mScrolledToRefAlready : 1;
uint8_t mChangeScrollPosWhenScrollingToRef : 1;
// Tracking for plugins in the document.
nsTHashtable< nsPtrHashKey<nsIObjectLoadingContent> > mPlugins;
RefPtr<mozilla::dom::DocumentTimeline> mDocumentTimeline;
@@ -1363,16 +1368,17 @@ public:
bool mWillReparent;
#endif
private:
void RecordNavigationTiming(ReadyState aReadyState);
bool mDOMLoadingSet : 1;
bool mDOMInteractiveSet : 1;
bool mDOMCompleteSet : 1;
+ bool mAutoFocusFired : 1;
};
class nsDocumentOnStack
{
public:
explicit nsDocumentOnStack(nsDocument* aDoc) : mDoc(aDoc)
{
mDoc->IncreaseStackRefCnt();
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -1594,17 +1594,17 @@ nsFocusManager::CheckIfFocusable(nsICont
nsCOMPtr<nsIDocument> doc = aContent->GetComposedDoc();
// can't focus elements that are not in documents
if (!doc) {
LOGCONTENT("Cannot focus %s because content not in document", aContent)
return nullptr;
}
// Make sure that our frames are up to date while ensuring the presshell is
- // also initialized in case we come from an autofocus event.
+ // also initialized in case we come from a script calling focus() early.
mEventHandlingNeedsFlush = false;
doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames);
nsIPresShell *shell = doc->GetShell();
if (!shell)
return nullptr;
// the root content can always be focused,
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2636,16 +2636,19 @@ public:
*/
mozilla::EventStates GetDocumentState() const
{
return mDocumentState;
}
virtual nsISupports* GetCurrentContentSink() = 0;
+ virtual void SetAutoFocusElement(Element* aAutoFocusElement) = 0;
+ virtual void TriggerAutoFocus() = 0;
+
virtual void SetScrollToRef(nsIURI *aDocumentURI) = 0;
virtual void ScrollToRef() = 0;
virtual void ResetScrolledToRefAlready() = 0;
virtual void SetChangeScrollPosWhenScrollingToRef(bool aValue) = 0;
using mozilla::dom::DocumentOrShadowRoot::GetElementById;
using mozilla::dom::DocumentOrShadowRoot::GetElementsByTagName;
using mozilla::dom::DocumentOrShadowRoot::GetElementsByTagNameNS;
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -107,74 +107,16 @@
#include "mozilla/StyleSetHandleInlines.h"
#include "ReferrerPolicy.h"
#include "mozilla/dom/HTMLLabelElement.h"
#include "mozilla/dom/HTMLInputElement.h"
using namespace mozilla;
using namespace mozilla::dom;
-/**
- * nsAutoFocusEvent is used to dispatch a focus event when a
- * nsGenericHTMLFormElement is binded to the tree with the autofocus attribute
- * enabled.
- */
-class nsAutoFocusEvent : public Runnable
-{
-public:
- explicit nsAutoFocusEvent(nsGenericHTMLFormElement* aElement)
- : mozilla::Runnable("nsAutoFocusEvent")
- , mElement(aElement)
- {
- }
-
- NS_IMETHOD Run() override {
- nsFocusManager* fm = nsFocusManager::GetFocusManager();
- if (!fm) {
- return NS_ERROR_NULL_POINTER;
- }
-
- nsIDocument* document = mElement->OwnerDoc();
-
- nsPIDOMWindowOuter* window = document->GetWindow();
- if (!window) {
- return NS_OK;
- }
-
- // Trying to found the top window (equivalent to window.top).
- if (nsCOMPtr<nsPIDOMWindowOuter> top = window->GetTop()) {
- window = top;
- }
-
- if (window->GetFocusedNode()) {
- return NS_OK;
- }
-
- nsCOMPtr<nsIDocument> topDoc = window->GetExtantDoc();
- if (topDoc && topDoc->GetReadyStateEnum() == nsIDocument::READYSTATE_COMPLETE) {
- return NS_OK;
- }
-
- // If something is focused in the same document, ignore autofocus.
- if (!fm->GetFocusedContent() ||
- fm->GetFocusedContent()->OwnerDoc() != document) {
- mozilla::ErrorResult rv;
- mElement->Focus(rv);
- return rv.StealNSResult();
- }
-
- return NS_OK;
- }
-private:
- // NOTE: nsGenericHTMLFormElement is saved as a nsGenericHTMLElement
- // because AddRef/Release are ambiguous with nsGenericHTMLFormElement
- // and Focus() is declared (and defined) in nsGenericHTMLElement class.
- RefPtr<nsGenericHTMLElement> mElement;
-};
-
NS_IMPL_ADDREF_INHERITED(nsGenericHTMLElement, nsGenericHTMLElementBase)
NS_IMPL_RELEASE_INHERITED(nsGenericHTMLElement, nsGenericHTMLElementBase)
NS_INTERFACE_MAP_BEGIN(nsGenericHTMLElement)
NS_INTERFACE_MAP_ENTRY(nsIDOMHTMLElement)
NS_INTERFACE_MAP_ENTRY(nsIDOMElement)
NS_INTERFACE_MAP_ENTRY(nsIDOMNode)
NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElementBase)
@@ -1881,20 +1823,18 @@ nsGenericHTMLFormElement::BindToTree(nsI
aCompileEventHandlers);
NS_ENSURE_SUCCESS(rv, rv);
// An autofocus event has to be launched if the autofocus attribute is
// specified and the element accept the autofocus attribute. In addition,
// the document should not be already loaded and the "browser.autofocus"
// preference should be 'true'.
if (IsAutofocusable() && HasAttr(kNameSpaceID_None, nsGkAtoms::autofocus) &&
- nsContentUtils::AutoFocusEnabled()) {
- nsCOMPtr<nsIRunnable> event = new nsAutoFocusEvent(this);
- rv = NS_DispatchToCurrentThread(event);
- NS_ENSURE_SUCCESS(rv, rv);
+ nsContentUtils::AutoFocusEnabled() && aDocument) {
+ aDocument->SetAutoFocusElement(this);
}
// If @form is set, the element *has* to be in a document, otherwise it
// wouldn't be possible to find an element with the corresponding id.
// If @form isn't set, the element *has* to have a parent, otherwise it
// wouldn't be possible to find a form ancestor.
// We should not call UpdateFormOwner if none of these conditions are
// fulfilled.
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -1831,16 +1831,18 @@ PresShell::Initialize(nscoord aWidth, ns
NS_ENSURE_STATE(!mHaveShutDown);
// Run the XBL binding constructors for any new frames we've constructed.
// (Do this in a script runner, since our caller might have a script
// blocker on the stack.)
nsContentUtils::AddScriptRunner(new XBLConstructorRunner(mDocument));
}
+ mDocument->TriggerAutoFocus();
+
NS_ASSERTION(rootFrame, "How did that happen?");
// Note: when the frame was created above it had the NS_FRAME_IS_DIRTY bit
// set, but XBL processing could have caused a reflow which clears it.
if (MOZ_LIKELY(rootFrame->GetStateBits() & NS_FRAME_IS_DIRTY)) {
// Unset the DIRTY bits so that FrameNeedsReflow() will work right.
rootFrame->RemoveStateBits(NS_FRAME_IS_DIRTY |
NS_FRAME_HAS_DIRTY_CHILDREN);