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
This commit is contained in:
decltype
2018-01-08 22:35:00 +01:00
parent 8735f74909
commit 402ab7cd6d
6 changed files with 142 additions and 63 deletions

View File

@@ -1582,6 +1582,7 @@ nsDocument::nsDocument(const char* aContentType)
, mDOMLoadingSet(false)
, mDOMInteractiveSet(false)
, mDOMCompleteSet(false)
, mAutoFocusFired(false)
{
SetContentTypeInternal(nsDependentCString(aContentType));
@@ -9444,6 +9445,133 @@ nsDocument::GetTemplateContentsOwner()
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)
{

View File

@@ -804,6 +804,9 @@ public:
// 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;
@@ -1306,6 +1309,8 @@ private:
RefPtr<nsContentList> mImageMaps;
nsWeakPtr mAutoFocusElement;
nsCString mScrollToRef;
uint8_t mScrolledToRefAlready : 1;
uint8_t mChangeScrollPosWhenScrollingToRef : 1;
@@ -1356,6 +1361,7 @@ private:
bool mDOMLoadingSet : 1;
bool mDOMInteractiveSet : 1;
bool mDOMCompleteSet : 1;
bool mAutoFocusFired : 1;
};
class nsDocumentOnStack

View File

@@ -1603,7 +1603,7 @@ nsFocusManager::CheckIfFocusable(nsIContent* aContent, uint32_t aFlags)
}
// 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);

View File

@@ -2637,6 +2637,9 @@ public:
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;

View File

@@ -111,64 +111,6 @@
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)
@@ -1882,10 +1824,8 @@ nsGenericHTMLFormElement::BindToTree(nsIDocument* aDocument,
// 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

View File

@@ -1817,6 +1817,8 @@ PresShell::Initialize(nscoord aWidth, nscoord aHeight)
NS_ENSURE_STATE(!mHaveShutDown);
}
mDocument->TriggerAutoFocus();
NS_ASSERTION(rootFrame, "How did that happen?");
// Note: when the frame was created above it had the NS_FRAME_IS_DIRTY bit