tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 24dfdded0327f92b4bf2204deeae7cf75d774f1b
parent 7217e527a5570ecd09cb447a3dd653a2e01b484e
Author: Neil Deakin <neil@mozilla.com>
Date:   Tue,  2 Dec 2025 08:08:13 +0000

Bug 1968086, trigger the formautofill field identification from the form fill controller instead of from a separate focus event, and wait for the field identification to complete before showing a popup, r=dimi,credential-management-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D266456

Diffstat:
Mbrowser/extensions/formautofill/api.js | 2+-
Mbrowser/extensions/formautofill/test/browser/browser_email_dropdown.js | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/formautofill/FormAutofillChild.sys.mjs | 219++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mtoolkit/components/passwordmgr/LoginManagerChild.sys.mjs | 8++++----
Mtoolkit/components/satchel/nsFormFillController.cpp | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtoolkit/components/satchel/nsFormFillController.h | 9+++++++++
Mtoolkit/components/satchel/nsIFormFillController.idl | 18++++++++++++++++++
Mtoolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html | 2+-
8 files changed, 298 insertions(+), 111 deletions(-)

diff --git a/browser/extensions/formautofill/api.js b/browser/extensions/formautofill/api.js @@ -147,7 +147,7 @@ this.formautofill = class extends ExtensionAPI { child: { esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs", events: { - focusin: {}, + focusin: { capture: true }, "form-changed": { createActor: false }, "form-submission-detected": { createActor: false }, }, diff --git a/browser/extensions/formautofill/test/browser/browser_email_dropdown.js b/browser/extensions/formautofill/test/browser/browser_email_dropdown.js @@ -7,6 +7,9 @@ const PAGE_URL = // login manager and formautofill providers, that if an address is saved, // that the formautofill popup gets priority over the login manager. +// The first two tests check what happens when the field is focused and the +// popup is manually opened with the keyboard. + add_task(async function test_email_field_is_address_dropdown() { await SpecialPowers.pushPrefEnv({ set: [["signon.rememberSignons", true]], @@ -57,3 +60,64 @@ add_task( ); } ); + +// The next two tests check what happens when the field is focused but the +// popup is not manually opened. + +add_task(async function test_email_field_is_address_dropdown_onfocus() { + // However, if no addresses are saved, show the login manager. + await removeAllRecords(); + + await SpecialPowers.pushPrefEnv({ + set: [["signon.rememberSignons", true]], + }); + // If an address is saved, show the formautofill dropdown. + await setStorage(TEST_ADDRESS_1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_URL }, + async function (browser) { + // Note that at present the popup will appear on focus because + // it could be a login form, even through the address items appear + // in the popup menu. + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("email").focus(); + }); + await runAndWaitForAutocompletePopupOpen(browser, () => {}); + const item = getDisplayedPopupItems(browser)[2]; + + is( + item.getAttribute("ac-value"), + "Manage addresses", + "Address popup should show a valid email suggestion" + ); + + await closePopup(browser); + } + ); +}); + +add_task( + async function test_email_field_shows_login_dropdown_when_no_saved_address_onfocus() { + // However, if no addresses are saved, show the login manager. + await removeAllRecords(); + await BrowserTestUtils.withNewTab( + { gBrowser, url: PAGE_URL }, + async function (browser) { + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("email").focus(); + }); + await runAndWaitForAutocompletePopupOpen(browser, () => {}); + const item = getDisplayedPopupItems(browser)[0]; + + is( + item.getAttribute("ac-value"), + "Manage Passwords", + "Login Manager should be shown" + ); + + await closePopup(browser); + } + ); + } +); diff --git a/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs @@ -31,6 +31,18 @@ ChromeUtils.defineESModuleGetters(lazy, { setTimeout: "resource://gre/modules/Timer.sys.mjs", }); +class FormFillFocusListener { + handleFocus(element) { + let actor = + element.ownerGlobal?.windowGlobalChild?.getActor("FormAutofill"); + return actor?.handleFocus(element); + } + + QueryInterface = ChromeUtils.generateQI(["nsIFormFillFocusListener"]); +} + +let gFormFillFocusListener; + /** * Handles content's interactions for the frame. */ @@ -39,7 +51,7 @@ export class FormAutofillChild extends JSWindowActorChild { * Keep track of autofill handlers that are waiting for the parent process * to send back the identified result. */ - #handlerWaitingForDetectedComplete = new Set(); + #handlerWaitingForDetectedComplete = new Map(); /** * Keep track of handler that are waiting for the @@ -90,92 +102,100 @@ export class FormAutofillChild extends JSWindowActorChild { * is run due to a form change */ onFieldsDetectedComplete(fieldDetails, isUpdate = false) { - if (!fieldDetails.length) { - return; - } + let fieldsDetectedResolver; - const handler = this._fieldDetailsManager.getFormHandlerByRootElementId( - fieldDetails[0].rootElementId - ); - this.#handlerWaitingForDetectedComplete.delete(handler); - - if (isUpdate) { - if (this.#handlerWaitingForFormSubmissionComplete.has(handler)) { - // The form change was detected before the form submission, but was probably initiated - // by it, so don't touch the fieldDetails in this case. + try { + if (!fieldDetails.length) { return; } - handler.updateFormByElement(fieldDetails[0].element); - this._fieldDetailsManager.addFormHandlerByElementEntries(handler); - } - - handler.setIdentifiedFieldDetails(fieldDetails); - handler.setUpDynamicFormChangeObserver(); - let addressFields = []; - let creditcardFields = []; - - handler.fieldDetails.forEach(fd => { - if (lazy.FormAutofillUtils.isAddressField(fd.fieldName)) { - addressFields.push(fd); - } else if (lazy.FormAutofillUtils.isCreditCardField(fd.fieldName)) { - creditcardFields.push(fd); + const handler = this._fieldDetailsManager.getFormHandlerByRootElementId( + fieldDetails[0].rootElementId + ); + fieldsDetectedResolver = + this.#handlerWaitingForDetectedComplete.get(handler); + this.#handlerWaitingForDetectedComplete.delete(handler); + + if (isUpdate) { + if (this.#handlerWaitingForFormSubmissionComplete.has(handler)) { + // The form change was detected before the form submission, but was probably initiated + // by it, so don't touch the fieldDetails in this case. + return; + } + handler.updateFormByElement(fieldDetails[0].element); + this._fieldDetailsManager.addFormHandlerByElementEntries(handler); } - }); - // Bug 1905040. This is only a temporarily workaround for now to skip marking address fields - // autocompletable whenever we detect an address field. We only mark address field when - // it is a valid address section (This is done in the parent) - const addressFieldSet = new Set(addressFields.map(fd => fd.fieldName)); - if ( - addressFieldSet.size < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD - ) { - addressFields = []; - } + handler.setIdentifiedFieldDetails(fieldDetails); + handler.setUpDynamicFormChangeObserver(); - // Inform the autocomplete controller these fields are autofillable - [...addressFields, ...creditcardFields].forEach(fieldDetail => { - this.#markAsAutofillField(fieldDetail); + let addressFields = []; + let creditcardFields = []; + + handler.fieldDetails.forEach(fd => { + if (lazy.FormAutofillUtils.isAddressField(fd.fieldName)) { + addressFields.push(fd); + } else if (lazy.FormAutofillUtils.isCreditCardField(fd.fieldName)) { + creditcardFields.push(fd); + } + }); + // Bug 1905040. This is only a temporarily workaround for now to skip marking address fields + // autocompletable whenever we detect an address field. We only mark address field when + // it is a valid address section (This is done in the parent) + const addressFieldSet = new Set(addressFields.map(fd => fd.fieldName)); if ( - fieldDetail.element == lazy.FormAutofillContent.controlledElement && - !isUpdate + addressFieldSet.size < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD ) { - this.showPopupIfEmpty(fieldDetail.element, fieldDetail.fieldName); + addressFields = []; } - }); - if (isUpdate) { - // The fields detection was re-run because of a form change, this means - // FormAutofillChild already registered its interest in form submissions - // in the initial field detection process - return; - } + // Inform the autocomplete controller these fields are autofillable + [...addressFields, ...creditcardFields].forEach(fieldDetail => { + this.#markAsAutofillField(fieldDetail); - // Do not need to listen to form submission event because if the address fields do not contain - // 'street-address' or `address-linx`, we will not save the address. - if ( - creditcardFields.length || - (addressFields.length && - [ - "street-address", - "address-line1", - "address-line2", - "address-line3", - ].some(fieldName => addressFieldSet.has(fieldName))) - ) { - this.manager - .getActor("FormHandler") - .registerFormSubmissionInterest(this, { - includesFormRemoval: lazy.FormAutofill.captureOnFormRemoval, - includesPageNavigation: lazy.FormAutofill.captureOnPageNavigation, - }); + if ( + fieldDetail.element == lazy.FormAutofillContent.controlledElement && + !isUpdate + ) { + this.showPopupIfEmpty(fieldDetail.element, fieldDetail.fieldName); + } + }); + + if (isUpdate) { + // The fields detection was re-run because of a form change, this means + // FormAutofillChild already registered its interest in form submissions + // in the initial field detection process + return; + } + + // Do not need to listen to form submission event because if the address fields do not contain + // 'street-address' or `address-linx`, we will not save the address. + if ( + creditcardFields.length || + (addressFields.length && + [ + "street-address", + "address-line1", + "address-line2", + "address-line3", + ].some(fieldName => addressFieldSet.has(fieldName))) + ) { + this.manager + .getActor("FormHandler") + .registerFormSubmissionInterest(this, { + includesFormRemoval: lazy.FormAutofill.captureOnFormRemoval, + includesPageNavigation: lazy.FormAutofill.captureOnPageNavigation, + }); - // TODO (Bug 1901486): Integrate pagehide to FormHandler. - if (!this._hasRegisteredPageHide.has(handler)) { - this.registerPageHide(handler); - this._hasRegisteredPageHide.add(true); + // TODO (Bug 1901486): Integrate pagehide to FormHandler. + if (!this._hasRegisteredPageHide.has(handler)) { + this.registerPageHide(handler); + this._hasRegisteredPageHide.add(true); + } } + } finally { + fieldsDetectedResolver?.(); } } @@ -238,7 +258,7 @@ export class FormAutofillChild extends JSWindowActorChild { // Bail out if the child process is still waiting for the parent to send a // `onFieldsDetectedComplete` or `onFieldsUpdatedComplete` message, // or a form submission is currently still getting processed. - return; + return null; } if (handler.fillOnFormChangeData.isWithinDynamicFormChangeThreshold) { @@ -246,7 +266,7 @@ export class FormAutofillChild extends JSWindowActorChild { // initiated by a user but by the site due to the form change. Bail out here, // because we will receive the form-changed-event anyway and should not process the // field detection here, since this would block the second autofill process. - return; + return null; } // Bail out if there is nothing changed since last time we identified this element @@ -278,18 +298,22 @@ export class FormAutofillChild extends JSWindowActorChild { ) ) { handler.setIdentifiedFieldDetails(detectedFields); - return; + return null; } - this.sendAsyncMessage( - "FormAutofill:OnFieldsDetected", - detectedFields.map(field => field.toVanillaObject()) - ); + return new Promise(resolve => { + this.sendAsyncMessage( + "FormAutofill:OnFieldsDetected", + detectedFields.map(field => field.toVanillaObject()) + ); - // Notify the parent about the newly identified fields because - // the autofill section information is maintained on the parent side. - this.#handlerWaitingForDetectedComplete.add(handler); + // Notify the parent about the newly identified fields because + // the autofill section information is maintained on the parent side. + this.#handlerWaitingForDetectedComplete.set(handler, resolve); + }); } + + return null; } /** @@ -339,7 +363,7 @@ export class FormAutofillChild extends JSWindowActorChild { if (detectedFields.length) { // This actor should receive `onFieldsDetectedComplete`message after // `idenitfyFields` is called - this.#handlerWaitingForDetectedComplete.add(handler); + this.#handlerWaitingForDetectedComplete.set(handler, null); } return detectedFields; } @@ -454,9 +478,22 @@ export class FormAutofillChild extends JSWindowActorChild { switch (evt.type) { case "focusin": { - this.onFocusIn(evt.target); + if (AppConstants.MOZ_GECKOVIEW) { + this.handleFocus(evt.target); + break; + } + + if (!gFormFillFocusListener) { + gFormFillFocusListener = new FormFillFocusListener(); + + const formFillController = Cc[ + "@mozilla.org/satchel/form-fill-controller;1" + ].getService(Ci.nsIFormFillController); + formFillController.addFocusListener(gFormFillFocusListener); + } break; } + case "form-changed": { const { form, changes } = evt.detail; this.onFormChange(form, changes); @@ -474,7 +511,7 @@ export class FormAutofillChild extends JSWindowActorChild { } } - onFocusIn(element) { + handleFocus(element) { const handler = this._fieldDetailsManager.getFormHandler(element); // When autofilling and clearing a field, we focus on the element before modifying the value. // (See FormAutofillHandler.fillFieldValue and FormAutofillHandler.clearFilledFields). @@ -483,7 +520,7 @@ export class FormAutofillChild extends JSWindowActorChild { !lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) || handler?.isAutofillInProgress ) { - return; + return null; } const doc = element.ownerDocument; @@ -494,25 +531,25 @@ export class FormAutofillChild extends JSWindowActorChild { this._hasDOMContentLoadedHandler = true; doc.addEventListener( "DOMContentLoaded", - () => this.onFocusIn(lazy.FormAutofillContent.controlledElement), + () => this.handleFocus(lazy.FormAutofillContent.controlledElement), { once: true } ); } - return; + return null; } if ( AppConstants.MOZ_GECKOVIEW || !lazy.FormAutofillContent.savedFieldNames ) { - this.debug("onFocusIn: savedFieldNames are not known yet"); + this.debug("handleFocus: savedFieldNames are not known yet"); // Init can be asynchronous because we don't need anything from the parent // at this point. this.sendAsyncMessage("FormAutofill:InitStorage"); } - this.identifyFieldsWhenFocused(element); + return this.identifyFieldsWhenFocused(element); } /** @@ -648,7 +685,7 @@ export class FormAutofillChild extends JSWindowActorChild { mergedFields.map(field => field.toVanillaObject()) ); - this.#handlerWaitingForDetectedComplete.add(handler); + this.#handlerWaitingForDetectedComplete.set(handler, null); if ( lazy.FormAutofill.fillOnDynamicFormChanges && diff --git a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs @@ -150,7 +150,7 @@ const observer = { this.handleKeydown(aEvent, field, loginManagerChild, ownerDocument); break; - case "focus": + case "focusin": this.handleFocus(field, docState, aEvent.target); break; @@ -616,7 +616,7 @@ export class LoginFormState { this.generatedPasswordFields.add(passwordField); // blur/focus: listen for focus changes to we can mask/unmask generated passwords - for (let eventType of ["blur", "focus"]) { + for (let eventType of ["blur", "focusin"]) { passwordField.addEventListener(eventType, observer, { capture: true, mozSystemGroup: true, @@ -668,7 +668,7 @@ export class LoginFormState { this.generatedPasswordFields.delete(passwordField); // Remove all the event listeners added in _passwordEditedOrGenerated - for (let eventType of ["blur", "focus"]) { + for (let eventType of ["blur", "focusin"]) { passwordField.removeEventListener(eventType, observer, { capture: true, mozSystemGroup: true, @@ -3113,7 +3113,7 @@ export class LoginManagerChild extends JSWindowActorChild { if (usernameField) { lazy.log("Attaching event listeners to usernameField."); - usernameField.addEventListener("focus", observer); + usernameField.addEventListener("focusin", observer); usernameField.addEventListener("mousedown", observer); } diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp @@ -21,6 +21,7 @@ #include "mozilla/dom/KeyboardEventBinding.h" #include "mozilla/dom/MouseEvent.h" #include "mozilla/dom/PageTransitionEvent.h" +#include "mozilla/dom/Promise-inl.h" #include "mozilla/Logging.h" #include "mozilla/PresShell.h" #include "mozilla/Services.h" @@ -50,7 +51,7 @@ using mozilla::LogLevel; static mozilla::LazyLogModule sLogger("satchel"); NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mFocusedPopup, - mLastListener) + mLastListener, mFocusListeners) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController) @@ -104,6 +105,7 @@ nsFormFillController::~nsFormFillController() { mControlledElement = nullptr; } RemoveForDocument(nullptr); + mFocusPendingPromise = nullptr; } /* static */ @@ -748,7 +750,7 @@ nsFormFillController::HandleEvent(Event* aEvent) { NS_ENSURE_STATE(internalEvent); switch (internalEvent->mMessage) { - case eFocus: + case eFocusIn: return Focus(aEvent); case eMouseDown: return MouseDown(aEvent); @@ -821,7 +823,7 @@ void nsFormFillController::AttachListeners(EventTarget* aEventTarget) { EventListenerManager* elm = aEventTarget->GetOrCreateListenerManager(); NS_ENSURE_TRUE_VOID(elm); - elm->AddEventListenerByType(this, u"focus"_ns, TrustedEventsAtCapture()); + elm->AddEventListenerByType(this, u"focusin"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"blur"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"pagehide"_ns, TrustedEventsAtCapture()); elm->AddEventListenerByType(this, u"mousedown"_ns, TrustedEventsAtCapture()); @@ -892,27 +894,70 @@ nsresult nsFormFillController::HandleFocus(Element* aElement) { // multiple input forms and the fact that a mousedown into an already focused // field does not trigger another focus. - if (!HasBeenTypePassword(mControlledElement)) { - return NS_OK; - } + bool shouldShowPopup = false; // If we have not seen a right click yet, just show the popup. - if (mLastRightClickTimeStamp.IsNull()) { - mPasswordPopupAutomaticallyOpened = true; - ShowPopup(); - return NS_OK; + if (mControlledElement) { + if (HasBeenTypePassword(mControlledElement)) { + if (mLastRightClickTimeStamp.IsNull()) { + mPasswordPopupAutomaticallyOpened = true; + shouldShowPopup = true; + } else { + uint64_t timeDiff = + (TimeStamp::Now() - mLastRightClickTimeStamp).ToMilliseconds(); + if (timeDiff > mFocusAfterRightClickThreshold) { + shouldShowPopup = true; + } + } + } + } + + // Some handlers, such as the form fill component need time to identify + // which fields match which field types, so ask them to provide a promise + // which will resolve when this task is complete. This allows the + // autocomplete popup to be delayed until the field type is known. Note + // that this only handles popups that open when a field is focused; popups + // opened via, for example, a user keyboard action do not wait for this + // promise. + for (uint32_t idx = 0; idx < mFocusListeners.Length(); idx++) { + RefPtr<Promise> promise; + nsCOMPtr<nsIFormFillFocusListener> formFillFocus = mFocusListeners[idx]; + formFillFocus->HandleFocus(aElement, getter_AddRefs(promise)); + if (!mFocusPendingPromise && promise && + promise->State() == Promise::PromiseState::Pending) { + // Cache the promise. If some other handler calls ShowPopup() + // it will also need to wait on this promise. + mFocusPendingPromise = promise; + WaitForPromise(shouldShowPopup); + return NS_OK; + } } - uint64_t timeDiff = - (TimeStamp::Now() - mLastRightClickTimeStamp).ToMilliseconds(); - if (timeDiff > mFocusAfterRightClickThreshold) { - mPasswordPopupAutomaticallyOpened = true; + if (shouldShowPopup) { ShowPopup(); } return NS_OK; } +void nsFormFillController::WaitForPromise(bool showPopup) { + mFocusPendingPromise->AddCallbacksWithCycleCollectedArgs( + [showPopup](JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { + RefPtr<nsFormFillController> controller = + nsFormFillController::GetSingleton(); + controller->mFocusPendingPromise = nullptr; + if (showPopup) { + controller->ShowPopup(); + } + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + RefPtr<nsFormFillController> controller = + nsFormFillController::GetSingleton(); + controller->mFocusPendingPromise = nullptr; + }); +} + nsresult nsFormFillController::Focus(Event* aEvent) { nsCOMPtr<nsIContent> input = do_QueryInterface(aEvent->GetComposedTarget()); return HandleFocus(MOZ_KnownLive(Element::FromNodeOrNull(input))); @@ -1068,6 +1113,11 @@ nsresult nsFormFillController::MouseDown(Event* aEvent) { NS_IMETHODIMP nsFormFillController::ShowPopup() { + if (mFocusPendingPromise) { + WaitForPromise(true); + return NS_OK; + } + bool isOpen = false; GetPopupOpen(&isOpen); if (isOpen) { @@ -1106,6 +1156,15 @@ NS_IMETHODIMP nsFormFillController::GetPasswordPopupAutomaticallyOpened( return NS_OK; } +NS_IMETHODIMP +nsFormFillController::AddFocusListener(nsIFormFillFocusListener* aListener) { + if (!mFocusListeners.Contains(aListener)) { + mFocusListeners.AppendElement(aListener); + } + + return NS_OK; +} + void nsFormFillController::StartControllingInput(Element* aElement) { MOZ_LOG(sLogger, LogLevel::Verbose, ("StartControllingInput for %p", aElement)); diff --git a/toolkit/components/satchel/nsFormFillController.h b/toolkit/components/satchel/nsFormFillController.h @@ -7,6 +7,7 @@ #define __nsFormFillController__ #include "mozilla/TimeStamp.h" +#include "mozilla/dom/Promise.h" #include "nsIFormFillController.h" #include "nsIAutoCompleteInput.h" #include "nsIAutoCompleteSearch.h" @@ -14,6 +15,7 @@ #include "nsIAutoCompletePopup.h" #include "nsIDOMEventListener.h" #include "nsCOMPtr.h" +#include "nsCOMArray.h" #include "nsStubMutationObserver.h" #include "nsTHashMap.h" #include "nsInterfaceHashtable.h" @@ -107,6 +109,9 @@ class nsFormFillController final : public nsIFormFillController, bool IsTextControl(nsINode* aNode); + MOZ_CAN_RUN_SCRIPT + void WaitForPromise(bool showPopup); + // members ////////////////////////////////////////// nsCOMPtr<nsIAutoCompleteController> mController; @@ -127,6 +132,10 @@ class nsFormFillController final : public nsIFormFillController, nsTHashMap<nsPtrHashKey<const nsINode>, bool> mAutoCompleteInputs; + nsCOMArray<nsIFormFillFocusListener> mFocusListeners; + + RefPtr<mozilla::dom::Promise> mFocusPendingPromise; + uint16_t mFocusAfterRightClickThreshold; uint32_t mTimeout; uint32_t mMinResultsForPopup; diff --git a/toolkit/components/satchel/nsIFormFillController.idl b/toolkit/components/satchel/nsIFormFillController.idl @@ -11,6 +11,17 @@ webidl Document; webidl Element; webidl Event; +[scriptable, uuid(644ac1f4-2122-498e-87bf-2b01d2e24c1e)] +interface nsIFormFillFocusListener : nsISupports { + /* + * Called when a form field is focused and may be used to delay showing + * autocomplete UI until the operation is complete. + * + * @param element the element that is focused. + */ + Promise handleFocus(in Element element); +}; + /* * nsIFormFillController is an interface for controlling form fill behavior * on HTML documents. Any number of docShells can be controller concurrently. @@ -46,6 +57,13 @@ interface nsIFormFillController : nsISupports * Open the autocomplete popup, if possible. */ [can_run_script] void showPopup(); + + /* + * Add a listener which will be notified when a field is focused. + * + * @param listener the listener to be notified. + */ + void addFocusListener(in nsIFormFillFocusListener listener); }; [scriptable, function, uuid(604419ab-55a0-4831-9eca-1b9e67cc4751)] diff --git a/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html @@ -59,7 +59,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=998893 let field = document.getElementById("field1"); let promiseFieldFocus = new Promise(resolve => { - field.addEventListener("focus", function onFocus() { + field.addEventListener("focusin", function onFocus() { info("field focused"); field.value = "New value"; sendKey("DOWN");