summaryrefslogtreecommitdiffstats
path: root/layout/forms
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/forms/HTMLSelectEventListener.cpp856
-rw-r--r--layout/forms/HTMLSelectEventListener.h103
-rw-r--r--layout/forms/ListMutationObserver.cpp92
-rw-r--r--layout/forms/ListMutationObserver.h67
-rw-r--r--layout/forms/crashtests/1102791.html33
-rw-r--r--layout/forms/crashtests/1140216.html20
-rw-r--r--layout/forms/crashtests/1182414.html17
-rw-r--r--layout/forms/crashtests/1212688.html27
-rw-r--r--layout/forms/crashtests/1228670.xhtml7
-rw-r--r--layout/forms/crashtests/1279354.html21
-rw-r--r--layout/forms/crashtests/1388230-1.html3
-rw-r--r--layout/forms/crashtests/1388230-2.html1
-rw-r--r--layout/forms/crashtests/1405830.html19
-rw-r--r--layout/forms/crashtests/1418477.html4
-rw-r--r--layout/forms/crashtests/1432853.html8
-rw-r--r--layout/forms/crashtests/1460787-1.html7
-rw-r--r--layout/forms/crashtests/1464165-1.html14
-rw-r--r--layout/forms/crashtests/1471157.html11
-rw-r--r--layout/forms/crashtests/1488219.html26
-rw-r--r--layout/forms/crashtests/1600207.html9
-rw-r--r--layout/forms/crashtests/1600367.html8
-rw-r--r--layout/forms/crashtests/1617753.html21
-rw-r--r--layout/forms/crashtests/166750-1.html14
-rw-r--r--layout/forms/crashtests/1679471.html8
-rw-r--r--layout/forms/crashtests/1690166-1.html19
-rw-r--r--layout/forms/crashtests/1690166-2.html19
-rw-r--r--layout/forms/crashtests/1873802-1.html114
-rw-r--r--layout/forms/crashtests/200347-1.html8
-rw-r--r--layout/forms/crashtests/203041-1.html24
-rw-r--r--layout/forms/crashtests/213390-1.html23
-rw-r--r--layout/forms/crashtests/258101-1.html18
-rw-r--r--layout/forms/crashtests/266225-1.html7
-rw-r--r--layout/forms/crashtests/310426-1.xhtml9
-rw-r--r--layout/forms/crashtests/310520-1.xhtml19
-rw-r--r--layout/forms/crashtests/315752-1.xhtml21
-rw-r--r--layout/forms/crashtests/317502-1.xhtml13
-rw-r--r--layout/forms/crashtests/321894.html17
-rw-r--r--layout/forms/crashtests/343510-1.html4
-rw-r--r--layout/forms/crashtests/363696-1.xhtml10
-rw-r--r--layout/forms/crashtests/363696-2.html2
-rw-r--r--layout/forms/crashtests/363696-3.html5
-rw-r--r--layout/forms/crashtests/366205-1.html11
-rw-r--r--layout/forms/crashtests/366537-1.xhtml32
-rw-r--r--layout/forms/crashtests/367587-1.html37
-rw-r--r--layout/forms/crashtests/370703-1.html30
-rw-r--r--layout/forms/crashtests/370940-1.html28
-rw-r--r--layout/forms/crashtests/370967.html13
-rw-r--r--layout/forms/crashtests/378369.html19
-rw-r--r--layout/forms/crashtests/380116-1.xhtml11
-rw-r--r--layout/forms/crashtests/382610-1.html11
-rw-r--r--layout/forms/crashtests/383887-1.html20
-rw-r--r--layout/forms/crashtests/386554-1.html14
-rw-r--r--layout/forms/crashtests/388374-1.xhtml22
-rw-r--r--layout/forms/crashtests/388374-2.html25
-rw-r--r--layout/forms/crashtests/393656-1.xhtml13
-rw-r--r--layout/forms/crashtests/393656-2.xhtml22
-rw-r--r--layout/forms/crashtests/399262.html50
-rw-r--r--layout/forms/crashtests/402852-1.html2
-rw-r--r--layout/forms/crashtests/403148-1.html22
-rw-r--r--layout/forms/crashtests/404118-1.html5
-rw-r--r--layout/forms/crashtests/404123-1.html12
-rw-r--r--layout/forms/crashtests/407066.html1
-rw-r--r--layout/forms/crashtests/451316.html7
-rw-r--r--layout/forms/crashtests/455451-1.html17
-rw-r--r--layout/forms/crashtests/457537-1.html17
-rw-r--r--layout/forms/crashtests/457537-2.html17
-rw-r--r--layout/forms/crashtests/498698-1.html6
-rw-r--r--layout/forms/crashtests/513113-1.html6
-rw-r--r--layout/forms/crashtests/538062-1.xhtml20
-rw-r--r--layout/forms/crashtests/570624-1.html15
-rw-r--r--layout/forms/crashtests/578604-1.html17
-rw-r--r--layout/forms/crashtests/590302-1.xhtml4
-rw-r--r--layout/forms/crashtests/626014.xhtml20
-rw-r--r--layout/forms/crashtests/639733.xhtml26
-rw-r--r--layout/forms/crashtests/669767.html14
-rw-r--r--layout/forms/crashtests/682684-binding.xml4
-rw-r--r--layout/forms/crashtests/682684.xhtml3
-rw-r--r--layout/forms/crashtests/865602.html9
-rw-r--r--layout/forms/crashtests/893331.html9
-rw-r--r--layout/forms/crashtests/893332-1.html10
-rw-r--r--layout/forms/crashtests/944198.html9
-rw-r--r--layout/forms/crashtests/949891.xhtml5
-rw-r--r--layout/forms/crashtests/959311.html17
-rw-r--r--layout/forms/crashtests/960277-2.html14
-rw-r--r--layout/forms/crashtests/997709-1.html5
-rw-r--r--layout/forms/crashtests/crashtests.list80
-rw-r--r--layout/forms/moz.build54
-rw-r--r--layout/forms/nsButtonFrameRenderer.cpp471
-rw-r--r--layout/forms/nsButtonFrameRenderer.h83
-rw-r--r--layout/forms/nsCheckboxRadioFrame.cpp167
-rw-r--r--layout/forms/nsCheckboxRadioFrame.h88
-rw-r--r--layout/forms/nsColorControlFrame.cpp128
-rw-r--r--layout/forms/nsColorControlFrame.h60
-rw-r--r--layout/forms/nsComboboxControlFrame.cpp965
-rw-r--r--layout/forms/nsComboboxControlFrame.h244
-rw-r--r--layout/forms/nsDateTimeControlFrame.cpp198
-rw-r--r--layout/forms/nsDateTimeControlFrame.h66
-rw-r--r--layout/forms/nsFieldSetFrame.cpp938
-rw-r--r--layout/forms/nsFieldSetFrame.h114
-rw-r--r--layout/forms/nsFileControlFrame.cpp422
-rw-r--r--layout/forms/nsFileControlFrame.h137
-rw-r--r--layout/forms/nsGfxButtonControlFrame.cpp178
-rw-r--r--layout/forms/nsGfxButtonControlFrame.h62
-rw-r--r--layout/forms/nsHTMLButtonControlFrame.cpp394
-rw-r--r--layout/forms/nsHTMLButtonControlFrame.h110
-rw-r--r--layout/forms/nsIFormControlFrame.h41
-rw-r--r--layout/forms/nsISelectControlFrame.h50
-rw-r--r--layout/forms/nsITextControlFrame.h44
-rw-r--r--layout/forms/nsImageControlFrame.cpp151
-rw-r--r--layout/forms/nsListControlFrame.cpp1211
-rw-r--r--layout/forms/nsListControlFrame.h350
-rw-r--r--layout/forms/nsMeterFrame.cpp224
-rw-r--r--layout/forms/nsMeterFrame.h77
-rw-r--r--layout/forms/nsNumberControlFrame.cpp175
-rw-r--r--layout/forms/nsNumberControlFrame.h105
-rw-r--r--layout/forms/nsProgressFrame.cpp250
-rw-r--r--layout/forms/nsProgressFrame.h84
-rw-r--r--layout/forms/nsRangeFrame.cpp780
-rw-r--r--layout/forms/nsRangeFrame.h213
-rw-r--r--layout/forms/nsSearchControlFrame.cpp82
-rw-r--r--layout/forms/nsSearchControlFrame.h68
-rw-r--r--layout/forms/nsSelectsAreaFrame.cpp189
-rw-r--r--layout/forms/nsSelectsAreaFrame.h58
-rw-r--r--layout/forms/nsTextControlFrame.cpp1325
-rw-r--r--layout/forms/nsTextControlFrame.h358
-rw-r--r--layout/forms/test/bug287446_subframe.html38
-rw-r--r--layout/forms/test/bug477700_subframe.html39
-rw-r--r--layout/forms/test/bug536567_iframe.html9
-rw-r--r--layout/forms/test/bug536567_subframe.html14
-rw-r--r--layout/forms/test/bug564115_window.html10
-rw-r--r--layout/forms/test/chrome.toml8
-rw-r--r--layout/forms/test/mochitest.toml121
-rw-r--r--layout/forms/test/test_bug1111995.html60
-rw-r--r--layout/forms/test/test_bug1301290.html49
-rw-r--r--layout/forms/test/test_bug1305282.html57
-rw-r--r--layout/forms/test/test_bug1327129.html385
-rw-r--r--layout/forms/test/test_bug1529036.html73
-rw-r--r--layout/forms/test/test_bug231389.html55
-rw-r--r--layout/forms/test/test_bug287446.html74
-rw-r--r--layout/forms/test/test_bug345267.html97
-rw-r--r--layout/forms/test/test_bug346043.html65
-rw-r--r--layout/forms/test/test_bug348236.html123
-rw-r--r--layout/forms/test/test_bug353539.html52
-rw-r--r--layout/forms/test/test_bug365410.html132
-rw-r--r--layout/forms/test/test_bug378670.html55
-rw-r--r--layout/forms/test/test_bug402198.html77
-rw-r--r--layout/forms/test/test_bug411236.html71
-rw-r--r--layout/forms/test/test_bug446663.html80
-rw-r--r--layout/forms/test/test_bug476308.html31
-rw-r--r--layout/forms/test/test_bug477531.html65
-rw-r--r--layout/forms/test/test_bug477700.html59
-rw-r--r--layout/forms/test/test_bug534785.html88
-rw-r--r--layout/forms/test/test_bug536567_perwindowpb.html215
-rw-r--r--layout/forms/test/test_bug542914.html115
-rw-r--r--layout/forms/test/test_bug549170.html77
-rw-r--r--layout/forms/test/test_bug562447.html62
-rw-r--r--layout/forms/test/test_bug563642.html82
-rw-r--r--layout/forms/test/test_bug564115.html57
-rw-r--r--layout/forms/test/test_bug571352.html86
-rw-r--r--layout/forms/test/test_bug572406.html48
-rw-r--r--layout/forms/test/test_bug572649.html63
-rw-r--r--layout/forms/test/test_bug595310.html64
-rw-r--r--layout/forms/test/test_bug620936.html35
-rw-r--r--layout/forms/test/test_bug644542.html63
-rw-r--r--layout/forms/test/test_bug672810.html120
-rw-r--r--layout/forms/test/test_bug704049.html50
-rw-r--r--layout/forms/test/test_bug717878_input_scroll.html107
-rw-r--r--layout/forms/test/test_bug869314.html55
-rw-r--r--layout/forms/test/test_bug903715.html81
-rw-r--r--layout/forms/test/test_bug935876.html502
-rw-r--r--layout/forms/test/test_bug957562.html43
-rw-r--r--layout/forms/test/test_bug960277.html29
-rw-r--r--layout/forms/test/test_listcontrol_search.html46
-rw-r--r--layout/forms/test/test_readonly.html58
-rw-r--r--layout/forms/test/test_select_collapsed_page_keys.html43
-rw-r--r--layout/forms/test/test_select_key_navigation_bug1498769.html123
-rw-r--r--layout/forms/test/test_select_key_navigation_bug961363.html131
-rw-r--r--layout/forms/test/test_select_prevent_default.html116
-rw-r--r--layout/forms/test/test_select_reframe.html52
-rw-r--r--layout/forms/test/test_select_vertical.html75
-rw-r--r--layout/forms/test/test_textarea_resize.html102
-rw-r--r--layout/forms/test/test_unstyled_control_height.html72
182 files changed, 17952 insertions, 0 deletions
diff --git a/layout/forms/HTMLSelectEventListener.cpp b/layout/forms/HTMLSelectEventListener.cpp
new file mode 100644
index 0000000000..4f6b1e3561
--- /dev/null
+++ b/layout/forms/HTMLSelectEventListener.cpp
@@ -0,0 +1,856 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "HTMLSelectEventListener.h"
+
+#include "nsListControlFrame.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/Casting.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/ClearOnShutdown.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static bool IsOptionInteractivelySelectable(HTMLSelectElement& aSelect,
+ HTMLOptionElement& aOption,
+ bool aIsCombobox) {
+ if (aSelect.IsOptionDisabled(&aOption)) {
+ return false;
+ }
+ if (!aIsCombobox) {
+ return aOption.GetPrimaryFrame();
+ }
+ // In dropdown mode no options have frames, but we can check whether they
+ // are rendered / not in a display: none subtree.
+ if (!aOption.HasServoData() || Servo_Element_IsDisplayNone(&aOption)) {
+ return false;
+ }
+ // TODO(emilio): This is a bit silly and doesn't match the options that we
+ // show / don't show in the dropdown, but matches the frame construction we
+ // do for multiple selects. For backwards compat also don't allow selecting
+ // options in a display: contents subtree interactively.
+ // test_select_key_navigation_bug1498769.html tests for this and should
+ // probably be changed (and this loop removed) or alternatively
+ // SelectChild.jsm should be changed to match it.
+ for (Element* el = &aOption; el && el != &aSelect;
+ el = el->GetParentElement()) {
+ if (Servo_Element_IsDisplayContents(el)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+namespace mozilla {
+
+static StaticAutoPtr<nsString> sIncrementalString;
+static TimeStamp gLastKeyTime;
+static uintptr_t sLastKeyListener = 0;
+static constexpr int32_t kNothingSelected = -1;
+
+static nsString& GetIncrementalString() {
+ MOZ_ASSERT(sLastKeyListener != 0);
+ if (!sIncrementalString) {
+ sIncrementalString = new nsString();
+ ClearOnShutdown(&sIncrementalString);
+ }
+ return *sIncrementalString;
+}
+
+class MOZ_RAII AutoIncrementalSearchHandler {
+ public:
+ explicit AutoIncrementalSearchHandler(HTMLSelectEventListener& aListener) {
+ if (sLastKeyListener != uintptr_t(&aListener)) {
+ sLastKeyListener = uintptr_t(&aListener);
+ GetIncrementalString().Truncate();
+ // To make it easier to handle time comparisons in the other methods,
+ // initialize gLastKeyTime to a value in the past.
+ gLastKeyTime = TimeStamp::Now() -
+ TimeDuration::FromMilliseconds(
+ StaticPrefs::ui_menu_incremental_search_timeout() * 2);
+ }
+ }
+ ~AutoIncrementalSearchHandler() {
+ if (!mResettingCancelled) {
+ GetIncrementalString().Truncate();
+ }
+ }
+ void CancelResetting() { mResettingCancelled = true; }
+
+ private:
+ bool mResettingCancelled = false;
+};
+
+NS_IMPL_ISUPPORTS(HTMLSelectEventListener, nsIMutationObserver,
+ nsIDOMEventListener)
+
+HTMLSelectEventListener::~HTMLSelectEventListener() {
+ if (sLastKeyListener == uintptr_t(this)) {
+ sLastKeyListener = 0;
+ }
+}
+
+nsListControlFrame* HTMLSelectEventListener::GetListControlFrame() const {
+ if (mIsCombobox) {
+ MOZ_ASSERT(!mElement->GetPrimaryFrame() ||
+ !mElement->GetPrimaryFrame()->IsListControlFrame());
+ return nullptr;
+ }
+ return do_QueryFrame(mElement->GetPrimaryFrame());
+}
+
+int32_t HTMLSelectEventListener::GetEndSelectionIndex() const {
+ if (auto* lf = GetListControlFrame()) {
+ return lf->GetEndSelectionIndex();
+ }
+ // Combobox selects only have one selected index, so the end and start is the
+ // same.
+ return mElement->SelectedIndex();
+}
+
+bool HTMLSelectEventListener::IsOptionInteractivelySelectable(
+ uint32_t aIndex) const {
+ HTMLOptionElement* option = mElement->Item(aIndex);
+ return option &&
+ ::IsOptionInteractivelySelectable(*mElement, *option, mIsCombobox);
+}
+
+//---------------------------------------------------------------------
+// Ok, the entire idea of this routine is to move to the next item that
+// is suppose to be selected. If the item is disabled then we search in
+// the same direction looking for the next item to select. If we run off
+// the end of the list then we start at the end of the list and search
+// backwards until we get back to the original item or an enabled option
+//
+// aStartIndex - the index to start searching from
+// aNewIndex - will get set to the new index if it finds one
+// aNumOptions - the total number of options in the list
+// aDoAdjustInc - the initial index increment / decrement
+// aDoAdjustIncNext - the subsequent index increment/decrement used to search
+// for the next enabled option
+//
+// the aDoAdjustInc could be a "1" for a single item or
+// any number greater representing a page of items
+//
+void HTMLSelectEventListener::AdjustIndexForDisabledOpt(
+ int32_t aStartIndex, int32_t& aNewIndex, int32_t aNumOptions,
+ int32_t aDoAdjustInc, int32_t aDoAdjustIncNext) {
+ // Cannot select anything if there is nothing to select
+ if (aNumOptions == 0) {
+ aNewIndex = kNothingSelected;
+ return;
+ }
+
+ // means we reached the end of the list and now we are searching backwards
+ bool doingReverse = false;
+ // lowest index in the search range
+ int32_t bottom = 0;
+ // highest index in the search range
+ int32_t top = aNumOptions;
+
+ // Start off keyboard options at selectedIndex if nothing else is defaulted to
+ //
+ // XXX Perhaps this should happen for mouse too, to start off shift click
+ // automatically in multiple ... to do this, we'd need to override
+ // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not
+ // sure of the effects, though, so I'm not doing it just yet.
+ int32_t startIndex = aStartIndex;
+ if (startIndex < bottom) {
+ startIndex = mElement->SelectedIndex();
+ }
+ int32_t newIndex = startIndex + aDoAdjustInc;
+
+ // make sure we start off in the range
+ if (newIndex < bottom) {
+ newIndex = 0;
+ } else if (newIndex >= top) {
+ newIndex = aNumOptions - 1;
+ }
+
+ while (true) {
+ // if the newIndex is selectable, we are golden, bail out
+ if (IsOptionInteractivelySelectable(newIndex)) {
+ break;
+ }
+
+ // it WAS disabled, so sart looking ahead for the next enabled option
+ newIndex += aDoAdjustIncNext;
+
+ // well, if we reach end reverse the search
+ if (newIndex < bottom) {
+ if (doingReverse) {
+ return; // if we are in reverse mode and reach the end bail out
+ }
+ // reset the newIndex to the end of the list we hit
+ // reverse the incrementer
+ // set the other end of the list to our original starting index
+ newIndex = bottom;
+ aDoAdjustIncNext = 1;
+ doingReverse = true;
+ top = startIndex;
+ } else if (newIndex >= top) {
+ if (doingReverse) {
+ return; // if we are in reverse mode and reach the end bail out
+ }
+ // reset the newIndex to the end of the list we hit
+ // reverse the incrementer
+ // set the other end of the list to our original starting index
+ newIndex = top - 1;
+ aDoAdjustIncNext = -1;
+ doingReverse = true;
+ bottom = startIndex;
+ }
+ }
+
+ // Looks like we found one
+ aNewIndex = newIndex;
+}
+
+NS_IMETHODIMP
+HTMLSelectEventListener::HandleEvent(dom::Event* aEvent) {
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("keydown")) {
+ return KeyDown(aEvent);
+ }
+ if (eventType.EqualsLiteral("keypress")) {
+ return KeyPress(aEvent);
+ }
+ if (eventType.EqualsLiteral("mousedown")) {
+ if (aEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+ return MouseDown(aEvent);
+ }
+ if (eventType.EqualsLiteral("mouseup")) {
+ // Don't try to honor defaultPrevented here - it's not web compatible.
+ // (bug 1194733)
+ return MouseUp(aEvent);
+ }
+ if (eventType.EqualsLiteral("mousemove")) {
+ // I don't think we want to honor defaultPrevented on mousemove
+ // in general, and it would only prevent highlighting here.
+ return MouseMove(aEvent);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
+ return NS_OK;
+}
+
+void HTMLSelectEventListener::Attach() {
+ mElement->AddSystemEventListener(u"keydown"_ns, this, false, false);
+ mElement->AddSystemEventListener(u"keypress"_ns, this, false, false);
+ mElement->AddSystemEventListener(u"mousedown"_ns, this, false, false);
+ mElement->AddSystemEventListener(u"mouseup"_ns, this, false, false);
+ mElement->AddSystemEventListener(u"mousemove"_ns, this, false, false);
+
+ if (mIsCombobox) {
+ mElement->AddMutationObserver(this);
+ }
+}
+
+void HTMLSelectEventListener::Detach() {
+ mElement->RemoveSystemEventListener(u"keydown"_ns, this, false);
+ mElement->RemoveSystemEventListener(u"keypress"_ns, this, false);
+ mElement->RemoveSystemEventListener(u"mousedown"_ns, this, false);
+ mElement->RemoveSystemEventListener(u"mouseup"_ns, this, false);
+ mElement->RemoveSystemEventListener(u"mousemove"_ns, this, false);
+
+ if (mIsCombobox) {
+ mElement->RemoveMutationObserver(this);
+ if (mElement->OpenInParentProcess()) {
+ nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+ "HTMLSelectEventListener::Detach", [element = mElement] {
+ // Don't hide the dropdown if the element has another frame already,
+ // this prevents closing dropdowns on reframe, see bug 1440506.
+ //
+ // FIXME(emilio): The flush is needed to deal with reframes started
+ // from DOM node removal. But perhaps we can be a bit smarter here.
+ if (!element->IsCombobox() ||
+ !element->GetPrimaryFrame(FlushType::Frames)) {
+ nsContentUtils::DispatchChromeEvent(
+ element->OwnerDoc(), element, u"mozhidedropdown"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+ }
+ }));
+ }
+ }
+}
+
+const uint32_t kMaxDropdownRows = 20; // matches the setting for 4.x browsers
+
+int32_t HTMLSelectEventListener::ItemsPerPage() const {
+ uint32_t size = [&] {
+ if (mIsCombobox) {
+ return kMaxDropdownRows;
+ }
+ if (auto* lf = GetListControlFrame()) {
+ return lf->GetNumDisplayRows();
+ }
+ return mElement->Size();
+ }();
+ if (size <= 1) {
+ return 1;
+ }
+ if (MOZ_UNLIKELY(size > INT32_MAX)) {
+ return INT32_MAX - 1;
+ }
+ return AssertedCast<int32_t>(size - 1u);
+}
+
+void HTMLSelectEventListener::OptionValueMightHaveChanged(
+ nsIContent* aMutatingNode) {
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* acc = GetAccService()) {
+ acc->ComboboxOptionMaybeChanged(mElement->OwnerDoc()->GetPresShell(),
+ aMutatingNode);
+ }
+#endif
+}
+
+void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ if (aElement->IsHTMLElement(nsGkAtoms::option) &&
+ aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::label) {
+ // A11y has its own mutation listener for this so no need to do
+ // OptionValueMightHaveChanged().
+ ComboboxMightHaveChanged();
+ }
+}
+
+void HTMLSelectEventListener::CharacterDataChanged(
+ nsIContent* aContent, const CharacterDataChangeInfo&) {
+ if (nsContentUtils::IsInSameAnonymousTree(mElement, aContent)) {
+ OptionValueMightHaveChanged(aContent);
+ ComboboxMightHaveChanged();
+ }
+}
+
+void HTMLSelectEventListener::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) {
+ OptionValueMightHaveChanged(aChild);
+ ComboboxMightHaveChanged();
+ }
+}
+
+void HTMLSelectEventListener::ContentAppended(nsIContent* aFirstNewContent) {
+ if (nsContentUtils::IsInSameAnonymousTree(mElement, aFirstNewContent)) {
+ OptionValueMightHaveChanged(aFirstNewContent);
+ ComboboxMightHaveChanged();
+ }
+}
+
+void HTMLSelectEventListener::ContentInserted(nsIContent* aChild) {
+ if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) {
+ OptionValueMightHaveChanged(aChild);
+ ComboboxMightHaveChanged();
+ }
+}
+
+void HTMLSelectEventListener::ComboboxMightHaveChanged() {
+ if (nsIFrame* f = mElement->GetPrimaryFrame()) {
+ PresShell* ps = f->PresShell();
+ // nsComoboxControlFrame::Reflow updates the selected text. AddOption /
+ // RemoveOption / etc takes care of keeping the displayed index up to date.
+ ps->FrameNeedsReflow(f, IntrinsicDirty::FrameAncestorsAndDescendants,
+ NS_FRAME_IS_DIRTY);
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* acc = GetAccService()) {
+ acc->ScheduleAccessibilitySubtreeUpdate(ps, mElement);
+ }
+#endif
+ }
+}
+
+void HTMLSelectEventListener::FireOnInputAndOnChange() {
+ RefPtr<HTMLSelectElement> element = mElement;
+ element->UserFinishedInteracting(/* aChanged = */ true);
+}
+
+static void FireDropDownEvent(HTMLSelectElement* aElement, bool aShow,
+ bool aIsSourceTouchEvent) {
+ const auto eventName = [&] {
+ if (aShow) {
+ return aIsSourceTouchEvent ? u"mozshowdropdown-sourcetouch"_ns
+ : u"mozshowdropdown"_ns;
+ }
+ return u"mozhidedropdown"_ns;
+ }();
+ nsContentUtils::DispatchChromeEvent(aElement->OwnerDoc(), aElement, eventName,
+ CanBubble::eYes, Cancelable::eNo);
+}
+
+nsresult HTMLSelectEventListener::MouseDown(dom::Event* aMouseEvent) {
+ NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
+
+ MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
+
+ if (mElement->State().HasState(ElementState::DISABLED)) {
+ return NS_OK;
+ }
+
+ // only allow selection with the left button
+ // if a right button click is on the combobox itself
+ // or on the select when in listbox mode, then let the click through
+ const bool isLeftButton = mouseEvent->Button() == 0;
+ if (!isLeftButton) {
+ return NS_OK;
+ }
+
+ if (mIsCombobox) {
+ uint16_t inputSource = mouseEvent->InputSource();
+ if (mElement->OpenInParentProcess()) {
+ nsCOMPtr<nsIContent> target = do_QueryInterface(aMouseEvent->GetTarget());
+ if (target && target->IsHTMLElement(nsGkAtoms::option)) {
+ return NS_OK;
+ }
+ }
+
+ const bool isSourceTouchEvent =
+ inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+ FireDropDownEvent(mElement, !mElement->OpenInParentProcess(),
+ isSourceTouchEvent);
+ return NS_OK;
+ }
+
+ if (nsListControlFrame* list = GetListControlFrame()) {
+ mButtonDown = true;
+ return list->HandleLeftButtonMouseDown(aMouseEvent);
+ }
+ return NS_OK;
+}
+
+nsresult HTMLSelectEventListener::MouseUp(dom::Event* aMouseEvent) {
+ NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
+
+ MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
+
+ mButtonDown = false;
+
+ if (mElement->State().HasState(ElementState::DISABLED)) {
+ return NS_OK;
+ }
+
+ if (nsListControlFrame* lf = GetListControlFrame()) {
+ lf->CaptureMouseEvents(false);
+ }
+
+ // only allow selection with the left button
+ // if a right button click is on the combobox itself
+ // or on the select when in listbox mode, then let the click through
+ const bool isLeftButton = mouseEvent->Button() == 0;
+ if (!isLeftButton) {
+ return NS_OK;
+ }
+
+ if (nsListControlFrame* lf = GetListControlFrame()) {
+ return lf->HandleLeftButtonMouseUp(aMouseEvent);
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLSelectEventListener::MouseMove(dom::Event* aMouseEvent) {
+ NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
+
+ MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
+
+ if (!mButtonDown) {
+ return NS_OK;
+ }
+
+ if (nsListControlFrame* lf = GetListControlFrame()) {
+ return lf->DragMove(aMouseEvent);
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLSelectEventListener::KeyPress(dom::Event* aKeyEvent) {
+ MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
+
+ if (mElement->State().HasState(ElementState::DISABLED)) {
+ return NS_OK;
+ }
+
+ AutoIncrementalSearchHandler incrementalHandler(*this);
+
+ const WidgetKeyboardEvent* keyEvent =
+ aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
+ MOZ_ASSERT(keyEvent,
+ "DOM event must have WidgetKeyboardEvent for its internal event");
+
+ // Select option with this as the first character
+ // XXX Not I18N compliant
+
+ // Don't do incremental search if the key event has already consumed.
+ if (keyEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+
+ if (keyEvent->IsAlt()) {
+ return NS_OK;
+ }
+
+ // With some keyboard layout, space key causes non-ASCII space.
+ // So, the check in keydown event handler isn't enough, we need to check it
+ // again with keypress event.
+ if (keyEvent->mCharCode != ' ') {
+ mControlSelectMode = false;
+ }
+
+ const bool isControlOrMeta =
+ keyEvent->IsControl()
+#if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK)
+ // Ignore Windows Logo key press in Win/Linux because it's not a usual
+ // modifier for applications. Here wants to check "Accel" like modifier.
+ || keyEvent->IsMeta()
+#endif
+ ;
+ if (isControlOrMeta && keyEvent->mCharCode != ' ') {
+ return NS_OK;
+ }
+
+ // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0.
+ // Therefore, all non-printable keys are not handled after this block.
+ if (!keyEvent->mCharCode) {
+ // Backspace key will delete the last char in the string. Otherwise,
+ // non-printable keypress should reset incremental search.
+ if (keyEvent->mKeyCode == NS_VK_BACK) {
+ incrementalHandler.CancelResetting();
+ if (!GetIncrementalString().IsEmpty()) {
+ GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
+ }
+ aKeyEvent->PreventDefault();
+ } else {
+ // XXX When a select element has focus, even if the key causes nothing,
+ // it might be better to call preventDefault() here because nobody
+ // should expect one of other elements including chrome handles the
+ // key event.
+ }
+ return NS_OK;
+ }
+
+ incrementalHandler.CancelResetting();
+
+ // We ate the key if we got this far.
+ aKeyEvent->PreventDefault();
+
+ // XXX Why don't we check/modify timestamp first?
+
+ // Incremental Search: if time elapsed is below
+ // ui.menu.incremental_search.timeout, append this keystroke to the search
+ // string we will use to find options and start searching at the current
+ // keystroke. Otherwise, Truncate the string if it's been a long time
+ // since our last keypress.
+ if ((keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() >
+ StaticPrefs::ui_menu_incremental_search_timeout()) {
+ // If this is ' ' and we are at the beginning of the string, treat it as
+ // "select this option" (bug 191543)
+ if (keyEvent->mCharCode == ' ') {
+ // Actually process the new index and let the selection code
+ // do the scrolling for us
+ PostHandleKeyEvent(GetEndSelectionIndex(), keyEvent->mCharCode,
+ keyEvent->IsShift(), isControlOrMeta);
+
+ return NS_OK;
+ }
+
+ GetIncrementalString().Truncate();
+ }
+
+ gLastKeyTime = keyEvent->mTimeStamp;
+
+ // Append this keystroke to the search string.
+ char16_t uniChar = ToLowerCase(static_cast<char16_t>(keyEvent->mCharCode));
+ GetIncrementalString().Append(uniChar);
+
+ // See bug 188199, if all letters in incremental string are same, just try to
+ // match the first one
+ nsAutoString incrementalString(GetIncrementalString());
+ uint32_t charIndex = 1, stringLength = incrementalString.Length();
+ while (charIndex < stringLength &&
+ incrementalString[charIndex] == incrementalString[charIndex - 1]) {
+ charIndex++;
+ }
+ if (charIndex == stringLength) {
+ incrementalString.Truncate(1);
+ stringLength = 1;
+ }
+
+ // Determine where we're going to start reading the string
+ // If we have multiple characters to look for, we start looking *at* the
+ // current option. If we have only one character to look for, we start
+ // looking *after* the current option.
+ // Exception: if there is no option selected to start at, we always start
+ // *at* 0.
+ int32_t startIndex = mElement->SelectedIndex();
+ if (startIndex == kNothingSelected) {
+ startIndex = 0;
+ } else if (stringLength == 1) {
+ startIndex++;
+ }
+
+ // now make sure there are options or we are wasting our time
+ RefPtr<dom::HTMLOptionsCollection> options = mElement->Options();
+ uint32_t numOptions = options->Length();
+
+ for (uint32_t i = 0; i < numOptions; ++i) {
+ uint32_t index = (i + startIndex) % numOptions;
+ RefPtr<dom::HTMLOptionElement> optionElement = options->ItemAsOption(index);
+ if (!optionElement || !::IsOptionInteractivelySelectable(
+ *mElement, *optionElement, mIsCombobox)) {
+ continue;
+ }
+
+ nsAutoString text;
+ optionElement->GetRenderedLabel(text);
+ if (!StringBeginsWith(
+ nsContentUtils::TrimWhitespace<
+ nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false),
+ incrementalString, nsCaseInsensitiveStringComparator)) {
+ continue;
+ }
+
+ if (mIsCombobox) {
+ if (optionElement->Selected()) {
+ return NS_OK;
+ }
+ optionElement->SetSelected(true);
+ FireOnInputAndOnChange();
+ return NS_OK;
+ }
+
+ if (nsListControlFrame* lf = GetListControlFrame()) {
+ bool wasChanged =
+ lf->PerformSelection(index, keyEvent->IsShift(), isControlOrMeta);
+ if (!wasChanged) {
+ return NS_OK;
+ }
+ FireOnInputAndOnChange();
+ }
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLSelectEventListener::KeyDown(dom::Event* aKeyEvent) {
+ MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
+
+ if (mElement->State().HasState(ElementState::DISABLED)) {
+ return NS_OK;
+ }
+
+ AutoIncrementalSearchHandler incrementalHandler(*this);
+
+ if (aKeyEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+
+ const WidgetKeyboardEvent* keyEvent =
+ aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
+ MOZ_ASSERT(keyEvent,
+ "DOM event must have WidgetKeyboardEvent for its internal event");
+
+ bool dropDownMenuOnUpDown;
+ bool dropDownMenuOnSpace;
+#ifdef XP_MACOSX
+ dropDownMenuOnUpDown = mIsCombobox && !mElement->OpenInParentProcess();
+ dropDownMenuOnSpace = mIsCombobox && !keyEvent->IsAlt() &&
+ !keyEvent->IsControl() && !keyEvent->IsMeta();
+#else
+ dropDownMenuOnUpDown = mIsCombobox && keyEvent->IsAlt();
+ dropDownMenuOnSpace = mIsCombobox && !mElement->OpenInParentProcess();
+#endif
+ bool withinIncrementalSearchTime =
+ (keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() <=
+ StaticPrefs::ui_menu_incremental_search_timeout();
+ if ((dropDownMenuOnUpDown &&
+ (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN)) ||
+ (dropDownMenuOnSpace && keyEvent->mKeyCode == NS_VK_SPACE &&
+ !withinIncrementalSearchTime)) {
+ FireDropDownEvent(mElement, !mElement->OpenInParentProcess(), false);
+ aKeyEvent->PreventDefault();
+ return NS_OK;
+ }
+ if (keyEvent->IsAlt()) {
+ return NS_OK;
+ }
+
+ // We should not change the selection if the popup is "opened in the parent
+ // process" (even when we're in single-process mode).
+ const bool shouldSelect = !mIsCombobox || !mElement->OpenInParentProcess();
+
+ // now make sure there are options or we are wasting our time
+ RefPtr<dom::HTMLOptionsCollection> options = mElement->Options();
+ uint32_t numOptions = options->Length();
+
+ // this is the new index to set
+ int32_t newIndex = kNothingSelected;
+
+ bool isControlOrMeta =
+ keyEvent->IsControl()
+#if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK)
+ // Ignore Windows Logo key press in Win/Linux because it's not a usual
+ // modifier for applications. Here wants to check "Accel" like modifier.
+ || keyEvent->IsMeta()
+#endif
+ ;
+ // Don't try to handle multiple-select pgUp/pgDown in single-select lists.
+ if (isControlOrMeta && !mElement->Multiple() &&
+ (keyEvent->mKeyCode == NS_VK_PAGE_UP ||
+ keyEvent->mKeyCode == NS_VK_PAGE_DOWN)) {
+ return NS_OK;
+ }
+ if (isControlOrMeta &&
+ (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_LEFT ||
+ keyEvent->mKeyCode == NS_VK_DOWN || keyEvent->mKeyCode == NS_VK_RIGHT ||
+ keyEvent->mKeyCode == NS_VK_HOME || keyEvent->mKeyCode == NS_VK_END)) {
+ // Don't go into multiple-select mode unless this list can handle it.
+ isControlOrMeta = mControlSelectMode = mElement->Multiple();
+ } else if (keyEvent->mKeyCode != NS_VK_SPACE) {
+ mControlSelectMode = false;
+ }
+
+ switch (keyEvent->mKeyCode) {
+ case NS_VK_UP:
+ case NS_VK_LEFT:
+ if (shouldSelect) {
+ AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
+ int32_t(numOptions), -1, -1);
+ }
+ break;
+ case NS_VK_DOWN:
+ case NS_VK_RIGHT:
+ if (shouldSelect) {
+ AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
+ int32_t(numOptions), 1, 1);
+ }
+ break;
+ case NS_VK_RETURN:
+ // If this is single select listbox, Enter key doesn't cause anything.
+ if (!mElement->Multiple()) {
+ return NS_OK;
+ }
+
+ newIndex = GetEndSelectionIndex();
+ break;
+ case NS_VK_PAGE_UP: {
+ if (shouldSelect) {
+ AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
+ int32_t(numOptions), -ItemsPerPage(), -1);
+ }
+ break;
+ }
+ case NS_VK_PAGE_DOWN: {
+ if (shouldSelect) {
+ AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
+ int32_t(numOptions), ItemsPerPage(), 1);
+ }
+ break;
+ }
+ case NS_VK_HOME:
+ if (shouldSelect) {
+ AdjustIndexForDisabledOpt(0, newIndex, int32_t(numOptions), 0, 1);
+ }
+ break;
+ case NS_VK_END:
+ if (shouldSelect) {
+ AdjustIndexForDisabledOpt(int32_t(numOptions) - 1, newIndex,
+ int32_t(numOptions), 0, -1);
+ }
+ break;
+ default: // printable key will be handled by keypress event.
+ incrementalHandler.CancelResetting();
+ return NS_OK;
+ }
+
+ aKeyEvent->PreventDefault();
+
+ // Actually process the new index and let the selection code
+ // do the scrolling for us
+ PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta);
+ return NS_OK;
+}
+
+HTMLOptionElement* HTMLSelectEventListener::GetCurrentOption() const {
+ // The mEndSelectionIndex is what is currently being selected. Use
+ // the selected index if this is kNothingSelected.
+ int32_t endIndex = GetEndSelectionIndex();
+ int32_t focusedIndex =
+ endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex;
+ if (focusedIndex != kNothingSelected) {
+ return mElement->Item(AssertedCast<uint32_t>(focusedIndex));
+ }
+
+ // There is no selected option. Return the first non-disabled option, if any.
+ return GetNonDisabledOptionFrom(0);
+}
+
+HTMLOptionElement* HTMLSelectEventListener::GetNonDisabledOptionFrom(
+ int32_t aFromIndex, int32_t* aFoundIndex) const {
+ const uint32_t length = mElement->Length();
+ for (uint32_t i = std::max(aFromIndex, 0); i < length; ++i) {
+ if (IsOptionInteractivelySelectable(i)) {
+ if (aFoundIndex) {
+ *aFoundIndex = i;
+ }
+ return mElement->Item(i);
+ }
+ }
+ return nullptr;
+}
+
+void HTMLSelectEventListener::PostHandleKeyEvent(int32_t aNewIndex,
+ uint32_t aCharCode,
+ bool aIsShift,
+ bool aIsControlOrMeta) {
+ if (aNewIndex == kNothingSelected) {
+ int32_t endIndex = GetEndSelectionIndex();
+ int32_t focusedIndex =
+ endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex;
+ if (focusedIndex != kNothingSelected) {
+ return;
+ }
+ // No options are selected. In this case the focus ring is on the first
+ // non-disabled option (if any), so we should behave as if that's the option
+ // the user acted on.
+ if (!GetNonDisabledOptionFrom(0, &aNewIndex)) {
+ return;
+ }
+ }
+
+ if (mIsCombobox) {
+ RefPtr<HTMLOptionElement> newOption = mElement->Item(aNewIndex);
+ MOZ_ASSERT(newOption);
+ if (newOption->Selected()) {
+ return;
+ }
+ newOption->SetSelected(true);
+ FireOnInputAndOnChange();
+ return;
+ }
+ if (nsListControlFrame* lf = GetListControlFrame()) {
+ lf->UpdateSelectionAfterKeyEvent(aNewIndex, aCharCode, aIsShift,
+ aIsControlOrMeta, mControlSelectMode);
+ }
+}
+
+} // namespace mozilla
diff --git a/layout/forms/HTMLSelectEventListener.h b/layout/forms/HTMLSelectEventListener.h
new file mode 100644
index 0000000000..452610a217
--- /dev/null
+++ b/layout/forms/HTMLSelectEventListener.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_HTMLSelectEventListener_h
+#define mozilla_HTMLSelectEventListener_h
+
+#include "nsIDOMEventListener.h"
+#include "nsStubMutationObserver.h"
+
+class nsIFrame;
+class nsListControlFrame;
+
+namespace mozilla {
+
+namespace dom {
+class HTMLSelectElement;
+class HTMLOptionElement;
+class Event;
+} // namespace dom
+
+/**
+ * HTMLSelectEventListener
+ * This class is responsible for propagating events to the select element while
+ * it has a frame.
+ * Frames are not refcounted so they can't be used as event listeners.
+ */
+
+class HTMLSelectEventListener final : public nsStubMutationObserver,
+ public nsIDOMEventListener {
+ public:
+ enum class SelectType : uint8_t { Listbox, Combobox };
+ HTMLSelectEventListener(dom::HTMLSelectElement& aElement,
+ SelectType aSelectType)
+ : mElement(&aElement), mIsCombobox(aSelectType == SelectType::Combobox) {
+ Attach();
+ }
+
+ NS_DECL_ISUPPORTS
+
+ // For comboboxes, we need to keep the list up to date when options change.
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+
+ // nsIDOMEventListener
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD HandleEvent(dom::Event*) override;
+
+ void Attach();
+ void Detach();
+
+ dom::HTMLOptionElement* GetCurrentOption() const;
+
+ MOZ_CAN_RUN_SCRIPT void FireOnInputAndOnChange();
+
+ private:
+ // This is always guaranteed to be > 0, but callers want signed integers so we
+ // do the cast for them.
+ int32_t ItemsPerPage() const;
+
+ nsListControlFrame* GetListControlFrame() const;
+
+ MOZ_CAN_RUN_SCRIPT nsresult KeyDown(dom::Event*);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyPress(dom::Event*);
+ MOZ_CAN_RUN_SCRIPT nsresult MouseDown(dom::Event*);
+ MOZ_CAN_RUN_SCRIPT nsresult MouseUp(dom::Event*);
+ MOZ_CAN_RUN_SCRIPT nsresult MouseMove(dom::Event*);
+
+ void AdjustIndexForDisabledOpt(int32_t aStartIndex, int32_t& aNewIndex,
+ int32_t aNumOptions, int32_t aDoAdjustInc,
+ int32_t aDoAdjustIncNext);
+ bool IsOptionInteractivelySelectable(uint32_t aIndex) const;
+ int32_t GetEndSelectionIndex() const;
+
+ MOZ_CAN_RUN_SCRIPT
+ void PostHandleKeyEvent(int32_t aNewIndex, uint32_t aCharCode, bool aIsShift,
+ bool aIsControlOrMeta);
+
+ /**
+ * Return the first non-disabled option starting at aFromIndex (inclusive).
+ * @param aFoundIndex if non-null, set to the index of the returned option
+ */
+ dom::HTMLOptionElement* GetNonDisabledOptionFrom(
+ int32_t aFromIndex, int32_t* aFoundIndex = nullptr) const;
+
+ void ComboboxMightHaveChanged();
+ void OptionValueMightHaveChanged(nsIContent* aMutatingNode);
+
+ ~HTMLSelectEventListener();
+
+ RefPtr<dom::HTMLSelectElement> mElement;
+ const bool mIsCombobox;
+ bool mButtonDown = false;
+ bool mControlSelectMode = false;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_HTMLSelectEventListener_h
diff --git a/layout/forms/ListMutationObserver.cpp b/layout/forms/ListMutationObserver.cpp
new file mode 100644
index 0000000000..13f9e367a0
--- /dev/null
+++ b/layout/forms/ListMutationObserver.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "ListMutationObserver.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+#include "nsIFrame.h"
+
+namespace mozilla {
+NS_IMPL_ISUPPORTS(ListMutationObserver, nsIMutationObserver)
+
+ListMutationObserver::~ListMutationObserver() = default;
+
+void ListMutationObserver::Attach(bool aRepaint) {
+ nsAutoString id;
+ if (InputElement().GetAttr(nsGkAtoms::list_, id)) {
+ Unlink();
+ RefPtr<nsAtom> idAtom = NS_AtomizeMainThread(id);
+ ResetWithID(InputElement(), idAtom);
+ AddObserverIfNeeded();
+ }
+ if (aRepaint) {
+ mOwningElementFrame->InvalidateFrame();
+ }
+}
+
+void ListMutationObserver::AddObserverIfNeeded() {
+ if (auto* list = get()) {
+ if (list->IsHTMLElement(nsGkAtoms::datalist)) {
+ list->AddMutationObserver(this);
+ }
+ }
+}
+
+void ListMutationObserver::RemoveObserverIfNeeded(dom::Element* aList) {
+ if (aList && aList->IsHTMLElement(nsGkAtoms::datalist)) {
+ aList->RemoveMutationObserver(this);
+ }
+}
+
+void ListMutationObserver::Detach() {
+ RemoveObserverIfNeeded();
+ Unlink();
+}
+
+dom::HTMLInputElement& ListMutationObserver::InputElement() const {
+ MOZ_ASSERT(mOwningElementFrame->GetContent()->IsHTMLElement(nsGkAtoms::input),
+ "bad cast");
+ return *static_cast<dom::HTMLInputElement*>(
+ mOwningElementFrame->GetContent());
+}
+
+void ListMutationObserver::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ if (aAttribute == nsGkAtoms::value && aNameSpaceID == kNameSpaceID_None &&
+ aElement->IsHTMLElement(nsGkAtoms::option)) {
+ mOwningElementFrame->InvalidateFrame();
+ }
+}
+
+void ListMutationObserver::CharacterDataChanged(
+ nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
+ mOwningElementFrame->InvalidateFrame();
+}
+
+void ListMutationObserver::ContentAppended(nsIContent* aFirstNewContent) {
+ mOwningElementFrame->InvalidateFrame();
+}
+
+void ListMutationObserver::ContentInserted(nsIContent* aChild) {
+ mOwningElementFrame->InvalidateFrame();
+}
+
+void ListMutationObserver::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ mOwningElementFrame->InvalidateFrame();
+}
+
+void ListMutationObserver::ElementChanged(dom::Element* aFrom,
+ dom::Element* aTo) {
+ IDTracker::ElementChanged(aFrom, aTo);
+ RemoveObserverIfNeeded(aFrom);
+ AddObserverIfNeeded();
+ mOwningElementFrame->InvalidateFrame();
+}
+
+} // namespace mozilla
diff --git a/layout/forms/ListMutationObserver.h b/layout/forms/ListMutationObserver.h
new file mode 100644
index 0000000000..8c81077fb6
--- /dev/null
+++ b/layout/forms/ListMutationObserver.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ListMutationObserver_h
+#define mozilla_ListMutationObserver_h
+
+#include "IDTracker.h"
+#include "nsStubMutationObserver.h"
+
+class nsIFrame;
+
+namespace mozilla {
+
+namespace dom {
+class HTMLInputElement;
+} // namespace dom
+
+/**
+ * ListMutationObserver
+ * This class invalidates paint for the input element's frame when the content
+ * of its @list changes, or when the @list ID identifies a different element. It
+ * does *not* invalidate paint when the @list attribute itself changes.
+ */
+
+class ListMutationObserver final : public nsStubMutationObserver,
+ public dom::IDTracker {
+ public:
+ explicit ListMutationObserver(nsIFrame& aOwningElementFrame,
+ bool aRepaint = false)
+ : mOwningElementFrame(&aOwningElementFrame) {
+ // We can skip invalidating paint if the frame is still being initialized.
+ Attach(aRepaint);
+ }
+
+ NS_DECL_ISUPPORTS
+
+ // We need to invalidate paint when the list or its options change.
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ /**
+ * Triggered when the same @list ID identifies a different element than
+ * before.
+ */
+ void ElementChanged(dom::Element* aFrom, dom::Element* aTo) override;
+
+ void Attach(bool aRepaint = true);
+ void Detach();
+ void AddObserverIfNeeded();
+ void RemoveObserverIfNeeded(dom::Element* aList);
+ void RemoveObserverIfNeeded() { RemoveObserverIfNeeded(get()); }
+ dom::HTMLInputElement& InputElement() const;
+
+ private:
+ ~ListMutationObserver();
+
+ nsIFrame* mOwningElementFrame;
+};
+} // namespace mozilla
+
+#endif // mozilla_ListMutationObserver_h
diff --git a/layout/forms/crashtests/1102791.html b/layout/forms/crashtests/1102791.html
new file mode 100644
index 0000000000..0fd64d7553
--- /dev/null
+++ b/layout/forms/crashtests/1102791.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html class="reftest-paged"><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug 1102791</title>
+ <style type="text/css">
+
+html,body {
+ color:black; background-color:white; font-size:16px; padding:0; margin:0;
+}
+
+button {
+ position: absolute;
+ -moz-appearance: none;
+ background: transparent;
+ padding: 0;
+ border-style:none;
+}
+button::before {
+ position: absolute;
+ content: "::before";
+ width: 10px;
+ height: 200em;
+ border: 1px solid black;
+}
+
+ </style>
+</head>
+<body>
+
+<button></button>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/1140216.html b/layout/forms/crashtests/1140216.html
new file mode 100644
index 0000000000..72612b8b3c
--- /dev/null
+++ b/layout/forms/crashtests/1140216.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+
+input { -moz-appearance: textfield; }
+
+</style>
+<script>
+
+function boom()
+{
+ window.getComputedStyle(x, "::-moz-number-spin-down").getPropertyValue("color");
+}
+
+</script>
+<body onload="boom();">
+<input type="number" id="x">
+</body>
+</html>
diff --git a/layout/forms/crashtests/1182414.html b/layout/forms/crashtests/1182414.html
new file mode 100644
index 0000000000..3aaa0bbfcc
--- /dev/null
+++ b/layout/forms/crashtests/1182414.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-paged">
+<head>
+<meta charset="UTF-8">
+<style type="text/css">
+#menu { position: fixed; left: 0px; top: 0px; }
+</style>
+</head>
+<body>
+ <svg id="canvas" width="2427" height="2295.5" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
+
+<div id="menu">
+ <input id="chooseSize" type="range">
+</div>
+</body>
+</html>
+
diff --git a/layout/forms/crashtests/1212688.html b/layout/forms/crashtests/1212688.html
new file mode 100644
index 0000000000..68262d907f
--- /dev/null
+++ b/layout/forms/crashtests/1212688.html
@@ -0,0 +1,27 @@
+<style type="text/css">
+ optgroup {overflow-x: hidden;}
+</style>
+<select>
+ <optgroup label="optgroup">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+ <option>8</option>
+ <option>9</option>
+ <option>10</option>
+ <option>11</option>
+ <option>12</option>
+ <option>13</option>
+ <option>14</option>
+ <option>15</option>
+ <option>16</option>
+ <option>17</option>
+ <option>18</option>
+ <option>19</option>
+ <option>20</option>
+</optgroup>
+</select>
diff --git a/layout/forms/crashtests/1228670.xhtml b/layout/forms/crashtests/1228670.xhtml
new file mode 100644
index 0000000000..cc8d309c0e
--- /dev/null
+++ b/layout/forms/crashtests/1228670.xhtml
@@ -0,0 +1,7 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <select>
+ <optgroup style="display: list-item; list-style: inside;"></optgroup>
+ </select>
+ </body>
+</html>
diff --git a/layout/forms/crashtests/1279354.html b/layout/forms/crashtests/1279354.html
new file mode 100644
index 0000000000..5013392526
--- /dev/null
+++ b/layout/forms/crashtests/1279354.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html class="reftest-paged"><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug 1279354</title>
+</head>
+<body>
+
+<div style="position:fixed"><progress></progress></div>
+1
+<br style="page-break-after: always">
+2
+<br style="page-break-after: always">
+3
+<br style="page-break-after: always">
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/1388230-1.html b/layout/forms/crashtests/1388230-1.html
new file mode 100644
index 0000000000..6662a6ef01
--- /dev/null
+++ b/layout/forms/crashtests/1388230-1.html
@@ -0,0 +1,3 @@
+<cite contenteditable='true'>
+ <input type='color'/>
+</cite>
diff --git a/layout/forms/crashtests/1388230-2.html b/layout/forms/crashtests/1388230-2.html
new file mode 100644
index 0000000000..630031d750
--- /dev/null
+++ b/layout/forms/crashtests/1388230-2.html
@@ -0,0 +1 @@
+<input contenteditable='true' type='color'>
diff --git a/layout/forms/crashtests/1405830.html b/layout/forms/crashtests/1405830.html
new file mode 100644
index 0000000000..c16eeca86a
--- /dev/null
+++ b/layout/forms/crashtests/1405830.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<style>
+#a { display: -webkit-box }
+* { column-width: 0em }
+</style>
+<script>
+function go() {
+ document.body.appendChild(a);
+}
+</script>
+<body onload=go()>
+<select id="a" multiple=""></select>
+0
+Q
+-
+w
+g
+k
diff --git a/layout/forms/crashtests/1418477.html b/layout/forms/crashtests/1418477.html
new file mode 100644
index 0000000000..0be4732d58
--- /dev/null
+++ b/layout/forms/crashtests/1418477.html
@@ -0,0 +1,4 @@
+<head>
+<meta charset="UTF-8">
+<meta http-equiv='content-language' content='onnect-src http://e/%2e%2f%2f%0B%1Bs%EF\u0073%09'>
+<input type='number' value='9'>
diff --git a/layout/forms/crashtests/1432853.html b/layout/forms/crashtests/1432853.html
new file mode 100644
index 0000000000..bc0735c47f
--- /dev/null
+++ b/layout/forms/crashtests/1432853.html
@@ -0,0 +1,8 @@
+<style>
+.c1 { height: 10ch; position: fixed }
+#a { column-width: 0em }
+.c2 { -webkit-transform-style: preserve-3d }
+</style>
+<details id="a" open="">
+<h4 class="c2">
+<select class="c1">
diff --git a/layout/forms/crashtests/1460787-1.html b/layout/forms/crashtests/1460787-1.html
new file mode 100644
index 0000000000..5b330555e2
--- /dev/null
+++ b/layout/forms/crashtests/1460787-1.html
@@ -0,0 +1,7 @@
+<script>
+document.addEventListener("DOMContentLoaded", function(){
+ document.getElementById('a').style.cssText="padding-inline-end:599352cm";
+});
+</script>
+<fieldset>
+<kbd id='a'>
diff --git a/layout/forms/crashtests/1464165-1.html b/layout/forms/crashtests/1464165-1.html
new file mode 100644
index 0000000000..2de4360f3c
--- /dev/null
+++ b/layout/forms/crashtests/1464165-1.html
@@ -0,0 +1,14 @@
+<html>
+ <head>
+ <script>
+ function start () {
+ try { o1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'select') } catch(e) { }
+ try { o2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'style') } catch(e) { }
+ try { document.documentElement.appendChild(o1) } catch(e) { }
+ try { document.documentElement.appendChild(o2) } catch(e) { }
+ try { o2.sheet.insertRule('* { border-left: green; -moz-padding-start: 5% !important; padding: 171798691; -moz-border-end: solid;', 0) } catch(e) { }
+ }
+ document.addEventListener('DOMContentLoaded', start)
+ </script>
+ </head>
+</html>
diff --git a/layout/forms/crashtests/1471157.html b/layout/forms/crashtests/1471157.html
new file mode 100644
index 0000000000..5cde3d0b84
--- /dev/null
+++ b/layout/forms/crashtests/1471157.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<input type="file">
+<script>
+ onload = function() {
+ var input = document.querySelector("input");
+ console.log(input.offsetWidth); // Force layout flush and hence layout box
+ // creation.
+ input.dispatchEvent(new DragEvent("drop"));
+ }
+</script>
+
diff --git a/layout/forms/crashtests/1488219.html b/layout/forms/crashtests/1488219.html
new file mode 100644
index 0000000000..d10d3ef014
--- /dev/null
+++ b/layout/forms/crashtests/1488219.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<script>
+function doTest(){
+ document.getElementById('c').appendChild(document.getElementById('b').cloneNode(true));
+ window.frames[0].document.body.appendChild(document.getElementById('a'));
+ document.documentElement.removeAttribute('class');
+}
+document.addEventListener("MozReftestInvalidate", doTest);
+</script>
+
+<iframe hidden></iframe>
+<style>
+:last-of-type {
+ column-count:71;
+ float:left;
+}
+</style>
+<bdo id='a'>A</bdo>
+<address>
+<blockquote>
+</address>
+<footer id='b'>
+</footer>
+<input id='c' type='time'>
+</html>
diff --git a/layout/forms/crashtests/1600207.html b/layout/forms/crashtests/1600207.html
new file mode 100644
index 0000000000..a54a0d0c7d
--- /dev/null
+++ b/layout/forms/crashtests/1600207.html
@@ -0,0 +1,9 @@
+<style>
+:not(isindex) {
+ columns: 0px;
+ margin-top: 1%;
+}
+</style>
+<fieldset>
+<output>
+<br></br>
diff --git a/layout/forms/crashtests/1600367.html b/layout/forms/crashtests/1600367.html
new file mode 100644
index 0000000000..7a481f814e
--- /dev/null
+++ b/layout/forms/crashtests/1600367.html
@@ -0,0 +1,8 @@
+<style>
+FIELDSET {
+ border: 35482ex groove ! important;
+ all: unset;
+}
+</style>
+<i style="font-size-adjust: 113;" readonly>
+<fieldset>
diff --git a/layout/forms/crashtests/1617753.html b/layout/forms/crashtests/1617753.html
new file mode 100644
index 0000000000..213c483451
--- /dev/null
+++ b/layout/forms/crashtests/1617753.html
@@ -0,0 +1,21 @@
+<style>
+ LEGEND, FORM {
+ white-space: pre-wrap;
+ }
+
+ LEGEND {
+ max-height: 0em;
+ }
+
+ FORM {
+ column-width: 1px;
+ }
+
+ FIELDSET {
+ column-width: 1em;
+ }
+</style>
+<form id="htmlvar00005" class="class3">
+ <fieldset>
+ <legend>
+ <!-- COMMENT -->
diff --git a/layout/forms/crashtests/166750-1.html b/layout/forms/crashtests/166750-1.html
new file mode 100644
index 0000000000..d873be1b29
--- /dev/null
+++ b/layout/forms/crashtests/166750-1.html
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head><title>Killer</title></head>
+<body>
+ <form style="overflow: auto;">
+ <select style="position: fixed;">
+ <option>First</option>
+ <option>Second</option>
+ <option>Third</option>
+ </select>
+ </form>
+</body>
+</html> \ No newline at end of file
diff --git a/layout/forms/crashtests/1679471.html b/layout/forms/crashtests/1679471.html
new file mode 100644
index 0000000000..8d2bbc8eb0
--- /dev/null
+++ b/layout/forms/crashtests/1679471.html
@@ -0,0 +1,8 @@
+<style>
+* {
+ rotate: 0.8339rad;
+ inset: 84pt -15742cm 8% ! important;
+ position: fixed;
+}
+</style>
+<button>
diff --git a/layout/forms/crashtests/1690166-1.html b/layout/forms/crashtests/1690166-1.html
new file mode 100644
index 0000000000..85e9b56702
--- /dev/null
+++ b/layout/forms/crashtests/1690166-1.html
@@ -0,0 +1,19 @@
+<style>
+body {
+ columns: 0px;
+}
+* {
+ column-width: 95em;
+ break-before: page;
+}
+</style>
+<script>
+window.onload = () => {
+ document.vlinkColor = "x"
+}
+</script>
+<body>
+<canvas></canvas>
+<form style="-webkit-perspective: 16px">
+<fieldset style="position: fixed">
+<legend>x</legend>
diff --git a/layout/forms/crashtests/1690166-2.html b/layout/forms/crashtests/1690166-2.html
new file mode 100644
index 0000000000..c39d53873c
--- /dev/null
+++ b/layout/forms/crashtests/1690166-2.html
@@ -0,0 +1,19 @@
+<style>
+body {
+ columns: 0px;
+}
+* {
+ column-width: 95em;
+ break-before: page;
+}
+</style>
+<script>
+window.onload = () => {
+ document.vlinkColor = "x"
+}
+</script>
+<body>
+<canvas></canvas>
+<form style="-webkit-perspective: 16px">
+<fieldset style="float: left">
+<legend>x</legend>
diff --git a/layout/forms/crashtests/1873802-1.html b/layout/forms/crashtests/1873802-1.html
new file mode 100644
index 0000000000..f5cb28e1fa
--- /dev/null
+++ b/layout/forms/crashtests/1873802-1.html
@@ -0,0 +1,114 @@
+<html>
+<head>
+<script></script>
+<script>
+</script>
+<!-- foo -->
+<!-- foo -->
+<button>jLNJegDr5m(\r74f??</button>
+<details>
+<summary>
+<img></img>
+</summary>
+h45@|V+J)</details>
+<p>
+<title>N=y)sO6</title>
+</p>
+<iframe>B9xX</iframe>
+<style>
+</style>
+<h2>
+<form>
+<object>
+<param></param>
+<param></param>
+</object>
+</form>
+<q>
+<ol>
+<li></li>
+</ol>
+</q>
+</h2>
+<dir>#]76[#&lt;I%!&quot;Rrs.wL</dir>
+<table>
+<tbody>}}</tbody>
+<video>
+<track>rUe</track>
+</video>
+<caption>W9A1.&quot;\84gtu6%d</caption>
+<colgroup>k9</colgroup>
+<pre>6,$.MxUA&lt;F</pre>
+<b>
+<keygen>
+</b>
+</del>
+<select>
+<optgroup>
+<option>Cm]</option>
+<option>
+<basefont></basefont>
+<h3>
+<data>
+<canvas>
+<svg>
+<set />
+<path>
+<animateTransform />
+</path>
+<defs>
+<feFuncA />
+</defs>
+<metadata>
+<meshgradient>
+<feMergeNode>
+<feMergeNode>
+<feFlood />
+<altGlyphItem />
+<clipPath />
+<feDistantLight />
+<solidcolor />
+</feMergeNode>
+<font />
+<discard />
+</feMergeNode>
+<feDropShadow />
+</meshgradient>
+<feDiffuseLighting>
+<fePointLight />
+<fePointLight />
+<feSpotLight />
+</feDiffuseLighting>
+</metadata>
+<rect>
+<discard />
+<animateTransform />
+</rect>
+<text>
+<set />
+<tref>
+<unknown>
+<solidcolor>
+<unknown />
+<pattern>
+<animateColor />
+</pattern>
+</solidcolor>
+</unknown>
+<text />
+<meshrow />
+</tref>
+Text</text>
+<mask />
+<discard />
+<feDistantLight />
+<desc>0R</desc>
+</svg>
+<img></img>
+</canvas>
+</command>
+<pre>
+<video>
+<track>
+<input type="range">
+<!-- foo -->
diff --git a/layout/forms/crashtests/200347-1.html b/layout/forms/crashtests/200347-1.html
new file mode 100644
index 0000000000..b41ec8365a
--- /dev/null
+++ b/layout/forms/crashtests/200347-1.html
@@ -0,0 +1,8 @@
+<html>
+ <body>
+ <fieldset style="width: 300px; position: fixed">
+ <legend>Crash test</legend>
+ <div style="float: right; background-color: orange; border: 1px solid black">Hello, my name is Inigo Montoya.</div> hello world content is the best content around, I love hello world content to death, especially when it wraps; that just gives me the chills. Anything less than hello world content is uncivilized.
+ </fieldset>
+ </body>
+</html>
diff --git a/layout/forms/crashtests/203041-1.html b/layout/forms/crashtests/203041-1.html
new file mode 100644
index 0000000000..32d95b40e6
--- /dev/null
+++ b/layout/forms/crashtests/203041-1.html
@@ -0,0 +1,24 @@
+<html>
+
+<head></head>
+
+<body>
+<form method="post" action="#" enctype="multipart/form-data" name="content">
+
+ <div id="sshot" style="position:absolute; left:0; top:70; width:600;
+visibility:visible">
+ <input type="file" name="sshot-1" size="20">
+ </div>
+
+ <div id="comment" style="position:absolute; left:0; top:70; width:600;
+visibility:hidden"></div>
+
+ <script language="JavaScript">
+ <!--
+ document.documentElement.offsetHeight;
+ document.getElementById('sshot').style.display = 'none';
+ document.getElementById('comment').style.display = 'none';
+ // -->
+ </script>
+</form>
+</body></html> \ No newline at end of file
diff --git a/layout/forms/crashtests/213390-1.html b/layout/forms/crashtests/213390-1.html
new file mode 100644
index 0000000000..0ff94ec542
--- /dev/null
+++ b/layout/forms/crashtests/213390-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
+
+<HTML>
+ <BODY>
+
+ <EMBED>
+
+ <TABLE>
+ <TR>
+ <TD STYLE="FONT: 10px Arial;">
+ <A STYLE="FONT: 11px Arial;">1</A>
+ </TD>
+ </TR>
+ </TABLE>
+
+ <DIV STYLE="POSITION: absolute;">
+ <FORM>
+ <SELECT STYLE="FONT: 11px Arial;">
+ </FORM>
+ </DIV>
+
+ </BODY>
+</HTML>
diff --git a/layout/forms/crashtests/258101-1.html b/layout/forms/crashtests/258101-1.html
new file mode 100644
index 0000000000..245917cbf7
--- /dev/null
+++ b/layout/forms/crashtests/258101-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta content="text/html; charset=ISO-8859-1"
+ http-equiv="content-type">
+ <title>Crash Testcase</title>
+ <script type="text/javascript">
+function crash()
+{
+ var inp = document.getElementById("theinp");
+ inp.type = "file";
+}
+ </script>
+</head>
+<body onload="crash();">
+<input id="theinp" type="text">
+</body>
+</html>
diff --git a/layout/forms/crashtests/266225-1.html b/layout/forms/crashtests/266225-1.html
new file mode 100644
index 0000000000..c714125dd5
--- /dev/null
+++ b/layout/forms/crashtests/266225-1.html
@@ -0,0 +1,7 @@
+<HTML>
+<HEAD>
+</HEAD>
+<BODY>
+<FIELDSET STYLE="float:right; text-indent:999px;">Test</FIELDSET>
+</BODY>
+</HTML>
diff --git a/layout/forms/crashtests/310426-1.xhtml b/layout/forms/crashtests/310426-1.xhtml
new file mode 100644
index 0000000000..683dba679f
--- /dev/null
+++ b/layout/forms/crashtests/310426-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<body>
+
+<select><span style="position: absolute;" /></select>
+
+</body>
+
+</html>
diff --git a/layout/forms/crashtests/310520-1.xhtml b/layout/forms/crashtests/310520-1.xhtml
new file mode 100644
index 0000000000..6e111832a4
--- /dev/null
+++ b/layout/forms/crashtests/310520-1.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<script><![CDATA[
+
+function init()
+{
+ var select = document.getElementsByTagName("select")[0];
+ select.remove();
+}
+
+window.addEventListener("load", init);
+
+]]></script>
+
+
+</head>
+<body><select><input/></select></body>
+</html>
diff --git a/layout/forms/crashtests/315752-1.xhtml b/layout/forms/crashtests/315752-1.xhtml
new file mode 100644
index 0000000000..66d3d793d7
--- /dev/null
+++ b/layout/forms/crashtests/315752-1.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace">
+<head>
+<title>Settings - Unclassified NewsBoard</title>
+
+
+</head>
+<body>
+
+<select name="Language">
+ <option value="" style="clear: right;">
+ Automatic </option>
+ <option value="de" style="clear: right;">
+ <span alt="" style="float: right; margin-top: 1px;"> Deutsch</span> <small>(de)</small> </option>
+ <option value="en" selected="selected" style="clear: right;">
+ <span alt="" style="float: right; margin-top: 1px;"> English</span> <small>(en)</small> </option>
+</select>
+
+</body>
+</html> \ No newline at end of file
diff --git a/layout/forms/crashtests/317502-1.xhtml b/layout/forms/crashtests/317502-1.xhtml
new file mode 100644
index 0000000000..7ef5f0b4c1
--- /dev/null
+++ b/layout/forms/crashtests/317502-1.xhtml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+ <title>Testcase, bug 317502</title>
+</head>
+<body onload="document.getElementsByTagName('input')[0].style.width='200px'">
+
+<input type="file" style="position: fixed" />
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/321894.html b/layout/forms/crashtests/321894.html
new file mode 100644
index 0000000000..0a82645ba4
--- /dev/null
+++ b/layout/forms/crashtests/321894.html
@@ -0,0 +1,17 @@
+<html><head>
+<title>
+Testcase bug 321894 - ASSERTION: RemovedAsPrimaryFrame called after PreDestroy: 'PR_FALSE'
+</title>
+</head>
+<body>
+This shouldn't assert in a debug build
+<span>
+ <script>var x=document.body.offsetHeight;</script>
+ <div>
+ <span style="float:left;">
+ <input type="text">
+ </span>
+ </div>
+</span>
+</body>
+</html>
diff --git a/layout/forms/crashtests/343510-1.html b/layout/forms/crashtests/343510-1.html
new file mode 100644
index 0000000000..00cb94f211
--- /dev/null
+++ b/layout/forms/crashtests/343510-1.html
@@ -0,0 +1,4 @@
+<select style="position: absolute;">
+<meta>
+<option style="position: absolute;">
+<body style="display: block;">
diff --git a/layout/forms/crashtests/363696-1.xhtml b/layout/forms/crashtests/363696-1.xhtml
new file mode 100644
index 0000000000..19f938deba
--- /dev/null
+++ b/layout/forms/crashtests/363696-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+
+<xul:hbox>
+ <input type="file" />
+</xul:hbox>
+
+</body>
+</html> \ No newline at end of file
diff --git a/layout/forms/crashtests/363696-2.html b/layout/forms/crashtests/363696-2.html
new file mode 100644
index 0000000000..9d93edd185
--- /dev/null
+++ b/layout/forms/crashtests/363696-2.html
@@ -0,0 +1,2 @@
+<div style="display: -moz-box;">
+<input type="file">
diff --git a/layout/forms/crashtests/363696-3.html b/layout/forms/crashtests/363696-3.html
new file mode 100644
index 0000000000..e1d38076ed
--- /dev/null
+++ b/layout/forms/crashtests/363696-3.html
@@ -0,0 +1,5 @@
+<html><head></head><body>
+
+<span style="display: -moz-box;"><input type="file"><textarea></textarea></span>
+
+</body></html> \ No newline at end of file
diff --git a/layout/forms/crashtests/366205-1.html b/layout/forms/crashtests/366205-1.html
new file mode 100644
index 0000000000..2a3786afe0
--- /dev/null
+++ b/layout/forms/crashtests/366205-1.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+</head>
+<body onload='var yar=document.getElementById("yar"); yar.parentNode.removeChild(yar); document.getElementById("foo").removeAttribute("multiple");'>
+
+<div id="yar">Clicking the button below should not trigger an assertion</div>
+
+<select id="foo" multiple><option>A</option></select>
+
+</body>
+</html> \ No newline at end of file
diff --git a/layout/forms/crashtests/366537-1.xhtml b/layout/forms/crashtests/366537-1.xhtml
new file mode 100644
index 0000000000..b799cd2ca2
--- /dev/null
+++ b/layout/forms/crashtests/366537-1.xhtml
@@ -0,0 +1,32 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+
+<head>
+<script>
+
+function boom()
+{
+ var fieldset = document.getElementById("fieldset");
+ var h3 = document.getElementById("h3");
+ var legend = document.getElementById("legend");
+
+ fieldset.appendChild(legend);
+ fieldset.appendChild(h3);
+
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+
+<body onload="setTimeout(boom, 30);">
+
+<legend id="legend">legend</legend>
+
+<h3 id="h3">H3</h3>
+
+<select><option>option<fieldset id="fieldset"></fieldset></option></select>
+
+</body>
+
+</html>
diff --git a/layout/forms/crashtests/367587-1.html b/layout/forms/crashtests/367587-1.html
new file mode 100644
index 0000000000..9bffe569ed
--- /dev/null
+++ b/layout/forms/crashtests/367587-1.html
@@ -0,0 +1,37 @@
+<html class="reftest-wait">
+<head>
+
+<style>
+
+#opt1 { display: table-footer-group; }
+#opt1 { visibility: collapse; }
+#opt1 { overflow: hidden scroll; }
+
+#opt2 { display: table-cell; }
+#opt2 { clear: left; }
+
+select { width: 1px; }
+
+</style>
+
+<script>
+
+function boom()
+{
+ document.getElementById("opt2").style.clear = "none";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+</head>
+
+<body onload="setTimeout(boom, 30);">
+
+<select multiple>
+<option id="opt1">A</option>
+<option id="opt2">B</option>
+</select>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/370703-1.html b/layout/forms/crashtests/370703-1.html
new file mode 100644
index 0000000000..b447b180b5
--- /dev/null
+++ b/layout/forms/crashtests/370703-1.html
@@ -0,0 +1,30 @@
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("M").style.width = "6em";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+<body onload="setTimeout(boom, 30);">
+
+<div style="position:absolute; top: 50px; left: 50px; border: 2px solid orange;">
+
+ <select>
+ <option id="M">M</option>
+ </select>
+
+ <br>
+
+ <select style="width: 200%">
+ <option style="visibility: collapse; overflow: auto; display: table-footer-group;">X</option>
+ </select>
+
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/370940-1.html b/layout/forms/crashtests/370940-1.html
new file mode 100644
index 0000000000..ccaeb9643b
--- /dev/null
+++ b/layout/forms/crashtests/370940-1.html
@@ -0,0 +1,28 @@
+<html class="reftest-wait">
+<head>
+<script>
+
+var HTML_NS = "http://www.w3.org/1999/xhtml";
+
+function boom1()
+{
+ var newSpan = document.createElementNS(HTML_NS, "span");
+ document.body.appendChild(newSpan);
+
+ setTimeout(boom2, 30);
+}
+
+function boom2()
+{
+ var newDiv = document.createElementNS(HTML_NS, "div");
+ document.getElementById("s").appendChild(newDiv);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="setTimeout(boom1, 30);">
+ <p>العربي</p>
+ <span id="s"></span><input><select></select><isindex><span></span>
+</body>
+</html>
diff --git a/layout/forms/crashtests/370967.html b/layout/forms/crashtests/370967.html
new file mode 100644
index 0000000000..be7854b41c
--- /dev/null
+++ b/layout/forms/crashtests/370967.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<html>
+<head>
+<title>Untitled</title>
+</head>
+<body>
+Focus the input of the isindex, then do a reload, Mozilla should not crash
+<isindex autofocus>
+</label>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/378369.html b/layout/forms/crashtests/378369.html
new file mode 100644
index 0000000000..90327735ef
--- /dev/null
+++ b/layout/forms/crashtests/378369.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<title>Testcase bug - Crash [@ nsEventListenerManager::FixContextMenuEvent] when firing contextmenu event in display: none iframe</title>
+</head>
+<body>
+This page should not crash Mozilla
+<iframe style="display: none;"></iframe>
+
+<script>
+function docontextmenu(i){
+var doc = window.frames[0].document;
+ var ev = doc.createEvent ('MouseEvents');
+ ev.initMouseEvent('contextmenu', true,true, window, 3,5, 5, 400, 400, 0, 0, 0,0,0,null);
+ doc.dispatchEvent(ev);
+}
+setTimeout(docontextmenu,0);
+</script>
+</body>
+</html>
diff --git a/layout/forms/crashtests/380116-1.xhtml b/layout/forms/crashtests/380116-1.xhtml
new file mode 100644
index 0000000000..cb37ff079a
--- /dev/null
+++ b/layout/forms/crashtests/380116-1.xhtml
@@ -0,0 +1,11 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body onload="document.getElementById('t').style.display = 'block';">
+
+<table border="1" id="t">
+ <tr>
+ <td><select><option>foopy</option></select></td>
+ </tr>
+</table>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/382610-1.html b/layout/forms/crashtests/382610-1.html
new file mode 100644
index 0000000000..9fe9c5b5c1
--- /dev/null
+++ b/layout/forms/crashtests/382610-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+<div style="position: absolute">
+ <input type="file" style="position: absolute; -moz-appearance: menulist;">
+</div>
+
+</body>
+</html>
+
diff --git a/layout/forms/crashtests/383887-1.html b/layout/forms/crashtests/383887-1.html
new file mode 100644
index 0000000000..65d4751f7d
--- /dev/null
+++ b/layout/forms/crashtests/383887-1.html
@@ -0,0 +1,20 @@
+<html>
+<body>
+
+<table>
+ <tr>
+ <td>
+ <select>
+ <option style="padding: 200%;">
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div style="padding: 200%"><span style="float: left; border: 1px solid red;"></span></div>
+ </td>
+ </tr>
+</table>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/386554-1.html b/layout/forms/crashtests/386554-1.html
new file mode 100644
index 0000000000..585f1e5b54
--- /dev/null
+++ b/layout/forms/crashtests/386554-1.html
@@ -0,0 +1,14 @@
+<html>
+<head></head>
+
+<body>
+
+<div style="position: absolute"><input id="input" type="file" style="top: 100%"></div>
+
+<script>
+document.body.offsetHeight;
+document.getElementById("input").style.position = "absolute";
+</script>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/388374-1.xhtml b/layout/forms/crashtests/388374-1.xhtml
new file mode 100644
index 0000000000..2a871f68b4
--- /dev/null
+++ b/layout/forms/crashtests/388374-1.xhtml
@@ -0,0 +1,22 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+function boom()
+{
+ var newText = document.createTextNode('x');
+ document.getElementById("a").parentNode.appendChild(newText);
+}
+</script>
+</head>
+
+<body onload="boom()">
+
+<table border="1">
+ <tr>
+ <td>1</td>
+ <td id="a" style="padding-left: 10%"><select style="padding-left: 20%; bottom: 10%;"><option style="padding: 0 0 10% 20%;">pppp</option></select></td>
+ </tr>
+</table>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/388374-2.html b/layout/forms/crashtests/388374-2.html
new file mode 100644
index 0000000000..81918c37cd
--- /dev/null
+++ b/layout/forms/crashtests/388374-2.html
@@ -0,0 +1,25 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+function boom()
+{
+ var newDiv = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ newDiv.style.width = "100px";
+ newDiv.style.height = "8px";
+ newDiv.style.background = "lightgreen";
+ document.getElementById("tr").appendChild(newDiv);
+}
+</script>
+</head>
+
+<body onload="boom()">
+
+<table border="1">
+ <tr id="tr">
+ <td></td>
+ <td style="padding-left: 10%"><select style="padding-left: 20%; bottom: 10%;"><option style="padding: 0 0 10% 20%; width: 25px;"></option></select></td>
+ </tr>
+</table>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/393656-1.xhtml b/layout/forms/crashtests/393656-1.xhtml
new file mode 100644
index 0000000000..22a9326c5f
--- /dev/null
+++ b/layout/forms/crashtests/393656-1.xhtml
@@ -0,0 +1,13 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+</head>
+
+<body>
+ <select style="float: right; clear: right;">
+ <option><a style="float: right;"></a></option>
+ <option style="margin: 100%;"></option>
+ <option><div style="clear: both"></div></option>
+ </select>
+</body>
+
+</html>
diff --git a/layout/forms/crashtests/393656-2.xhtml b/layout/forms/crashtests/393656-2.xhtml
new file mode 100644
index 0000000000..7d9adaf20d
--- /dev/null
+++ b/layout/forms/crashtests/393656-2.xhtml
@@ -0,0 +1,22 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+</head>
+
+<body>
+ <div style="overflow: hidden;">
+ <div style="float: right;"><span>XXX</span></div>
+ <select style="float: right; clear: right;">
+ <option><a style="float: right;"></a></option>
+ <option style="margin: 100%;"></option>
+ <option><div style="clear: both"></div></option>
+ </select>
+ <dl style="float: left;"></dl>
+ <div>
+ <div style="float: left;"></div>
+ <div style="display: block; clear: both"></div>
+ <dl style="float: left;"></dl>
+ </div>
+ </div>
+</body>
+
+</html>
diff --git a/layout/forms/crashtests/399262.html b/layout/forms/crashtests/399262.html
new file mode 100644
index 0000000000..77bc0a16db
--- /dev/null
+++ b/layout/forms/crashtests/399262.html
@@ -0,0 +1,50 @@
+<style>
+select::first-letter, option::first-letter { color:green; }
+.block select, .block option{ display:block; }
+.scroll select, .scroll option{ display:block; overflow:hidden; }
+</style>
+
+<select></select>
+<select>F</select>
+<select>Ff</select>
+<select><option>F</select>
+<select><option>Ff</select>
+
+<div class="block">
+<select></select>
+<select>F</select>
+<select>Ff</select>
+<select><option>F</select>
+<select><option>Ff</select>
+</div>
+
+<div class="scroll">
+<select></select>
+<select>F</select>
+<select>Ff</select>
+<select><option>F</select>
+<select><option>Ff</select>
+</div>
+
+<select size=2></select>
+<select size=2>F</select>
+<select size=2>Ff</select>
+<select size=2><option>F</select>
+<select size=2><option>Ff</select>
+
+<div class="block">
+<select size=2></select>
+<select size=2>F</select>
+<select size=2>Ff</select>
+<select size=2><option>F</select>
+<select size=2><option>Ff</select>
+</div>
+
+<div class="scroll">
+<select size=2></select>
+<select size=2>F</select>
+<select size=2>Ff</select>
+<select size=2><option>F</select>
+<select size=2><option>Ff</select>
+</div>
+
diff --git a/layout/forms/crashtests/402852-1.html b/layout/forms/crashtests/402852-1.html
new file mode 100644
index 0000000000..e5ba9adb13
--- /dev/null
+++ b/layout/forms/crashtests/402852-1.html
@@ -0,0 +1,2 @@
+<body style="text-indent: 99999999999999999999px;">
+<fieldset>
diff --git a/layout/forms/crashtests/403148-1.html b/layout/forms/crashtests/403148-1.html
new file mode 100644
index 0000000000..cfb38fdb13
--- /dev/null
+++ b/layout/forms/crashtests/403148-1.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("ce").contentEditable = true;
+ document.documentElement.offsetHeight;
+ document.getElementById("finput").setAttribute("type", "");
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<input id="finput" type="file" disabled>
+
+<span id="ce"></span>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/404118-1.html b/layout/forms/crashtests/404118-1.html
new file mode 100644
index 0000000000..40a2ac72b0
--- /dev/null
+++ b/layout/forms/crashtests/404118-1.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<input type="file" style="position: fixed; height: 0; width: 10px; white-space: normal;">
+</body>
+</html>
diff --git a/layout/forms/crashtests/404123-1.html b/layout/forms/crashtests/404123-1.html
new file mode 100644
index 0000000000..73fbaa363c
--- /dev/null
+++ b/layout/forms/crashtests/404123-1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+
+<fieldset>
+<legend style="padding: 0pt 100%;"></legend>
+</fieldset>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/407066.html b/layout/forms/crashtests/407066.html
new file mode 100644
index 0000000000..98c4d0f4ae
--- /dev/null
+++ b/layout/forms/crashtests/407066.html
@@ -0,0 +1 @@
+<div style="display: inline-block; column-count: 2;">text<input type="reset" style="float: right;">
diff --git a/layout/forms/crashtests/451316.html b/layout/forms/crashtests/451316.html
new file mode 100644
index 0000000000..51fda22bc0
--- /dev/null
+++ b/layout/forms/crashtests/451316.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div style="column-width: 1px;"><select style="height: 12em; display: block;"></select></div>
+<div style="column-width: 1px;"><select multiple style="height: 12em; display: block;"></select></div>
+</body>
+</html>
diff --git a/layout/forms/crashtests/455451-1.html b/layout/forms/crashtests/455451-1.html
new file mode 100644
index 0000000000..fd7d7b6b37
--- /dev/null
+++ b/layout/forms/crashtests/455451-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ window.addEventListener("DOMCharacterDataModified", function(){});
+ document.body.appendChild(document.createElement("isindex"));
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+
+</html>
diff --git a/layout/forms/crashtests/457537-1.html b/layout/forms/crashtests/457537-1.html
new file mode 100644
index 0000000000..167d3b7076
--- /dev/null
+++ b/layout/forms/crashtests/457537-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ window.addEventListener("DOMAttrModified", function(){});
+ document.body.appendChild(document.createElement("input"));
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+
+</html>
diff --git a/layout/forms/crashtests/457537-2.html b/layout/forms/crashtests/457537-2.html
new file mode 100644
index 0000000000..626b035b48
--- /dev/null
+++ b/layout/forms/crashtests/457537-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ window.addEventListener("DOMAttrModified", function(){});
+ document.body.appendChild(document.createElement("isindex"));
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+
+</html>
diff --git a/layout/forms/crashtests/498698-1.html b/layout/forms/crashtests/498698-1.html
new file mode 100644
index 0000000000..fa8e2e17a8
--- /dev/null
+++ b/layout/forms/crashtests/498698-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<select style="height: 3401091640591pc"></select>
+</body>
+</html>
diff --git a/layout/forms/crashtests/513113-1.html b/layout/forms/crashtests/513113-1.html
new file mode 100644
index 0000000000..dc04ef6b6e
--- /dev/null
+++ b/layout/forms/crashtests/513113-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html><body onload="document.body.style.display = 'table';" style="column-count: 1; white-space: pre-line; -moz-appearance: resizer; height: 23698324514pc;">
+
+
+
+<input></body></html>
diff --git a/layout/forms/crashtests/538062-1.xhtml b/layout/forms/crashtests/538062-1.xhtml
new file mode 100644
index 0000000000..431f7ab847
--- /dev/null
+++ b/layout/forms/crashtests/538062-1.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ var option = document.getElementsByTagName("option")[0];
+ var b = option.firstChild;
+ option.appendChild(document.createTextNode("\u064A"));
+ document.documentElement.offsetHeight;
+ option.removeChild(b);
+}
+
+]]>
+</script>
+</head>
+
+<body onload="boom();"><select><option>B </option></select></body>
+</html>
diff --git a/layout/forms/crashtests/570624-1.html b/layout/forms/crashtests/570624-1.html
new file mode 100644
index 0000000000..47a2777282
--- /dev/null
+++ b/layout/forms/crashtests/570624-1.html
@@ -0,0 +1,15 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+function boom()
+{
+ var xt = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", 'textbox');
+ document.body.appendChild(xt);
+ xt.setAttribute('disabled', "true");
+ xt.setAttribute('value', "foo");
+}
+</script>
+</head>
+<body onload="boom();">
+</body>
+</html>
diff --git a/layout/forms/crashtests/578604-1.html b/layout/forms/crashtests/578604-1.html
new file mode 100644
index 0000000000..a7e3a870dd
--- /dev/null
+++ b/layout/forms/crashtests/578604-1.html
@@ -0,0 +1,17 @@
+<html class="reftest-paged">
+<head>
+<style>
+ h2 { page-break-before: always; }
+ #reviewer { position: fixed;}
+</style>
+</head>
+
+<body>
+
+<h2>Crash Test Case</h2>
+<div>
+<input id='reviewer'>
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/590302-1.xhtml b/layout/forms/crashtests/590302-1.xhtml
new file mode 100644
index 0000000000..85d49cb76c
--- /dev/null
+++ b/layout/forms/crashtests/590302-1.xhtml
@@ -0,0 +1,4 @@
+<html class="reftest-paged" xmlns="http://www.w3.org/1999/xhtml" style=" float: left; white-space: pre; ">
+<div style="page-break-before: always;"></div>
+<textarea style="position: fixed;"></textarea>
+</html>
diff --git a/layout/forms/crashtests/626014.xhtml b/layout/forms/crashtests/626014.xhtml
new file mode 100644
index 0000000000..05a2361d3d
--- /dev/null
+++ b/layout/forms/crashtests/626014.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+
+function boom()
+{
+ document.getElementById("i").selectionEnd;
+ document.getElementById("a").appendChild(document.getElementById("b"));
+}
+
+]]>
+</script>
+</head>
+<body onload="boom();">
+
+<mrow xmlns="http://www.w3.org/1998/Math/MathML" id="a"><mrow id="b"/><input xmlns="http://www.w3.org/1999/xhtml" id="i" /></mrow>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/639733.xhtml b/layout/forms/crashtests/639733.xhtml
new file mode 100644
index 0000000000..a6d5b01a8d
--- /dev/null
+++ b/layout/forms/crashtests/639733.xhtml
@@ -0,0 +1,26 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+
+function remove(n)
+{
+ n.remove();
+}
+
+function boom()
+{
+
+ document.documentElement.offsetHeight;
+ remove(document.getElementById("s"));
+ remove(document.getElementById("e"));
+ document.documentElement.offsetHeight;
+}
+
+]]>
+</script>
+</head>
+<body onload="boom();">
+<embed id="e" src="data:text/html,A"></embed><span id="s"><div></div></span><isindex/>
+</body>
+</html>
diff --git a/layout/forms/crashtests/669767.html b/layout/forms/crashtests/669767.html
new file mode 100644
index 0000000000..f41a9125c2
--- /dev/null
+++ b/layout/forms/crashtests/669767.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<title>Untitled</title>
+
+
+</head>
+<body>
+<iframe src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C/head%3E%3Cbody%3E%0A%3Ctextarea%3E%3C/textarea%3E%0A%0A%0A%3Cstyle%3E%0A@font-face%20%7B%0A%20%20%20%20%20%20font-family%3A%20%22cutabovetherest%22%3B%0A%20%20%20%20%20%20src%3A%20url%28%22http%3A//www.webpagepublicity.com/free-fonts/a/A%2520Cut%2520Above%2520The%2520Rest.ttf%22%29%3B%0A%7D%20%20%20%20%0A%0A%3C/style%3E%0A%0A%3Coptgroup%20contenteditable%3D%22true%22%20style%3D%22display%3A%20inline%3B%22%3E%3C/optgroup%3E%0A%0A%3C/body%3E%3C/html%3E"></iframe>
+<script>
+
+
+</script>
+</body>
+</html>
diff --git a/layout/forms/crashtests/682684-binding.xml b/layout/forms/crashtests/682684-binding.xml
new file mode 100644
index 0000000000..910eec33c8
--- /dev/null
+++ b/layout/forms/crashtests/682684-binding.xml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<style>html::before { content:"b"; }</style>
+<input contenteditable="true" spellcheck="true"/>
+</html> \ No newline at end of file
diff --git a/layout/forms/crashtests/682684.xhtml b/layout/forms/crashtests/682684.xhtml
new file mode 100644
index 0000000000..2b7b2c83fa
--- /dev/null
+++ b/layout/forms/crashtests/682684.xhtml
@@ -0,0 +1,3 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<span style="mask: url(682684-binding.xml#a);"/>
+</html>
diff --git a/layout/forms/crashtests/865602.html b/layout/forms/crashtests/865602.html
new file mode 100644
index 0000000000..f560e7a385
--- /dev/null
+++ b/layout/forms/crashtests/865602.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html><body>
+
+<div style="column-count: 2;"><fieldset style="transform-style: preserve-3d;"><fieldset style="white-space: pre-line; position: fixed;"><div style="position: fixed;">
+
+
+</div></fieldset></fieldset></div>
+
+</body></html>
diff --git a/layout/forms/crashtests/893331.html b/layout/forms/crashtests/893331.html
new file mode 100644
index 0000000000..86a01f363e
--- /dev/null
+++ b/layout/forms/crashtests/893331.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+</head>
+<body>
+<input type="range" min="10000.00000000003">
+</body>
+</html>
diff --git a/layout/forms/crashtests/893332-1.html b/layout/forms/crashtests/893332-1.html
new file mode 100644
index 0000000000..1cd17c0b1c
--- /dev/null
+++ b/layout/forms/crashtests/893332-1.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ <input min="-10" max="10" step="0.1" type="range" value="-5"/>
+ <input max="50" min="10" step="2" type="range" value="7"/>
+ </body>
+</html>
diff --git a/layout/forms/crashtests/944198.html b/layout/forms/crashtests/944198.html
new file mode 100644
index 0000000000..0da7bc54dd
--- /dev/null
+++ b/layout/forms/crashtests/944198.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+</head>
+<body onload="c.removeAttribute('type'); c.removeAttribute('value');">
+<input id="c" type="color">
+</body>
+</html>
diff --git a/layout/forms/crashtests/949891.xhtml b/layout/forms/crashtests/949891.xhtml
new file mode 100644
index 0000000000..8fbfe1e422
--- /dev/null
+++ b/layout/forms/crashtests/949891.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body>
+<input type="number"> </input>
+</body>
+</html>
diff --git a/layout/forms/crashtests/959311.html b/layout/forms/crashtests/959311.html
new file mode 100644
index 0000000000..1b4817221b
--- /dev/null
+++ b/layout/forms/crashtests/959311.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-paged">
+<head>
+<meta charset="utf-8">
+<style type="text/css">
+@page { size:5in 3in; margin:0in; }
+div { height: 2.5in; }
+select { height: 0.5in; display:block; padding:20px; page-break-inside:initial; }
+</style>
+</head>
+<body>
+ <div></div>
+ <select>
+ <option>Text</option>
+ </select>
+ </body>
+</html>
diff --git a/layout/forms/crashtests/960277-2.html b/layout/forms/crashtests/960277-2.html
new file mode 100644
index 0000000000..ea771ae798
--- /dev/null
+++ b/layout/forms/crashtests/960277-2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<style>
+#d { overflow:scroll; width:200px; height:200px; background:yellow; }
+#d2 { right:0; width:100px; height:100px; background:purple; }
+</style>
+<fieldset id="d">
+ <div id="d2"></div>
+</fieldset>
+<script>
+d.getBoundingClientRect();
+d.style.transform = "translateX(100px)";
+d.getBoundingClientRect();
+d2.style.position = "absolute";
+</script>
diff --git a/layout/forms/crashtests/997709-1.html b/layout/forms/crashtests/997709-1.html
new file mode 100644
index 0000000000..baf5d0b14b
--- /dev/null
+++ b/layout/forms/crashtests/997709-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE HTML>
+<html class="reftest-paged"><body><div style="position: fixed;">
+<div style="page-break-after: always"></div>
+<select style="display:flex; position: fixed;"><option>A</select>
+</div></body></html>
diff --git a/layout/forms/crashtests/crashtests.list b/layout/forms/crashtests/crashtests.list
new file mode 100644
index 0000000000..da43270ba6
--- /dev/null
+++ b/layout/forms/crashtests/crashtests.list
@@ -0,0 +1,80 @@
+load 166750-1.html
+load 200347-1.html
+load 203041-1.html
+load 213390-1.html
+load 258101-1.html
+load 266225-1.html
+load 310426-1.xhtml
+load 310520-1.xhtml
+load 315752-1.xhtml
+load 317502-1.xhtml
+load 321894.html
+load 343510-1.html
+load chrome://reftest/content/crashtests/layout/forms/crashtests/363696-1.xhtml
+load 363696-2.html
+load 363696-3.html
+load 366205-1.html
+load 366537-1.xhtml
+load 367587-1.html
+load 370703-1.html
+load 370940-1.html
+load 370967.html
+load 378369.html
+load 380116-1.xhtml
+load 382610-1.html
+load 383887-1.html
+load 386554-1.html
+load 388374-1.xhtml
+load 388374-2.html
+load 393656-1.xhtml
+load 393656-2.xhtml
+load 399262.html
+load 402852-1.html
+load 403148-1.html
+load 404118-1.html
+load 404123-1.html
+load 407066.html
+load 451316.html
+load 455451-1.html
+load 457537-1.html
+load 457537-2.html
+load 498698-1.html
+load 513113-1.html
+load 538062-1.xhtml
+load 570624-1.html
+asserts(1) load 578604-1.html # bug 584564
+asserts(4-7) load 590302-1.xhtml # bug 584564
+load 626014.xhtml
+load 639733.xhtml
+load 669767.html
+load 682684.xhtml
+load 865602.html
+load 893331.html
+load 893332-1.html
+load 944198.html
+load 949891.xhtml
+load 959311.html
+load 960277-2.html
+load 997709-1.html
+load 1102791.html
+load 1140216.html
+load 1182414.html
+load 1212688.html
+load 1228670.xhtml
+load 1279354.html
+load 1388230-1.html
+load 1388230-2.html
+load 1405830.html
+load 1418477.html
+load 1432853.html
+asserts(1-4) load 1460787-1.html
+load 1464165-1.html
+load 1471157.html
+load 1488219.html
+load 1600207.html
+load 1600367.html
+load 1617753.html
+load 1679471.html
+load 1690166-1.html
+load 1690166-2.html
+load 1873802-1.html
diff --git a/layout/forms/moz.build b/layout/forms/moz.build
new file mode 100644
index 0000000000..838a17bf92
--- /dev/null
+++ b/layout/forms/moz.build
@@ -0,0 +1,54 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Layout: Form Controls")
+
+MOCHITEST_MANIFESTS += ["test/mochitest.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome.toml"]
+
+EXPORTS += [
+ "nsIFormControlFrame.h",
+ "nsISelectControlFrame.h",
+ "nsITextControlFrame.h",
+]
+
+UNIFIED_SOURCES += [
+ "HTMLSelectEventListener.cpp",
+ "ListMutationObserver.cpp",
+ "nsButtonFrameRenderer.cpp",
+ "nsCheckboxRadioFrame.cpp",
+ "nsColorControlFrame.cpp",
+ "nsComboboxControlFrame.cpp",
+ "nsDateTimeControlFrame.cpp",
+ "nsFieldSetFrame.cpp",
+ "nsFileControlFrame.cpp",
+ "nsGfxButtonControlFrame.cpp",
+ "nsHTMLButtonControlFrame.cpp",
+ "nsImageControlFrame.cpp",
+ "nsListControlFrame.cpp",
+ "nsMeterFrame.cpp",
+ "nsNumberControlFrame.cpp",
+ "nsProgressFrame.cpp",
+ "nsRangeFrame.cpp",
+ "nsSearchControlFrame.cpp",
+ "nsSelectsAreaFrame.cpp",
+ "nsTextControlFrame.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "../base",
+ "../generic",
+ "../painting",
+ "../style",
+ "../xul",
+ "/dom/base",
+ "/dom/html",
+]
diff --git a/layout/forms/nsButtonFrameRenderer.cpp b/layout/forms/nsButtonFrameRenderer.cpp
new file mode 100644
index 0000000000..598610cf72
--- /dev/null
+++ b/layout/forms/nsButtonFrameRenderer.cpp
@@ -0,0 +1,471 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "nsButtonFrameRenderer.h"
+#include "nsCSSRendering.h"
+#include "nsPresContext.h"
+#include "nsPresContextInlines.h"
+#include "nsGkAtoms.h"
+#include "nsCSSPseudoElements.h"
+#include "nsNameSpaceManager.h"
+#include "mozilla/ServoStyleSet.h"
+#include "mozilla/Unused.h"
+#include "nsDisplayList.h"
+#include "nsITheme.h"
+#include "nsIFrame.h"
+#include "mozilla/dom/Element.h"
+
+#include "gfxUtils.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+using namespace mozilla::layers;
+
+namespace mozilla {
+class nsDisplayButtonBoxShadowOuter;
+class nsDisplayButtonBorder;
+class nsDisplayButtonForeground;
+} // namespace mozilla
+
+nsButtonFrameRenderer::nsButtonFrameRenderer() : mFrame(nullptr) {
+ MOZ_COUNT_CTOR(nsButtonFrameRenderer);
+}
+
+nsButtonFrameRenderer::~nsButtonFrameRenderer() {
+ MOZ_COUNT_DTOR(nsButtonFrameRenderer);
+}
+
+void nsButtonFrameRenderer::SetFrame(nsIFrame* aFrame,
+ nsPresContext* aPresContext) {
+ mFrame = aFrame;
+ ReResolveStyles(aPresContext);
+}
+
+nsIFrame* nsButtonFrameRenderer::GetFrame() { return mFrame; }
+
+void nsButtonFrameRenderer::SetDisabled(bool aDisabled, bool aNotify) {
+ dom::Element* element = mFrame->GetContent()->AsElement();
+ if (aDisabled)
+ element->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, aNotify);
+ else
+ element->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, aNotify);
+}
+
+bool nsButtonFrameRenderer::isDisabled() {
+ return mFrame->GetContent()->AsElement()->IsDisabled();
+}
+
+nsresult nsButtonFrameRenderer::DisplayButton(nsDisplayListBuilder* aBuilder,
+ nsDisplayList* aBackground,
+ nsDisplayList* aForeground) {
+ if (!mFrame->StyleEffects()->mBoxShadow.IsEmpty()) {
+ aBackground->AppendNewToTop<nsDisplayButtonBoxShadowOuter>(aBuilder,
+ GetFrame());
+ }
+
+ nsRect buttonRect =
+ mFrame->GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(mFrame);
+
+ const AppendedBackgroundType result =
+ nsDisplayBackgroundImage::AppendBackgroundItemsToTop(
+ aBuilder, mFrame, buttonRect, aBackground);
+ if (result == AppendedBackgroundType::None) {
+ aBuilder->BuildCompositorHitTestInfoIfNeeded(GetFrame(), aBackground);
+ }
+
+ aBackground->AppendNewToTop<nsDisplayButtonBorder>(aBuilder, GetFrame(),
+ this);
+
+ // Only display focus rings if we actually have them. Since at most one
+ // button would normally display a focus ring, most buttons won't have them.
+ if (mInnerFocusStyle && mInnerFocusStyle->StyleBorder()->HasBorder() &&
+ mFrame->IsThemed() &&
+ mFrame->PresContext()->Theme()->ThemeWantsButtonInnerFocusRing()) {
+ aForeground->AppendNewToTop<nsDisplayButtonForeground>(aBuilder, GetFrame(),
+ this);
+ }
+ return NS_OK;
+}
+
+void nsButtonFrameRenderer::GetButtonInnerFocusRect(const nsRect& aRect,
+ nsRect& aResult) {
+ aResult = aRect;
+ aResult.Deflate(mFrame->GetUsedBorderAndPadding());
+
+ if (mInnerFocusStyle) {
+ nsMargin innerFocusPadding(0, 0, 0, 0);
+ mInnerFocusStyle->StylePadding()->GetPadding(innerFocusPadding);
+
+ nsMargin framePadding = mFrame->GetUsedPadding();
+
+ innerFocusPadding.top = std::min(innerFocusPadding.top, framePadding.top);
+ innerFocusPadding.right =
+ std::min(innerFocusPadding.right, framePadding.right);
+ innerFocusPadding.bottom =
+ std::min(innerFocusPadding.bottom, framePadding.bottom);
+ innerFocusPadding.left =
+ std::min(innerFocusPadding.left, framePadding.left);
+
+ aResult.Inflate(innerFocusPadding);
+ }
+}
+
+ImgDrawResult nsButtonFrameRenderer::PaintInnerFocusBorder(
+ nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext, const nsRect& aDirtyRect,
+ const nsRect& aRect) {
+ // we draw the -moz-focus-inner border just inside the button's
+ // normal border and padding, to match Windows themes.
+
+ nsRect rect;
+
+ PaintBorderFlags flags = aBuilder->ShouldSyncDecodeImages()
+ ? PaintBorderFlags::SyncDecodeImages
+ : PaintBorderFlags();
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (mInnerFocusStyle) {
+ GetButtonInnerFocusRect(aRect, rect);
+
+ result &=
+ nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame,
+ aDirtyRect, rect, mInnerFocusStyle, flags);
+ }
+
+ return result;
+}
+
+Maybe<nsCSSBorderRenderer>
+nsButtonFrameRenderer::CreateInnerFocusBorderRenderer(
+ nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext,
+ gfxContext* aRenderingContext, const nsRect& aDirtyRect,
+ const nsRect& aRect, bool* aBorderIsEmpty) {
+ if (mInnerFocusStyle) {
+ nsRect rect;
+ GetButtonInnerFocusRect(aRect, rect);
+
+ gfx::DrawTarget* dt =
+ aRenderingContext ? aRenderingContext->GetDrawTarget() : nullptr;
+ return nsCSSRendering::CreateBorderRenderer(
+ aPresContext, dt, mFrame, aDirtyRect, rect, mInnerFocusStyle,
+ aBorderIsEmpty);
+ }
+
+ return Nothing();
+}
+
+ImgDrawResult nsButtonFrameRenderer::PaintBorder(nsDisplayListBuilder* aBuilder,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect,
+ const nsRect& aRect) {
+ // get the button rect this is inside the focus and outline rects
+ nsRect buttonRect = aRect;
+ ComputedStyle* context = mFrame->Style();
+
+ PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages()
+ ? PaintBorderFlags::SyncDecodeImages
+ : PaintBorderFlags();
+
+ nsCSSRendering::PaintBoxShadowInner(aPresContext, aRenderingContext, mFrame,
+ buttonRect);
+
+ ImgDrawResult result =
+ nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame,
+ aDirtyRect, buttonRect, context, borderFlags);
+
+ return result;
+}
+
+/**
+ * Call this when styles change
+ */
+void nsButtonFrameRenderer::ReResolveStyles(nsPresContext* aPresContext) {
+ // get all the styles
+ ServoStyleSet* styleSet = aPresContext->StyleSet();
+
+ // get styles assigned to -moz-focus-inner (ie dotted border on Windows)
+ mInnerFocusStyle = styleSet->ProbePseudoElementStyle(
+ *mFrame->GetContent()->AsElement(), PseudoStyleType::mozFocusInner,
+ nullptr, mFrame->Style());
+}
+
+ComputedStyle* nsButtonFrameRenderer::GetComputedStyle(int32_t aIndex) const {
+ switch (aIndex) {
+ case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX:
+ return mInnerFocusStyle;
+ default:
+ return nullptr;
+ }
+}
+
+void nsButtonFrameRenderer::SetComputedStyle(int32_t aIndex,
+ ComputedStyle* aComputedStyle) {
+ switch (aIndex) {
+ case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX:
+ mInnerFocusStyle = aComputedStyle;
+ break;
+ }
+}
+
+namespace mozilla {
+
+class nsDisplayButtonBoxShadowOuter : public nsPaintedDisplayItem {
+ public:
+ nsDisplayButtonBoxShadowOuter(nsDisplayListBuilder* aBuilder,
+ nsIFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayButtonBoxShadowOuter);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonBoxShadowOuter)
+
+ virtual bool CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+
+ bool CanBuildWebRenderDisplayItems();
+
+ virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override;
+ NS_DISPLAY_DECL_NAME("ButtonBoxShadowOuter", TYPE_BUTTON_BOX_SHADOW_OUTER)
+};
+
+nsRect nsDisplayButtonBoxShadowOuter::GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const {
+ *aSnap = false;
+ return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
+}
+
+void nsDisplayButtonBoxShadowOuter::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ nsRect frameRect = nsRect(ToReferenceFrame(), mFrame->GetSize());
+
+ nsCSSRendering::PaintBoxShadowOuter(mFrame->PresContext(), *aCtx, mFrame,
+ frameRect, GetPaintRect(aBuilder, aCtx));
+}
+
+bool nsDisplayButtonBoxShadowOuter::CanBuildWebRenderDisplayItems() {
+ // FIXME(emilio): Is this right? That doesn't make much sense.
+ if (mFrame->StyleEffects()->mBoxShadow.IsEmpty()) {
+ return false;
+ }
+
+ bool hasBorderRadius;
+ bool nativeTheme =
+ nsCSSRendering::HasBoxShadowNativeTheme(mFrame, hasBorderRadius);
+
+ // We don't support native themed things yet like box shadows around
+ // input buttons.
+ return !nativeTheme;
+}
+
+bool nsDisplayButtonBoxShadowOuter::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ if (!CanBuildWebRenderDisplayItems()) {
+ return false;
+ }
+ int32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel();
+ nsRect shadowRect = nsRect(ToReferenceFrame(), mFrame->GetSize());
+ LayoutDeviceRect deviceBox =
+ LayoutDeviceRect::FromAppUnits(shadowRect, appUnitsPerDevPixel);
+ wr::LayoutRect deviceBoxRect = wr::ToLayoutRect(deviceBox);
+
+ bool dummy;
+ LayoutDeviceRect clipRect = LayoutDeviceRect::FromAppUnits(
+ GetBounds(aDisplayListBuilder, &dummy), appUnitsPerDevPixel);
+ wr::LayoutRect deviceClipRect = wr::ToLayoutRect(clipRect);
+
+ bool hasBorderRadius;
+ Unused << nsCSSRendering::HasBoxShadowNativeTheme(mFrame, hasBorderRadius);
+
+ LayoutDeviceSize zeroSize;
+ wr::BorderRadius borderRadius =
+ wr::ToBorderRadius(zeroSize, zeroSize, zeroSize, zeroSize);
+ if (hasBorderRadius) {
+ gfx::RectCornerRadii borderRadii;
+ hasBorderRadius = nsCSSRendering::GetBorderRadii(shadowRect, shadowRect,
+ mFrame, borderRadii);
+ if (hasBorderRadius) {
+ borderRadius = wr::ToBorderRadius(borderRadii);
+ }
+ }
+
+ const Span<const StyleBoxShadow> shadows =
+ mFrame->StyleEffects()->mBoxShadow.AsSpan();
+ MOZ_ASSERT(!shadows.IsEmpty());
+
+ for (const StyleBoxShadow& shadow : Reversed(shadows)) {
+ if (shadow.inset) {
+ continue;
+ }
+ float blurRadius =
+ float(shadow.base.blur.ToAppUnits()) / float(appUnitsPerDevPixel);
+ gfx::DeviceColor shadowColor =
+ ToDeviceColor(nsCSSRendering::GetShadowColor(shadow.base, mFrame, 1.0));
+
+ LayoutDevicePoint shadowOffset = LayoutDevicePoint::FromAppUnits(
+ nsPoint(shadow.base.horizontal.ToAppUnits(),
+ shadow.base.vertical.ToAppUnits()),
+ appUnitsPerDevPixel);
+
+ float spreadRadius =
+ float(shadow.spread.ToAppUnits()) / float(appUnitsPerDevPixel);
+
+ aBuilder.PushBoxShadow(deviceBoxRect, deviceClipRect, !BackfaceIsHidden(),
+ deviceBoxRect, wr::ToLayoutVector2D(shadowOffset),
+ wr::ToColorF(shadowColor), blurRadius, spreadRadius,
+ borderRadius, wr::BoxShadowClipMode::Outset);
+ }
+ return true;
+}
+
+class nsDisplayButtonBorder final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayButtonBorder(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
+ nsButtonFrameRenderer* aRenderer)
+ : nsPaintedDisplayItem(aBuilder, aFrame), mBFR(aRenderer) {
+ MOZ_COUNT_CTOR(nsDisplayButtonBorder);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonBorder)
+
+ virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect,
+ HitTestState* aState,
+ nsTArray<nsIFrame*>* aOutFrames) override {
+ aOutFrames->AppendElement(mFrame);
+ }
+ virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override;
+ virtual bool CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+ NS_DISPLAY_DECL_NAME("ButtonBorderBackground", TYPE_BUTTON_BORDER_BACKGROUND)
+ private:
+ nsButtonFrameRenderer* mBFR;
+};
+
+bool nsDisplayButtonBorder::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ // This is really a combination of paint box shadow inner +
+ // paint border.
+ aBuilder.StartGroup(this);
+ const nsRect buttonRect = nsRect(ToReferenceFrame(), mFrame->GetSize());
+ bool snap;
+ nsRect visible = GetBounds(aDisplayListBuilder, &snap);
+ nsDisplayBoxShadowInner::CreateInsetBoxShadowWebRenderCommands(
+ aBuilder, aSc, visible, mFrame, buttonRect);
+
+ bool borderIsEmpty = false;
+ Maybe<nsCSSBorderRenderer> br = nsCSSRendering::CreateBorderRenderer(
+ mFrame->PresContext(), nullptr, mFrame, nsRect(),
+ nsRect(ToReferenceFrame(), mFrame->GetSize()), mFrame->Style(),
+ &borderIsEmpty, mFrame->GetSkipSides());
+ if (!br) {
+ if (borderIsEmpty) {
+ aBuilder.FinishGroup();
+ } else {
+ aBuilder.CancelGroup(true);
+ }
+
+ return borderIsEmpty;
+ }
+
+ br->CreateWebRenderCommands(this, aBuilder, aResources, aSc);
+ aBuilder.FinishGroup();
+ return true;
+}
+
+void nsDisplayButtonBorder::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ NS_ASSERTION(mFrame, "No frame?");
+ nsPresContext* pc = mFrame->PresContext();
+ nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize());
+
+ // draw the border and background inside the focus and outline borders
+ Unused << mBFR->PaintBorder(aBuilder, pc, *aCtx, GetPaintRect(aBuilder, aCtx),
+ r);
+}
+
+nsRect nsDisplayButtonBorder::GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const {
+ *aSnap = false;
+ return aBuilder->IsForEventDelivery()
+ ? nsRect(ToReferenceFrame(), mFrame->GetSize())
+ : mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
+}
+
+class nsDisplayButtonForeground final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayButtonForeground(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
+ nsButtonFrameRenderer* aRenderer)
+ : nsPaintedDisplayItem(aBuilder, aFrame), mBFR(aRenderer) {
+ MOZ_COUNT_CTOR(nsDisplayButtonForeground);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonForeground)
+
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+ bool CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+ NS_DISPLAY_DECL_NAME("ButtonForeground", TYPE_BUTTON_FOREGROUND)
+ private:
+ nsButtonFrameRenderer* mBFR;
+};
+
+void nsDisplayButtonForeground::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize());
+
+ // Draw the -moz-focus-inner border
+ Unused << mBFR->PaintInnerFocusBorder(aBuilder, mFrame->PresContext(), *aCtx,
+ GetPaintRect(aBuilder, aCtx), r);
+}
+
+bool nsDisplayButtonForeground::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ Maybe<nsCSSBorderRenderer> br;
+ bool borderIsEmpty = false;
+ bool dummy;
+ nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize());
+ br = mBFR->CreateInnerFocusBorderRenderer(
+ aDisplayListBuilder, mFrame->PresContext(), nullptr,
+ GetBounds(aDisplayListBuilder, &dummy), r, &borderIsEmpty);
+
+ if (!br) {
+ return borderIsEmpty;
+ }
+
+ aBuilder.StartGroup(this);
+ br->CreateWebRenderCommands(this, aBuilder, aResources, aSc);
+ aBuilder.FinishGroup();
+
+ return true;
+}
+
+} // namespace mozilla
diff --git a/layout/forms/nsButtonFrameRenderer.h b/layout/forms/nsButtonFrameRenderer.h
new file mode 100644
index 0000000000..ea5a9cdea4
--- /dev/null
+++ b/layout/forms/nsButtonFrameRenderer.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsButtonFrameRenderer_h___
+#define nsButtonFrameRenderer_h___
+
+#include "nsMargin.h"
+#include "nsCSSRenderingBorders.h"
+
+class gfxContext;
+class nsIFrame;
+class nsPresContext;
+struct nsRect;
+
+namespace mozilla {
+class nsDisplayList;
+class nsDisplayListBuilder;
+} // namespace mozilla
+
+#define NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX 0
+#define NS_BUTTON_RENDERER_LAST_CONTEXT_INDEX \
+ NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX
+
+class nsButtonFrameRenderer {
+ using nsDisplayList = mozilla::nsDisplayList;
+ using nsDisplayListBuilder = mozilla::nsDisplayListBuilder;
+
+ typedef mozilla::image::ImgDrawResult ImgDrawResult;
+ typedef mozilla::ComputedStyle ComputedStyle;
+
+ public:
+ nsButtonFrameRenderer();
+ ~nsButtonFrameRenderer();
+
+ /**
+ * Create display list items for the button
+ */
+ nsresult DisplayButton(nsDisplayListBuilder* aBuilder,
+ nsDisplayList* aBackground,
+ nsDisplayList* aForeground);
+
+ ImgDrawResult PaintInnerFocusBorder(nsDisplayListBuilder* aBuilder,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect,
+ const nsRect& aRect);
+
+ mozilla::Maybe<nsCSSBorderRenderer> CreateInnerFocusBorderRenderer(
+ nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext,
+ gfxContext* aRenderingContext, const nsRect& aDirtyRect,
+ const nsRect& aRect, bool* aBorderIsEmpty);
+
+ ImgDrawResult PaintBorder(nsDisplayListBuilder* aBuilder,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, const nsRect& aRect);
+
+ void SetFrame(nsIFrame* aFrame, nsPresContext* aPresContext);
+
+ void SetDisabled(bool aDisabled, bool notify);
+
+ bool isActive();
+ bool isDisabled();
+
+ void GetButtonInnerFocusRect(const nsRect& aRect, nsRect& aResult);
+
+ ComputedStyle* GetComputedStyle(int32_t aIndex) const;
+ void SetComputedStyle(int32_t aIndex, ComputedStyle* aComputedStyle);
+ void ReResolveStyles(nsPresContext* aPresContext);
+
+ nsIFrame* GetFrame();
+
+ private:
+ // cached style for optional inner focus outline (used on Windows).
+ RefPtr<ComputedStyle> mInnerFocusStyle;
+
+ nsIFrame* mFrame;
+};
+
+#endif
diff --git a/layout/forms/nsCheckboxRadioFrame.cpp b/layout/forms/nsCheckboxRadioFrame.cpp
new file mode 100644
index 0000000000..e2b8541613
--- /dev/null
+++ b/layout/forms/nsCheckboxRadioFrame.cpp
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCheckboxRadioFrame.h"
+
+#include "nsGkAtoms.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/PresShell.h"
+#include "nsIContent.h"
+#include "nsStyleConsts.h"
+
+using namespace mozilla;
+using mozilla::dom::HTMLInputElement;
+
+// #define FCF_NOISY
+
+nsCheckboxRadioFrame* NS_NewCheckboxRadioFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsCheckboxRadioFrame(aStyle, aPresShell->GetPresContext());
+}
+
+nsCheckboxRadioFrame::nsCheckboxRadioFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsAtomicContainerFrame(aStyle, aPresContext, kClassID) {}
+
+nsCheckboxRadioFrame::~nsCheckboxRadioFrame() = default;
+
+NS_IMPL_FRAMEARENA_HELPERS(nsCheckboxRadioFrame)
+
+NS_QUERYFRAME_HEAD(nsCheckboxRadioFrame)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsAtomicContainerFrame)
+
+nscoord nsCheckboxRadioFrame::DefaultSize() {
+ if (StyleDisplay()->HasAppearance()) {
+ return PresContext()->Theme()->GetCheckboxRadioPrefSize();
+ }
+ return CSSPixel::ToAppUnits(9);
+}
+
+/* virtual */
+void nsCheckboxRadioFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ DO_GLOBAL_REFLOW_COUNT_DSP("nsCheckboxRadioFrame");
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+}
+
+/* virtual */
+nscoord nsCheckboxRadioFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+ result = StyleDisplay()->HasAppearance() ? DefaultSize() : 0;
+ return result;
+}
+
+/* virtual */
+nscoord nsCheckboxRadioFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+ result = StyleDisplay()->HasAppearance() ? DefaultSize() : 0;
+ return result;
+}
+
+/* virtual */
+LogicalSize nsCheckboxRadioFrame::ComputeAutoSize(
+ gfxContext* aRC, WritingMode aWM, const LogicalSize& aCBSize,
+ nscoord aAvailableISize, const LogicalSize& aMargin,
+ const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
+ ComputeSizeFlags aFlags) {
+ LogicalSize size(aWM, 0, 0);
+ if (!StyleDisplay()->HasAppearance()) {
+ return size;
+ }
+
+ // Note: this call always set the BSize to NS_UNCONSTRAINEDSIZE.
+ size = nsAtomicContainerFrame::ComputeAutoSize(
+ aRC, aWM, aCBSize, aAvailableISize, aMargin, aBorderPadding,
+ aSizeOverrides, aFlags);
+ size.BSize(aWM) = DefaultSize();
+ return size;
+}
+
+Maybe<nscoord> nsCheckboxRadioFrame::GetNaturalBaselineBOffset(
+ WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext) const {
+ NS_ASSERTION(!IsSubtreeDirty(), "frame must not be dirty");
+
+ if (aBaselineGroup == BaselineSharingGroup::Last) {
+ return Nothing{};
+ }
+
+ if (StyleDisplay()->IsBlockOutsideStyle()) {
+ return Nothing{};
+ }
+
+ // For appearance:none we use a standard CSS baseline, i.e. synthesized from
+ // our margin-box.
+ if (!StyleDisplay()->HasAppearance()) {
+ return Nothing{};
+ }
+
+ if (aWM.IsCentralBaseline()) {
+ return Some(GetLogicalUsedBorderAndPadding(aWM).BStart(aWM) +
+ ContentBSize(aWM) / 2);
+ }
+ // This is for compatibility with Chrome, Safari and Edge (Dec 2016).
+ // Treat radio buttons and checkboxes as having an intrinsic baseline
+ // at the block-end of the control (use the block-end content edge rather
+ // than the margin edge).
+ // For "inverted" lines (typically in writing-mode:vertical-lr), use the
+ // block-start end instead.
+ return Some(aWM.IsLineInverted()
+ ? GetLogicalUsedBorderAndPadding(aWM).BStart(aWM)
+ : BSize(aWM) - GetLogicalUsedBorderAndPadding(aWM).BEnd(aWM));
+}
+
+void nsCheckboxRadioFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsCheckboxRadioFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ NS_FRAME_TRACE(
+ NS_FRAME_TRACE_CALLS,
+ ("enter nsCheckboxRadioFrame::Reflow: aMaxSize=%d,%d",
+ aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight()));
+
+ const auto wm = aReflowInput.GetWritingMode();
+ aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm));
+
+ if (nsLayoutUtils::FontSizeInflationEnabled(aPresContext)) {
+ float inflation = nsLayoutUtils::FontSizeInflationFor(this);
+ aDesiredSize.Width() *= inflation;
+ aDesiredSize.Height() *= inflation;
+ }
+
+ NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
+ ("exit nsCheckboxRadioFrame::Reflow: size=%d,%d",
+ aDesiredSize.Width(), aDesiredSize.Height()));
+
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+ FinishAndStoreOverflow(&aDesiredSize);
+}
+
+void nsCheckboxRadioFrame::SetFocus(bool aOn, bool aRepaint) {}
+
+nsresult nsCheckboxRadioFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ // Check for disabled content so that selection works properly (?).
+ if (IsContentDisabled()) {
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+ return NS_OK;
+}
+
+nsresult nsCheckboxRadioFrame::SetFormProperty(nsAtom* aName,
+ const nsAString& aValue) {
+ return NS_OK;
+}
diff --git a/layout/forms/nsCheckboxRadioFrame.h b/layout/forms/nsCheckboxRadioFrame.h
new file mode 100644
index 0000000000..6b1a902925
--- /dev/null
+++ b/layout/forms/nsCheckboxRadioFrame.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCheckboxRadioFrame_h___
+#define nsCheckboxRadioFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsIFormControlFrame.h"
+#include "nsAtomicContainerFrame.h"
+#include "nsDisplayList.h"
+
+/**
+ * nsCheckboxRadioFrame is used for radio buttons and checkboxes.
+ */
+class nsCheckboxRadioFrame final : public nsAtomicContainerFrame,
+ public nsIFormControlFrame {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsCheckboxRadioFrame)
+
+ explicit nsCheckboxRadioFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ // nsIFrame replacements
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ /**
+ * Both GetMinISize and GetPrefISize will return whatever GetIntrinsicISize
+ * returns.
+ */
+ nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ /**
+ * Our auto size is just intrinsic width and intrinsic height.
+ */
+ mozilla::LogicalSize ComputeAutoSize(
+ gfxContext* aRenderingContext, mozilla::WritingMode aWM,
+ const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize,
+ const mozilla::LogicalSize& aMargin,
+ const mozilla::LogicalSize& aBorderPadding,
+ const mozilla::StyleSizeOverrides& aSizeOverrides,
+ mozilla::ComputeSizeFlags aFlags) override;
+
+ /**
+ * Respond to a gui event
+ * @see nsIFrame::HandleEvent
+ */
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ Maybe<nscoord> GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext) const override;
+
+ /**
+ * Respond to the request to resize and/or reflow
+ * @see nsIFrame::Reflow
+ */
+ void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ // new behavior
+
+ void SetFocus(bool aOn = true, bool aRepaint = false) override;
+
+ // nsIFormControlFrame
+ nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"CheckboxRadio"_ns, aResult);
+ }
+#endif
+
+ protected:
+ virtual ~nsCheckboxRadioFrame();
+
+ nscoord DefaultSize();
+};
+
+#endif
diff --git a/layout/forms/nsColorControlFrame.cpp b/layout/forms/nsColorControlFrame.cpp
new file mode 100644
index 0000000000..f921016d06
--- /dev/null
+++ b/layout/forms/nsColorControlFrame.cpp
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsColorControlFrame.h"
+
+#include "nsContentCreatorFunctions.h"
+#include "nsContentUtils.h"
+#include "nsCSSPseudoElements.h"
+#include "nsGkAtoms.h"
+#include "nsIFormControl.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+using mozilla::dom::CallerType;
+using mozilla::dom::Document;
+using mozilla::dom::Element;
+using mozilla::dom::HTMLInputElement;
+
+nsColorControlFrame::nsColorControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {}
+
+nsIFrame* NS_NewColorControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsColorControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsColorControlFrame)
+
+NS_QUERYFRAME_HEAD(nsColorControlFrame)
+ NS_QUERYFRAME_ENTRY(nsColorControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame)
+
+void nsColorControlFrame::Destroy(DestroyContext& aContext) {
+ aContext.AddAnonymousContent(mColorContent.forget());
+ nsHTMLButtonControlFrame::Destroy(aContext);
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsColorControlFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"ColorControl"_ns, aResult);
+}
+#endif
+
+// Create the color area for the button.
+// The frame will be generated by the frame constructor.
+nsresult nsColorControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ RefPtr<Document> doc = mContent->GetComposedDoc();
+ mColorContent = doc->CreateHTMLElement(nsGkAtoms::div);
+ mColorContent->SetPseudoElementType(PseudoStyleType::mozColorSwatch);
+
+ // Mark the element to be native anonymous before setting any attributes.
+ mColorContent->SetIsNativeAnonymousRoot();
+
+ nsresult rv = UpdateColor();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ aElements.AppendElement(mColorContent);
+
+ return NS_OK;
+}
+
+void nsColorControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mColorContent) {
+ aElements.AppendElement(mColorContent);
+ }
+}
+
+nsresult nsColorControlFrame::UpdateColor() {
+ // Get the color from the "value" property of our content; it will return the
+ // default color (through the sanitization algorithm) if the value is empty.
+ nsAutoString color;
+ HTMLInputElement* elt = HTMLInputElement::FromNode(mContent);
+ elt->GetValue(color, CallerType::System);
+
+ if (color.IsEmpty()) {
+ // OK, there is one case the color string might be empty -- if our content
+ // is still being created, i.e. if it has mDoneCreating==false. In that
+ // case, we simply do nothing, because we'll be called again with a complete
+ // content node before we ever reflow or paint. Specifically: we can expect
+ // that HTMLInputElement::DoneCreatingElement() will set mDoneCreating to
+ // true (which enables sanitization) and then it'll call SetValueInternal(),
+ // which produces a nonempty color (via sanitization), and then it'll call
+ // this function here, and we'll get the nonempty default color.
+ MOZ_ASSERT(HasAnyStateBits(NS_FRAME_FIRST_REFLOW),
+ "Content node's GetValue() should return a valid color string "
+ "by the time we've been reflowed (the default color, in case "
+ "no valid color is set)");
+ return NS_OK;
+ }
+
+ // Set the background-color CSS property of the swatch element to this color.
+ return mColorContent->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ u"background-color:"_ns + color,
+ /* aNotify */ true);
+}
+
+nsresult nsColorControlFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ NS_ASSERTION(mColorContent, "The color div must exist");
+
+ // If the value attribute is set, update the color box, but only if we're
+ // still a color control, which might not be the case if the type attribute
+ // was removed/changed.
+ nsCOMPtr<nsIFormControl> fctrl = do_QueryInterface(GetContent());
+ if (fctrl->ControlType() == FormControlType::InputColor &&
+ aNameSpaceID == kNameSpaceID_None && nsGkAtoms::value == aAttribute) {
+ UpdateColor();
+ }
+ return nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute,
+ aModType);
+}
+
+nsContainerFrame* nsColorControlFrame::GetContentInsertionFrame() {
+ return this;
+}
diff --git a/layout/forms/nsColorControlFrame.h b/layout/forms/nsColorControlFrame.h
new file mode 100644
index 0000000000..a85816324f
--- /dev/null
+++ b/layout/forms/nsColorControlFrame.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsColorControlFrame_h___
+#define nsColorControlFrame_h___
+
+#include "nsCOMPtr.h"
+#include "nsHTMLButtonControlFrame.h"
+#include "nsIAnonymousContentCreator.h"
+
+namespace mozilla {
+enum class PseudoStyleType : uint8_t;
+class PresShell;
+} // namespace mozilla
+
+// Class which implements the input type=color
+
+class nsColorControlFrame final : public nsHTMLButtonControlFrame,
+ public nsIAnonymousContentCreator {
+ typedef mozilla::PseudoStyleType PseudoStyleType;
+ typedef mozilla::dom::Element Element;
+
+ public:
+ friend nsIFrame* NS_NewColorControlFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ void Destroy(DestroyContext&) override;
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsColorControlFrame)
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ // nsIAnonymousContentCreator
+ virtual nsresult CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) override;
+ virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ // nsIFrame
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+ virtual nsContainerFrame* GetContentInsertionFrame() override;
+
+ // Refresh the color swatch, using associated input's value
+ nsresult UpdateColor();
+
+ private:
+ explicit nsColorControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ nsCOMPtr<Element> mColorContent;
+};
+
+#endif // nsColorControlFrame_h___
diff --git a/layout/forms/nsComboboxControlFrame.cpp b/layout/forms/nsComboboxControlFrame.cpp
new file mode 100644
index 0000000000..a935e3c761
--- /dev/null
+++ b/layout/forms/nsComboboxControlFrame.cpp
@@ -0,0 +1,965 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsComboboxControlFrame.h"
+
+#include "gfxContext.h"
+#include "gfxUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "nsCOMPtr.h"
+#include "nsDeviceContext.h"
+#include "nsFocusManager.h"
+#include "nsCheckboxRadioFrame.h"
+#include "nsGkAtoms.h"
+#include "nsCSSAnonBoxes.h"
+#include "nsHTMLParts.h"
+#include "nsIFormControl.h"
+#include "nsILayoutHistoryState.h"
+#include "nsNameSpaceManager.h"
+#include "nsListControlFrame.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/PresState.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+#include "nsIContentInlines.h"
+#include "nsIDOMEventListener.h"
+#include "nsISelectControlFrame.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "mozilla/dom/Document.h"
+#include "nsIScrollableFrame.h"
+#include "mozilla/ServoStyleSet.h"
+#include "nsNodeInfoManager.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsLayoutUtils.h"
+#include "nsDisplayList.h"
+#include "nsITheme.h"
+#include "nsStyleConsts.h"
+#include "nsTextFrameUtils.h"
+#include "nsTextRunTransformations.h"
+#include "HTMLSelectEventListener.h"
+#include "mozilla/Likely.h"
+#include <algorithm>
+#include "nsTextNode.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/Unused.h"
+#include "gfx2DGlue.h"
+#include "mozilla/widget/nsAutoRollup.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+NS_IMETHODIMP
+nsComboboxControlFrame::RedisplayTextEvent::Run() {
+ if (mControlFrame) mControlFrame->HandleRedisplayTextEvent();
+ return NS_OK;
+}
+
+// Drop down list event management.
+// The combo box uses the following strategy for managing the drop-down list.
+// If the combo box or its arrow button is clicked on the drop-down list is
+// displayed If mouse exits the combo box with the drop-down list displayed the
+// drop-down list is asked to capture events The drop-down list will capture all
+// events including mouse down and up and will always return with
+// ListWasSelected method call regardless of whether an item in the list was
+// actually selected.
+// The ListWasSelected code will turn off mouse-capture for the drop-down list.
+// The drop-down list does not explicitly set capture when it is in the
+// drop-down mode.
+
+nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ nsComboboxControlFrame* it = new (aPresShell)
+ nsComboboxControlFrame(aStyle, aPresShell->GetPresContext());
+ return it;
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame)
+
+//-----------------------------------------------------------
+// Reflow Debugging Macros
+// These let us "see" how many reflow counts are happening
+//-----------------------------------------------------------
+#ifdef DO_REFLOW_COUNTER
+
+# define MAX_REFLOW_CNT 1024
+static int32_t gTotalReqs = 0;
+;
+static int32_t gTotalReflows = 0;
+;
+static int32_t gReflowControlCntRQ[MAX_REFLOW_CNT];
+static int32_t gReflowControlCnt[MAX_REFLOW_CNT];
+static int32_t gReflowInx = -1;
+
+# define REFLOW_COUNTER() \
+ if (mReflowId > -1) gReflowControlCnt[mReflowId]++;
+
+# define REFLOW_COUNTER_REQUEST() \
+ if (mReflowId > -1) gReflowControlCntRQ[mReflowId]++;
+
+# define REFLOW_COUNTER_DUMP(__desc) \
+ if (mReflowId > -1) { \
+ gTotalReqs += gReflowControlCntRQ[mReflowId]; \
+ gTotalReflows += gReflowControlCnt[mReflowId]; \
+ printf("** Id:%5d %s RF: %d RQ: %d %d/%d %5.2f\n", mReflowId, \
+ (__desc), gReflowControlCnt[mReflowId], \
+ gReflowControlCntRQ[mReflowId], gTotalReflows, gTotalReqs, \
+ float(gTotalReflows) / float(gTotalReqs) * 100.0f); \
+ }
+
+# define REFLOW_COUNTER_INIT() \
+ if (gReflowInx < MAX_REFLOW_CNT) { \
+ gReflowInx++; \
+ mReflowId = gReflowInx; \
+ gReflowControlCnt[mReflowId] = 0; \
+ gReflowControlCntRQ[mReflowId] = 0; \
+ } else { \
+ mReflowId = -1; \
+ }
+
+// reflow messages
+# define REFLOW_DEBUG_MSG(_msg1) printf((_msg1))
+# define REFLOW_DEBUG_MSG2(_msg1, _msg2) printf((_msg1), (_msg2))
+# define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) \
+ printf((_msg1), (_msg2), (_msg3))
+# define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) \
+ printf((_msg1), (_msg2), (_msg3), (_msg4))
+
+#else //-------------
+
+# define REFLOW_COUNTER_REQUEST()
+# define REFLOW_COUNTER()
+# define REFLOW_COUNTER_DUMP(__desc)
+# define REFLOW_COUNTER_INIT()
+
+# define REFLOW_DEBUG_MSG(_msg)
+# define REFLOW_DEBUG_MSG2(_msg1, _msg2)
+# define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3)
+# define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4)
+
+#endif
+
+//------------------------------------------
+// This is for being VERY noisy
+//------------------------------------------
+#ifdef DO_VERY_NOISY
+# define REFLOW_NOISY_MSG(_msg1) printf((_msg1))
+# define REFLOW_NOISY_MSG2(_msg1, _msg2) printf((_msg1), (_msg2))
+# define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) \
+ printf((_msg1), (_msg2), (_msg3))
+# define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) \
+ printf((_msg1), (_msg2), (_msg3), (_msg4))
+#else
+# define REFLOW_NOISY_MSG(_msg)
+# define REFLOW_NOISY_MSG2(_msg1, _msg2)
+# define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3)
+# define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4)
+#endif
+
+//------------------------------------------
+// Displays value in pixels or twips
+//------------------------------------------
+#ifdef DO_PIXELS
+# define PX(__v) __v / 15
+#else
+# define PX(__v) __v
+#endif
+
+//------------------------------------------------------
+//-- Done with macros
+//------------------------------------------------------
+
+nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBlockFrame(aStyle, aPresContext, kClassID),
+ mDisplayFrame(nullptr),
+ mButtonFrame(nullptr),
+ mDisplayISize(0),
+ mMaxDisplayISize(0),
+ mRecentSelectedIndex(NS_SKIP_NOTIFY_INDEX),
+ mDisplayedIndex(-1),
+ mInRedisplayText(false),
+ mIsOpenInParentProcess(false){REFLOW_COUNTER_INIT()}
+
+ //--------------------------------------------------------------
+ nsComboboxControlFrame::~nsComboboxControlFrame() {
+ REFLOW_COUNTER_DUMP("nsCCF");
+}
+
+//--------------------------------------------------------------
+
+NS_QUERYFRAME_HEAD(nsComboboxControlFrame)
+ NS_QUERYFRAME_ENTRY(nsComboboxControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+ NS_QUERYFRAME_ENTRY(nsISelectControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsComboboxControlFrame::AccessibleType() {
+ return a11y::eHTMLComboboxType;
+}
+#endif
+
+void nsComboboxControlFrame::SetFocus(bool aOn, bool aRepaint) {
+ // This is needed on a temporary basis. It causes the focus
+ // rect to be drawn. This is much faster than ReResolvingStyle
+ // Bug 32920
+ InvalidateFrame();
+}
+
+nsPoint nsComboboxControlFrame::GetCSSTransformTranslation() {
+ nsIFrame* frame = this;
+ bool is3DTransform = false;
+ Matrix transform;
+ while (frame) {
+ nsIFrame* parent;
+ Matrix4x4Flagged ctm = frame->GetTransformMatrix(
+ ViewportType::Layout, RelativeTo{nullptr}, &parent);
+ Matrix matrix;
+ if (ctm.Is2D(&matrix)) {
+ transform = transform * matrix;
+ } else {
+ is3DTransform = true;
+ break;
+ }
+ frame = parent;
+ }
+ nsPoint translation;
+ if (!is3DTransform && !transform.HasNonTranslation()) {
+ nsPresContext* pc = PresContext();
+ // To get the translation introduced only by transforms we subtract the
+ // regular non-transform translation.
+ nsRootPresContext* rootPC = pc->GetRootPresContext();
+ if (rootPC) {
+ int32_t apd = pc->AppUnitsPerDevPixel();
+ translation.x = NSFloatPixelsToAppUnits(transform._31, apd);
+ translation.y = NSFloatPixelsToAppUnits(transform._32, apd);
+ translation -= GetOffsetToCrossDoc(rootPC->PresShell()->GetRootFrame());
+ }
+ }
+ return translation;
+}
+
+//----------------------------------------------------------
+//
+//----------------------------------------------------------
+#ifdef DO_REFLOW_DEBUG
+static int myCounter = 0;
+
+static void printSize(char* aDesc, nscoord aSize) {
+ printf(" %s: ", aDesc);
+ if (aSize == NS_UNCONSTRAINEDSIZE) {
+ printf("UC");
+ } else {
+ printf("%d", PX(aSize));
+ }
+}
+#endif
+
+//-------------------------------------------------------------------
+//-- Main Reflow for the Combobox
+//-------------------------------------------------------------------
+
+bool nsComboboxControlFrame::HasDropDownButton() const {
+ const nsStyleDisplay* disp = StyleDisplay();
+ // FIXME(emilio): Blink also shows this for menulist-button and such... Seems
+ // more similar to our mac / linux implementation.
+ return disp->EffectiveAppearance() == StyleAppearance::Menulist &&
+ (!IsThemed(disp) ||
+ PresContext()->Theme()->ThemeNeedsComboboxDropmarker());
+}
+
+nscoord nsComboboxControlFrame::DropDownButtonISize() {
+ if (!HasDropDownButton()) {
+ return 0;
+ }
+
+ nsPresContext* pc = PresContext();
+ LayoutDeviceIntSize dropdownButtonSize = pc->Theme()->GetMinimumWidgetSize(
+ pc, this, StyleAppearance::MozMenulistArrowButton);
+ return pc->DevPixelsToAppUnits(dropdownButtonSize.width);
+}
+
+int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const {
+ uint32_t maxLength = 0;
+ nsAutoString label;
+ for (auto i : IntegerRange(Select().Options()->Length())) {
+ GetOptionText(i, label);
+ maxLength = std::max(
+ maxLength,
+ nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
+ label, StyleText()));
+ }
+ if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) {
+ return INT32_MAX;
+ }
+ return int32_t(maxLength);
+}
+
+nscoord nsComboboxControlFrame::GetLongestOptionISize(
+ gfxContext* aRenderingContext) const {
+ // Compute the width of each option's (potentially text-transformed) text,
+ // and use the widest one as part of our intrinsic size.
+ nscoord maxOptionSize = 0;
+ nsAutoString label;
+ nsAutoString transformedLabel;
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
+ const nsStyleText* textStyle = StyleText();
+ auto textTransform = textStyle->mTextTransform.IsNone()
+ ? Nothing()
+ : Some(textStyle->mTextTransform);
+ nsAtom* language = StyleFont()->mLanguage;
+ AutoTArray<bool, 50> charsToMergeArray;
+ AutoTArray<bool, 50> deletedCharsArray;
+ for (auto i : IntegerRange(Select().Options()->Length())) {
+ GetOptionText(i, label);
+ const nsAutoString* stringToUse = &label;
+ if (textTransform ||
+ textStyle->mWebkitTextSecurity != StyleTextSecurity::None) {
+ transformedLabel.Truncate();
+ charsToMergeArray.SetLengthAndRetainStorage(0);
+ deletedCharsArray.SetLengthAndRetainStorage(0);
+ nsCaseTransformTextRunFactory::TransformString(
+ label, transformedLabel, textTransform,
+ textStyle->TextSecurityMaskChar(),
+ /* aCaseTransformsOnly = */ false, language, charsToMergeArray,
+ deletedCharsArray);
+ stringToUse = &transformedLabel;
+ }
+ maxOptionSize = std::max(maxOptionSize,
+ nsLayoutUtils::AppUnitWidthOfStringBidi(
+ *stringToUse, this, *fm, *aRenderingContext));
+ }
+ if (maxOptionSize) {
+ // HACK: Add one app unit to workaround silly Netgear router styling, see
+ // bug 1769580. In practice since this comes from font metrics is unlikely
+ // to be perceivable.
+ maxOptionSize += 1;
+ }
+ return maxOptionSize;
+}
+
+nscoord nsComboboxControlFrame::GetIntrinsicISize(gfxContext* aRenderingContext,
+ IntrinsicISizeType aType) {
+ Maybe<nscoord> containISize = ContainIntrinsicISize(NS_UNCONSTRAINEDSIZE);
+ if (containISize && *containISize != NS_UNCONSTRAINEDSIZE) {
+ return *containISize;
+ }
+
+ nscoord displayISize = mDisplayFrame->IntrinsicISizeOffsets().padding;
+ if (!containISize && !StyleContent()->mContent.IsNone()) {
+ displayISize += GetLongestOptionISize(aRenderingContext);
+ }
+
+ // Add room for the dropmarker button (if there is one).
+ displayISize += DropDownButtonISize();
+ return displayISize;
+}
+
+nscoord nsComboboxControlFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord minISize;
+ DISPLAY_MIN_INLINE_SIZE(this, minISize);
+ minISize = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize);
+ return minISize;
+}
+
+nscoord nsComboboxControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord prefISize;
+ DISPLAY_PREF_INLINE_SIZE(this, prefISize);
+ prefISize =
+ GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize);
+ return prefISize;
+}
+
+dom::HTMLSelectElement& nsComboboxControlFrame::Select() const {
+ return *static_cast<dom::HTMLSelectElement*>(GetContent());
+}
+
+void nsComboboxControlFrame::GetOptionText(uint32_t aIndex,
+ nsAString& aText) const {
+ aText.Truncate();
+ if (Element* el = Select().Options()->GetElementAt(aIndex)) {
+ static_cast<dom::HTMLOptionElement*>(el)->GetRenderedLabel(aText);
+ }
+}
+
+void nsComboboxControlFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ // Constraints we try to satisfy:
+
+ // 1) Default inline size of button is the vertical scrollbar size
+ // 2) If the inline size of button is bigger than our inline size, set
+ // inline size of button to 0.
+ // 3) Default block size of button is block size of display area
+ // 4) Inline size of display area is whatever is left over from our
+ // inline size after allocating inline size for the button.
+
+ if (!mDisplayFrame) {
+ NS_ERROR("Why did the frame constructor allow this to happen? Fix it!!");
+ return;
+ }
+
+ // Make sure the displayed text is the same as the selected option,
+ // bug 297389.
+ mDisplayedIndex = Select().SelectedIndex();
+
+ // In dropped down mode the "selected index" is the hovered menu item,
+ // we want the last selected item which is |mDisplayedIndex| in this case.
+ RedisplayText();
+
+ WritingMode wm = aReflowInput.GetWritingMode();
+
+ // Check if the theme specifies a minimum size for the dropdown button
+ // first.
+ const nscoord buttonISize = DropDownButtonISize();
+ const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm);
+ const auto padding = aReflowInput.ComputedLogicalPadding(wm);
+ const auto border = borderPadding - padding;
+
+ mDisplayISize = aReflowInput.ComputedISize() - buttonISize;
+ mMaxDisplayISize = mDisplayISize + padding.IEnd(wm);
+
+ nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus);
+
+ // The button should occupy the same space as a scrollbar, and its position
+ // starts from the border edge.
+ if (mButtonFrame) {
+ LogicalRect buttonRect(wm);
+ buttonRect.IStart(wm) = borderPadding.IStart(wm) + mMaxDisplayISize;
+ buttonRect.BStart(wm) = border.BStart(wm);
+
+ buttonRect.ISize(wm) = buttonISize;
+ buttonRect.BSize(wm) = mDisplayFrame->BSize(wm) + padding.BStartEnd(wm);
+
+ const nsSize containerSize = aDesiredSize.PhysicalSize();
+ mButtonFrame->SetRect(buttonRect, containerSize);
+ }
+
+ if (!aStatus.IsInlineBreakBefore() && !aStatus.IsFullyComplete()) {
+ // This frame didn't fit inside a fragmentation container. Splitting
+ // a nsComboboxControlFrame makes no sense, so we override the status here.
+ aStatus.Reset();
+ }
+}
+
+void nsComboboxControlFrame::Init(nsIContent* aContent,
+ nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
+
+ mEventListener = new HTMLSelectEventListener(
+ Select(), HTMLSelectEventListener::SelectType::Combobox);
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsComboboxControlFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"ComboboxControl"_ns, aResult);
+}
+#endif
+
+///////////////////////////////////////////////////////////////
+
+nsresult nsComboboxControlFrame::RedisplaySelectedText() {
+ nsAutoScriptBlocker scriptBlocker;
+ mDisplayedIndex = Select().SelectedIndex();
+ return RedisplayText();
+}
+
+nsresult nsComboboxControlFrame::RedisplayText() {
+ nsString previewValue;
+ nsString previousText(mDisplayedOptionTextOrPreview);
+
+ Select().GetPreviewValue(previewValue);
+ // Get the text to display
+ if (!previewValue.IsEmpty()) {
+ mDisplayedOptionTextOrPreview = previewValue;
+ } else if (mDisplayedIndex != -1 && !StyleContent()->mContent.IsNone()) {
+ GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview);
+ } else {
+ mDisplayedOptionTextOrPreview.Truncate();
+ }
+
+ REFLOW_DEBUG_MSG2(
+ "RedisplayText \"%s\"\n",
+ NS_LossyConvertUTF16toASCII(mDisplayedOptionTextOrPreview).get());
+
+ // Send reflow command because the new text maybe larger
+ nsresult rv = NS_OK;
+ if (mDisplayContent && !previousText.Equals(mDisplayedOptionTextOrPreview)) {
+ // Don't call ActuallyDisplayText(true) directly here since that
+ // could cause recursive frame construction. See bug 283117 and the comment
+ // in HandleRedisplayTextEvent() below.
+
+ // Revoke outstanding events to avoid out-of-order events which could mean
+ // displaying the wrong text.
+ mRedisplayTextEvent.Revoke();
+
+ NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
+ "If we happen to run our redisplay event now, we might kill "
+ "ourselves!");
+
+ mRedisplayTextEvent = new RedisplayTextEvent(this);
+ nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get());
+ }
+ return rv;
+}
+
+void nsComboboxControlFrame::HandleRedisplayTextEvent() {
+ // First, make sure that the content model is up to date and we've
+ // constructed the frames for all our content in the right places.
+ // Otherwise they'll end up under the wrong insertion frame when we
+ // ActuallyDisplayText, since that flushes out the content sink by
+ // calling SetText on a DOM node with aNotify set to true. See bug
+ // 289730.
+ AutoWeakFrame weakThis(this);
+ PresContext()->Document()->FlushPendingNotifications(
+ FlushType::ContentAndNotify);
+ if (!weakThis.IsAlive()) return;
+
+ // Redirect frame insertions during this method (see
+ // GetContentInsertionFrame()) so that any reframing that the frame
+ // constructor forces upon us is inserted into the correct parent
+ // (mDisplayFrame). See bug 282607.
+ MOZ_ASSERT(!mInRedisplayText, "Nested RedisplayText");
+ mInRedisplayText = true;
+ mRedisplayTextEvent.Forget();
+
+ ActuallyDisplayText(true);
+ if (!weakThis.IsAlive()) {
+ return;
+ }
+
+ // XXXbz This should perhaps be IntrinsicDirty::None. Check.
+ PresShell()->FrameNeedsReflow(mDisplayFrame,
+ IntrinsicDirty::FrameAncestorsAndDescendants,
+ NS_FRAME_IS_DIRTY);
+
+ mInRedisplayText = false;
+}
+
+void nsComboboxControlFrame::ActuallyDisplayText(bool aNotify) {
+ RefPtr<nsTextNode> displayContent = mDisplayContent;
+ if (mDisplayedOptionTextOrPreview.IsEmpty()) {
+ // Have to use a space character of some sort for line-block-size
+ // calculations to be right. Also, the space character must be zero-width
+ // in order for the the inline-size calculations to be consistent between
+ // size-contained comboboxes vs. empty comboboxes.
+ //
+ // XXXdholbert Does this space need to be "non-breaking"? I'm not sure
+ // if it matters, but we previously had a comment here (added in 2002)
+ // saying "Have to use a non-breaking space for line-height calculations
+ // to be right". So I'll stick with a non-breaking space for now...
+ static const char16_t space = 0xFEFF;
+ displayContent->SetText(&space, 1, aNotify);
+ } else {
+ displayContent->SetText(mDisplayedOptionTextOrPreview, aNotify);
+ }
+}
+
+int32_t nsComboboxControlFrame::GetIndexOfDisplayArea() {
+ return mDisplayedIndex;
+}
+
+bool nsComboboxControlFrame::IsDroppedDown() const {
+ return Select().OpenInParentProcess();
+}
+
+//----------------------------------------------------------------------
+// nsISelectControlFrame
+//----------------------------------------------------------------------
+NS_IMETHODIMP
+nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; }
+
+NS_IMETHODIMP
+nsComboboxControlFrame::AddOption(int32_t aIndex) {
+ if (aIndex <= mDisplayedIndex) {
+ ++mDisplayedIndex;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComboboxControlFrame::RemoveOption(int32_t aIndex) {
+ if (Select().Options()->Length()) {
+ if (aIndex < mDisplayedIndex) {
+ --mDisplayedIndex;
+ } else if (aIndex == mDisplayedIndex) {
+ mDisplayedIndex = 0; // IE6 compat
+ RedisplayText();
+ }
+ } else {
+ // If we removed the last option, we need to blank things out
+ mDisplayedIndex = -1;
+ RedisplayText();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex,
+ int32_t aNewIndex) {
+ nsAutoScriptBlocker scriptBlocker;
+ mDisplayedIndex = aNewIndex;
+ RedisplayText();
+}
+
+// End nsISelectControlFrame
+//----------------------------------------------------------------------
+
+nsresult nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ if (mContent->AsElement()->State().HasState(dom::ElementState::DISABLED)) {
+ return NS_OK;
+ }
+
+ // If we have style that affects how we are selected, feed event down to
+ // nsIFrame::HandleEvent so that selection takes place when appropriate.
+ if (IsContentDisabled()) {
+ return nsBlockFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+ return NS_OK;
+}
+
+nsContainerFrame* nsComboboxControlFrame::GetContentInsertionFrame() {
+ return mInRedisplayText ? mDisplayFrame : nullptr;
+}
+
+void nsComboboxControlFrame::AppendDirectlyOwnedAnonBoxes(
+ nsTArray<OwnedAnonBox>& aResult) {
+ aResult.AppendElement(OwnedAnonBox(mDisplayFrame));
+}
+
+nsresult nsComboboxControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ // The frames used to display the combo box and the button used to popup the
+ // dropdown list are created through anonymous content. The dropdown list is
+ // not created through anonymous content because its frame is initialized
+ // specifically for the drop-down case and it is placed a special list
+ // referenced through NS_COMBO_FRAME_POPUP_LIST_INDEX to keep separate from
+ // the layout of the display and button.
+ //
+ // Note: The value attribute of the display content is set when an item is
+ // selected in the dropdown list. If the content specified below does not
+ // honor the value attribute than nothing will be displayed.
+
+ // For now the content that is created corresponds to two input buttons. It
+ // would be better to create the tag as something other than input, but then
+ // there isn't any way to create a button frame since it isn't possible to set
+ // the display type in CSS2 to create a button frame.
+
+ // create content used for display
+ // nsAtom* tag = NS_Atomize("mozcombodisplay");
+
+ // Add a child text content node for the label
+
+ nsNodeInfoManager* nimgr = mContent->NodeInfo()->NodeInfoManager();
+
+ mDisplayContent = new (nimgr) nsTextNode(nimgr);
+
+ // set the value of the text node
+ mDisplayedIndex = Select().SelectedIndex();
+ if (mDisplayedIndex != -1) {
+ GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview);
+ }
+ ActuallyDisplayText(false);
+
+ aElements.AppendElement(mDisplayContent);
+ if (HasDropDownButton()) {
+ mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button);
+ if (!mButtonContent) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // make someone to listen to the button.
+ mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"button"_ns,
+ false);
+ // Set tabindex="-1" so that the button is not tabbable
+ mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
+ false);
+ aElements.AppendElement(mButtonContent);
+ }
+
+ return NS_OK;
+}
+
+void nsComboboxControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mDisplayContent) {
+ aElements.AppendElement(mDisplayContent);
+ }
+
+ if (mButtonContent) {
+ aElements.AppendElement(mButtonContent);
+ }
+}
+
+nsIContent* nsComboboxControlFrame::GetDisplayNode() const {
+ return mDisplayContent;
+}
+
+// XXXbz this is a for-now hack. Now that display:inline-block works,
+// need to revisit this.
+class nsComboboxDisplayFrame final : public nsBlockFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame)
+
+ nsComboboxDisplayFrame(ComputedStyle* aStyle,
+ nsComboboxControlFrame* aComboBox)
+ : nsBlockFrame(aStyle, aComboBox->PresContext(), kClassID),
+ mComboBox(aComboBox) {}
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const final {
+ return MakeFrameName(u"ComboboxDisplay"_ns, aResult);
+ }
+#endif
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) final;
+
+ protected:
+ nsComboboxControlFrame* mComboBox;
+};
+
+NS_IMPL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame)
+
+void nsComboboxDisplayFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ MOZ_ASSERT(aReflowInput.mParentReflowInput &&
+ aReflowInput.mParentReflowInput->mFrame == mComboBox,
+ "Combobox's frame tree is wrong!");
+
+ ReflowInput state(aReflowInput);
+ if (state.ComputedBSize() == NS_UNCONSTRAINEDSIZE) {
+ state.SetLineHeight(state.mParentReflowInput->GetLineHeight());
+ }
+ const WritingMode wm = aReflowInput.GetWritingMode();
+ const LogicalMargin bp = state.ComputedLogicalBorderPadding(wm);
+ MOZ_ASSERT(bp.BStartEnd(wm) == 0,
+ "We shouldn't have border and padding in the block axis in UA!");
+ nscoord inlineBp = bp.IStartEnd(wm);
+ nscoord computedISize = mComboBox->mDisplayISize - inlineBp;
+
+ // Other UAs ignore padding in some (but not all) platforms for (themed only)
+ // comboboxes. Instead of doing that, we prevent that padding if present from
+ // clipping the display text, by enforcing the display text minimum size in
+ // that situation.
+ const bool shouldHonorMinISize =
+ mComboBox->StyleDisplay()->EffectiveAppearance() ==
+ StyleAppearance::Menulist;
+ if (shouldHonorMinISize) {
+ computedISize = std::max(state.ComputedMinISize(), computedISize);
+ // Don't let this size go over mMaxDisplayISize, since that'd be
+ // observable via clientWidth / scrollWidth.
+ computedISize =
+ std::min(computedISize, mComboBox->mMaxDisplayISize - inlineBp);
+ }
+
+ state.SetComputedISize(std::max(0, computedISize));
+ nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
+ aStatus.Reset(); // this type of frame can't be split
+}
+
+void nsComboboxDisplayFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ nsDisplayListCollection set(aBuilder);
+ nsBlockFrame::BuildDisplayList(aBuilder, set);
+
+ // remove background items if parent frame is themed
+ if (mComboBox->IsThemed()) {
+ set.BorderBackground()->DeleteAll(aBuilder);
+ }
+
+ set.MoveTo(aLists);
+}
+
+nsIFrame* nsComboboxControlFrame::CreateFrameForDisplayNode() {
+ MOZ_ASSERT(mDisplayContent);
+
+ // Get PresShell
+ mozilla::PresShell* ps = PresShell();
+ ServoStyleSet* styleSet = ps->StyleSet();
+
+ // create the ComputedStyle for the anonymous block frame and text frame
+ RefPtr<ComputedStyle> computedStyle =
+ styleSet->ResolveInheritingAnonymousBoxStyle(
+ PseudoStyleType::mozDisplayComboboxControlFrame, mComputedStyle);
+
+ RefPtr<ComputedStyle> textComputedStyle =
+ styleSet->ResolveStyleForText(mDisplayContent, mComputedStyle);
+
+ // Start by creating our anonymous block frame
+ mDisplayFrame = new (ps) nsComboboxDisplayFrame(computedStyle, this);
+ mDisplayFrame->Init(mContent, this, nullptr);
+
+ // Create a text frame and put it inside the block frame
+ nsIFrame* textFrame = NS_NewTextFrame(ps, textComputedStyle);
+
+ // initialize the text frame
+ textFrame->Init(mDisplayContent, mDisplayFrame, nullptr);
+ mDisplayContent->SetPrimaryFrame(textFrame);
+
+ mDisplayFrame->SetInitialChildList(FrameChildListID::Principal,
+ nsFrameList(textFrame, textFrame));
+ return mDisplayFrame;
+}
+
+void nsComboboxControlFrame::Destroy(DestroyContext& aContext) {
+ // Revoke any pending RedisplayTextEvent
+ mRedisplayTextEvent.Revoke();
+
+ mEventListener->Detach();
+
+ // Cleanup frames in popup child list
+ aContext.AddAnonymousContent(mDisplayContent.forget());
+ aContext.AddAnonymousContent(mButtonContent.forget());
+ nsBlockFrame::Destroy(aContext);
+}
+
+const nsFrameList& nsComboboxControlFrame::GetChildList(
+ ChildListID aListID) const {
+ return nsBlockFrame::GetChildList(aListID);
+}
+
+void nsComboboxControlFrame::GetChildLists(nsTArray<ChildList>* aLists) const {
+ nsBlockFrame::GetChildLists(aLists);
+}
+
+void nsComboboxControlFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ for (nsIFrame* f : aChildList) {
+ MOZ_ASSERT(f->GetParent() == this, "Unexpected parent");
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(f->GetContent());
+ if (formControl &&
+ formControl->ControlType() == FormControlType::ButtonButton) {
+ mButtonFrame = f;
+ break;
+ }
+ }
+ nsBlockFrame::SetInitialChildList(aListID, std::move(aChildList));
+}
+
+namespace mozilla {
+
+class nsDisplayComboboxFocus : public nsPaintedDisplayItem {
+ public:
+ nsDisplayComboboxFocus(nsDisplayListBuilder* aBuilder,
+ nsComboboxControlFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayComboboxFocus);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayComboboxFocus)
+
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+ NS_DISPLAY_DECL_NAME("ComboboxFocus", TYPE_COMBOBOX_FOCUS)
+};
+
+void nsDisplayComboboxFocus::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ static_cast<nsComboboxControlFrame*>(mFrame)->PaintFocus(
+ *aCtx->GetDrawTarget(), ToReferenceFrame());
+}
+
+} // namespace mozilla
+
+void nsComboboxControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ if (aBuilder->IsForEventDelivery()) {
+ // Don't allow children to receive events.
+ // REVIEW: following old GetFrameForPoint
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+ } else {
+ // REVIEW: Our in-flow child frames are inline-level so they will paint in
+ // our content list, so we don't need to mess with layers.
+ nsBlockFrame::BuildDisplayList(aBuilder, aLists);
+ }
+
+ // draw a focus indicator only when focus rings should be drawn
+ if (Select().State().HasState(dom::ElementState::FOCUSRING) && IsThemed() &&
+ PresContext()->Theme()->ThemeWantsButtonInnerFocusRing()) {
+ aLists.Content()->AppendNewToTop<nsDisplayComboboxFocus>(aBuilder, this);
+ }
+
+ DisplaySelectionOverlay(aBuilder, aLists.Content());
+}
+
+void nsComboboxControlFrame::PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt) {
+ /* Do we need to do anything? */
+ dom::ElementState state = mContent->AsElement()->State();
+ if (state.HasState(dom::ElementState::DISABLED) ||
+ !state.HasState(dom::ElementState::FOCUS)) {
+ return;
+ }
+
+ int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
+
+ nsRect clipRect = mDisplayFrame->GetRect() + aPt;
+ aDrawTarget.PushClipRect(
+ NSRectToSnappedRect(clipRect, appUnitsPerDevPixel, aDrawTarget));
+
+ StrokeOptions strokeOptions;
+ nsLayoutUtils::InitDashPattern(strokeOptions, StyleBorderStyle::Dotted);
+ ColorPattern color(ToDeviceColor(StyleText()->mColor));
+ nscoord onePixel = nsPresContext::CSSPixelsToAppUnits(1);
+ clipRect.width -= onePixel;
+ clipRect.height -= onePixel;
+ Rect r = ToRect(nsLayoutUtils::RectToGfxRect(clipRect, appUnitsPerDevPixel));
+ StrokeSnappedEdgesOfRect(r, aDrawTarget, color, strokeOptions);
+
+ aDrawTarget.PopClip();
+}
+
+//---------------------------------------------------------
+// gets the content (an option) by index and then set it as
+// being selected or not selected
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) {
+ if (aSelected) {
+ nsAutoScriptBlocker blocker;
+ mDisplayedIndex = aIndex;
+ RedisplayText();
+ } else {
+ AutoWeakFrame weakFrame(this);
+ RedisplaySelectedText();
+ if (weakFrame.IsAlive()) {
+ FireValueChangeEvent(); // Fire after old option is unselected
+ }
+ }
+ return NS_OK;
+}
+
+void nsComboboxControlFrame::FireValueChangeEvent() {
+ // Fire ValueChange event to indicate data value of combo box has changed
+ nsContentUtils::AddScriptRunner(new AsyncEventDispatcher(
+ mContent, u"ValueChange"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo));
+}
diff --git a/layout/forms/nsComboboxControlFrame.h b/layout/forms/nsComboboxControlFrame.h
new file mode 100644
index 0000000000..e878c0ebe3
--- /dev/null
+++ b/layout/forms/nsComboboxControlFrame.h
@@ -0,0 +1,244 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsComboboxControlFrame_h___
+#define nsComboboxControlFrame_h___
+
+#ifdef DEBUG_evaughan
+// #define DEBUG_rods
+#endif
+
+#ifdef DEBUG_rods
+// #define DO_REFLOW_DEBUG
+// #define DO_REFLOW_COUNTER
+// #define DO_UNCONSTRAINED_CHECK
+// #define DO_PIXELS
+// #define DO_NEW_REFLOW
+#endif
+
+// Mark used to indicate when onchange has been fired for current combobox item
+#define NS_SKIP_NOTIFY_INDEX -2
+
+#include "mozilla/Attributes.h"
+#include "nsBlockFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsISelectControlFrame.h"
+#include "nsIRollupListener.h"
+#include "nsThreadUtils.h"
+
+class nsComboboxDisplayFrame;
+class nsTextNode;
+
+namespace mozilla {
+class PresShell;
+class HTMLSelectEventListener;
+namespace dom {
+class HTMLSelectElement;
+}
+
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+} // namespace mozilla
+
+class nsComboboxControlFrame final : public nsBlockFrame,
+ public nsIFormControlFrame,
+ public nsIAnonymousContentCreator,
+ public nsISelectControlFrame {
+ using DrawTarget = mozilla::gfx::DrawTarget;
+ using Element = mozilla::dom::Element;
+
+ public:
+ friend nsComboboxControlFrame* NS_NewComboboxControlFrame(
+ mozilla::PresShell* aPresShell, ComputedStyle* aStyle);
+ friend class nsComboboxDisplayFrame;
+
+ explicit nsComboboxControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+ ~nsComboboxControlFrame();
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsComboboxControlFrame)
+
+ // nsIAnonymousContentCreator
+ nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) final;
+ void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) final;
+
+ nsIContent* GetDisplayNode() const;
+ nsIFrame* CreateFrameForDisplayNode();
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() final;
+#endif
+
+ nscoord GetMinISize(gfxContext* aRenderingContext) final;
+
+ nscoord GetPrefISize(gfxContext* aRenderingContext) final;
+
+ void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) final;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) final;
+
+ void PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt);
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) final;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const final;
+#endif
+ void Destroy(DestroyContext&) final;
+
+ void SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) final;
+ const nsFrameList& GetChildList(ChildListID aListID) const final;
+ void GetChildLists(nsTArray<ChildList>* aLists) const final;
+
+ nsContainerFrame* GetContentInsertionFrame() final;
+
+ // Return the dropdown and display frame.
+ void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) final;
+
+ // nsIFormControlFrame
+ nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final {
+ return NS_OK;
+ }
+
+ /**
+ * Inform the control that it got (or lost) focus.
+ * If it lost focus, the dropdown menu will be rolled up if needed,
+ * and FireOnChange() will be called.
+ * @param aOn true if got focus, false if lost focus.
+ * @param aRepaint if true then force repaint (NOTE: we always force repaint
+ * currently)
+ * @note This method might destroy |this|.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void SetFocus(bool aOn, bool aRepaint) final;
+
+ /**
+ * Return the available space before and after this frame for
+ * placing the drop-down list, and the current 2D translation.
+ * Note that either or both can be less than or equal to zero,
+ * if both are then the drop-down should be closed.
+ */
+ void GetAvailableDropdownSpace(mozilla::WritingMode aWM, nscoord* aBefore,
+ nscoord* aAfter,
+ mozilla::LogicalPoint* aTranslation);
+ int32_t GetIndexOfDisplayArea();
+ /**
+ * @note This method might destroy |this|.
+ */
+ nsresult RedisplaySelectedText();
+ int32_t UpdateRecentIndex(int32_t aIndex);
+
+ bool IsDroppedDown() const;
+
+ // nsISelectControlFrame
+ NS_IMETHOD AddOption(int32_t index) final;
+ NS_IMETHOD RemoveOption(int32_t index) final;
+ NS_IMETHOD DoneAddingChildren(bool aIsDone) final;
+ NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) final;
+ NS_IMETHOD_(void)
+ OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) final;
+
+ int32_t CharCountOfLargestOptionForInflation() const;
+
+ protected:
+ friend class RedisplayTextEvent;
+ friend class nsAsyncResize;
+ friend class nsResizeDropdownAtFinalPosition;
+
+ // Return true if we should render a dropdown button.
+ bool HasDropDownButton() const;
+ nscoord DropDownButtonISize();
+
+ enum DropDownPositionState {
+ // can't show the dropdown at its current position
+ eDropDownPositionSuppressed,
+ // a resize reflow is pending, don't show it yet
+ eDropDownPositionPendingResize,
+ // the dropdown has its final size and position and can be displayed here
+ eDropDownPositionFinal
+ };
+ DropDownPositionState AbsolutelyPositionDropDown();
+
+ nscoord GetLongestOptionISize(gfxContext*) const;
+
+ // Helper for GetMinISize/GetPrefISize
+ nscoord GetIntrinsicISize(gfxContext* aRenderingContext,
+ mozilla::IntrinsicISizeType aType);
+
+ class RedisplayTextEvent : public mozilla::Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+ explicit RedisplayTextEvent(nsComboboxControlFrame* c)
+ : mozilla::Runnable("nsComboboxControlFrame::RedisplayTextEvent"),
+ mControlFrame(c) {}
+ void Revoke() { mControlFrame = nullptr; }
+
+ private:
+ nsComboboxControlFrame* mControlFrame;
+ };
+
+ void CheckFireOnChange();
+ void FireValueChangeEvent();
+ nsresult RedisplayText();
+ void HandleRedisplayTextEvent();
+ void ActuallyDisplayText(bool aNotify);
+
+ // If our total transform to the root frame of the root document is only a 2d
+ // translation then return that translation, otherwise returns (0,0).
+ nsPoint GetCSSTransformTranslation();
+
+ mozilla::dom::HTMLSelectElement& Select() const;
+ void GetOptionText(uint32_t aIndex, nsAString& aText) const;
+
+ RefPtr<nsTextNode> mDisplayContent; // Anonymous content used to display the
+ // current selection
+ RefPtr<Element> mButtonContent; // Anonymous content for the button
+ nsContainerFrame* mDisplayFrame; // frame to display selection
+ nsIFrame* mButtonFrame; // button frame
+
+ // The inline size of our display area. Used by that frame's reflow
+ // to size to the full inline size except the drop-marker.
+ nscoord mDisplayISize;
+ // The maximum inline size of our display area, which is the
+ // nsComoboxControlFrame's border-box.
+ //
+ // Going over this would be observable via DOM APIs like client / scrollWidth.
+ nscoord mMaxDisplayISize;
+
+ nsRevocableEventPtr<RedisplayTextEvent> mRedisplayTextEvent;
+
+ int32_t mRecentSelectedIndex;
+ int32_t mDisplayedIndex;
+ nsString mDisplayedOptionTextOrPreview;
+
+ RefPtr<mozilla::HTMLSelectEventListener> mEventListener;
+
+ // See comment in HandleRedisplayTextEvent().
+ bool mInRedisplayText;
+ bool mIsOpenInParentProcess;
+
+ // static class data member for Bug 32920
+ // only one control can be focused at a time
+ static nsComboboxControlFrame* sFocused;
+
+#ifdef DO_REFLOW_COUNTER
+ int32_t mReflowId;
+#endif
+};
+
+#endif
diff --git a/layout/forms/nsDateTimeControlFrame.cpp b/layout/forms/nsDateTimeControlFrame.cpp
new file mode 100644
index 0000000000..b19f787dbc
--- /dev/null
+++ b/layout/forms/nsDateTimeControlFrame.cpp
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This frame type is used for input type=date, time, month, week, and
+ * datetime-local.
+ */
+
+#include "nsDateTimeControlFrame.h"
+
+#include "mozilla/PresShell.h"
+#include "nsLayoutUtils.h"
+#include "nsTextControlFrame.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame* NS_NewDateTimeControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsDateTimeControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsDateTimeControlFrame)
+
+NS_QUERYFRAME_HEAD(nsDateTimeControlFrame)
+ NS_QUERYFRAME_ENTRY(nsDateTimeControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+nsDateTimeControlFrame::nsDateTimeControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID) {}
+
+nscoord nsDateTimeControlFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+
+ nsIFrame* kid = mFrames.FirstChild();
+ if (kid) { // display:none?
+ result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, kid,
+ IntrinsicISizeType::MinISize);
+ } else {
+ result = 0;
+ }
+
+ return result;
+}
+
+nscoord nsDateTimeControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+
+ nsIFrame* kid = mFrames.FirstChild();
+ if (kid) { // display:none?
+ result = nsLayoutUtils::IntrinsicForContainer(
+ aRenderingContext, kid, IntrinsicISizeType::PrefISize);
+ } else {
+ result = 0;
+ }
+
+ return result;
+}
+
+Maybe<nscoord> nsDateTimeControlFrame::GetNaturalBaselineBOffset(
+ WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext) const {
+ return nsTextControlFrame::GetSingleLineTextControlBaseline(
+ this, mFirstBaseline, aWM, aBaselineGroup);
+}
+
+void nsDateTimeControlFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+
+ DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ NS_FRAME_TRACE(
+ NS_FRAME_TRACE_CALLS,
+ ("enter nsDateTimeControlFrame::Reflow: availSize=%d,%d",
+ aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight()));
+
+ NS_ASSERTION(mFrames.GetLength() <= 1,
+ "There should be no more than 1 frames");
+
+ const WritingMode myWM = aReflowInput.GetWritingMode();
+
+ {
+ auto baseline = nsTextControlFrame::ComputeBaseline(
+ this, aReflowInput, /* aForSingleLineControl = */ true);
+ mFirstBaseline = baseline.valueOr(NS_INTRINSIC_ISIZE_UNKNOWN);
+ if (baseline) {
+ aDesiredSize.SetBlockStartAscent(*baseline);
+ }
+ }
+
+ // The ISize of our content box, which is the available ISize
+ // for our anonymous content:
+ const nscoord contentBoxISize = aReflowInput.ComputedISize();
+ nscoord contentBoxBSize = aReflowInput.ComputedBSize();
+
+ // Figure out our border-box sizes as well (by adding borderPadding to
+ // content-box sizes):
+ const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(myWM);
+ const nscoord borderBoxISize =
+ contentBoxISize + borderPadding.IStartEnd(myWM);
+
+ nscoord borderBoxBSize;
+ if (contentBoxBSize != NS_UNCONSTRAINEDSIZE) {
+ borderBoxBSize = contentBoxBSize + borderPadding.BStartEnd(myWM);
+ } // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize.
+
+ nsIFrame* inputAreaFrame = mFrames.FirstChild();
+ if (!inputAreaFrame) { // display:none?
+ if (contentBoxBSize == NS_UNCONSTRAINEDSIZE) {
+ contentBoxBSize = 0;
+ borderBoxBSize = borderPadding.BStartEnd(myWM);
+ }
+ } else {
+ ReflowOutput childDesiredSize(aReflowInput);
+
+ WritingMode wm = inputAreaFrame->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+
+ ReflowInput childReflowInput(aPresContext, aReflowInput, inputAreaFrame,
+ availSize);
+
+ // Convert input area margin into my own writing-mode (in case it differs):
+ LogicalMargin childMargin = childReflowInput.ComputedLogicalMargin(myWM);
+
+ // offsets of input area frame within this frame:
+ LogicalPoint childOffset =
+ borderPadding.StartOffset(myWM) + childMargin.StartOffset(myWM);
+
+ nsReflowStatus childStatus;
+ // We initially reflow the child with a dummy containerSize; positioning
+ // will be fixed later.
+ const nsSize dummyContainerSize;
+ ReflowChild(inputAreaFrame, aPresContext, childDesiredSize,
+ childReflowInput, myWM, childOffset, dummyContainerSize,
+ ReflowChildFlags::Default, childStatus);
+ MOZ_ASSERT(childStatus.IsFullyComplete(),
+ "We gave our child unconstrained available block-size, "
+ "so it should be complete");
+
+ nscoord childMarginBoxBSize =
+ childDesiredSize.BSize(myWM) + childMargin.BStartEnd(myWM);
+
+ if (contentBoxBSize == NS_UNCONSTRAINEDSIZE) {
+ // We are intrinsically sized -- we should shrinkwrap the input area's
+ // block-size, or our line-height:
+ contentBoxBSize =
+ std::max(aReflowInput.GetLineHeight(), childMarginBoxBSize);
+
+ // Make sure we obey min/max-bsize in the case when we're doing intrinsic
+ // sizing (we get it for free when we have a non-intrinsic
+ // aReflowInput.ComputedBSize()). Note that we do this before
+ // adjusting for borderpadding, since ComputedMaxBSize and
+ // ComputedMinBSize are content heights.
+ contentBoxBSize = aReflowInput.ApplyMinMaxBSize(contentBoxBSize);
+
+ borderBoxBSize = contentBoxBSize + borderPadding.BStartEnd(myWM);
+ }
+
+ // Center child in block axis
+ nscoord extraSpace = contentBoxBSize - childMarginBoxBSize;
+ childOffset.B(myWM) += std::max(0, extraSpace / 2);
+
+ // Needed in FinishReflowChild, for logical-to-physical conversion:
+ nsSize borderBoxSize =
+ LogicalSize(myWM, borderBoxISize, borderBoxBSize).GetPhysicalSize(myWM);
+
+ // Place the child
+ FinishReflowChild(inputAreaFrame, aPresContext, childDesiredSize,
+ &childReflowInput, myWM, childOffset, borderBoxSize,
+ ReflowChildFlags::Default);
+ }
+
+ LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize);
+ aDesiredSize.SetSize(myWM, logicalDesiredSize);
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+ if (inputAreaFrame) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inputAreaFrame);
+ }
+
+ FinishAndStoreOverflow(&aDesiredSize);
+
+ NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
+ ("exit nsDateTimeControlFrame::Reflow: size=%d,%d",
+ aDesiredSize.Width(), aDesiredSize.Height()));
+}
diff --git a/layout/forms/nsDateTimeControlFrame.h b/layout/forms/nsDateTimeControlFrame.h
new file mode 100644
index 0000000000..0f2af85a34
--- /dev/null
+++ b/layout/forms/nsDateTimeControlFrame.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This frame type is used for input type=date, time, month, week, and
+ * datetime-local.
+ *
+ * NOTE: some of the above-mentioned input types are still to-be-implemented.
+ * See nsCSSFrameConstructor::FindInputData, as well as bug 1286182 (date),
+ * bug 1306215 (month), bug 1306216 (week) and bug 1306217 (datetime-local).
+ */
+
+#ifndef nsDateTimeControlFrame_h__
+#define nsDateTimeControlFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+struct DateTimeValue;
+} // namespace dom
+} // namespace mozilla
+
+class nsDateTimeControlFrame final : public nsContainerFrame {
+ typedef mozilla::dom::DateTimeValue DateTimeValue;
+
+ explicit nsDateTimeControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ public:
+ friend nsIFrame* NS_NewDateTimeControlFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsDateTimeControlFrame)
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"DateTimeControl"_ns, aResult);
+ }
+#endif
+
+ // Reflow
+ nscoord GetMinISize(gfxContext* aRenderingContext) override;
+
+ nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ Maybe<nscoord> GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext) const override;
+
+ nscoord mFirstBaseline = NS_INTRINSIC_ISIZE_UNKNOWN;
+};
+
+#endif // nsDateTimeControlFrame_h__
diff --git a/layout/forms/nsFieldSetFrame.cpp b/layout/forms/nsFieldSetFrame.cpp
new file mode 100644
index 0000000000..03781da8bd
--- /dev/null
+++ b/layout/forms/nsFieldSetFrame.cpp
@@ -0,0 +1,938 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsFieldSetFrame.h"
+#include "mozilla/dom/HTMLLegendElement.h"
+
+#include <algorithm>
+#include "gfxContext.h"
+#include "mozilla/Baseline.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Likely.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsBlockFrame.h"
+#include "nsCSSAnonBoxes.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsCSSRendering.h"
+#include "nsDisplayList.h"
+#include "nsGkAtoms.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsStyleConsts.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layout;
+using image::ImgDrawResult;
+
+nsContainerFrame* NS_NewFieldSetFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell) nsFieldSetFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsFieldSetFrame)
+NS_QUERYFRAME_HEAD(nsFieldSetFrame)
+ NS_QUERYFRAME_ENTRY(nsFieldSetFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+nsFieldSetFrame::nsFieldSetFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID),
+ mLegendRect(GetWritingMode()) {
+ mLegendSpace = 0;
+}
+
+nsRect nsFieldSetFrame::VisualBorderRectRelativeToSelf() const {
+ WritingMode wm = GetWritingMode();
+ LogicalRect r(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm));
+ nsSize containerSize = r.Size(wm).GetPhysicalSize(wm);
+ nsIFrame* legend = GetLegend();
+ if (legend && !GetPrevInFlow()) {
+ nscoord legendSize = legend->GetLogicalSize(wm).BSize(wm);
+ auto legendMargin = legend->GetLogicalUsedMargin(wm);
+ nscoord legendStartMargin = legendMargin.BStart(wm);
+ nscoord legendEndMargin = legendMargin.BEnd(wm);
+ nscoord border = GetUsedBorder().Side(wm.PhysicalSide(eLogicalSideBStart));
+ // Calculate the offset from the border area block-axis start edge needed to
+ // center-align our border with the legend's border-box (in the block-axis).
+ nscoord off = (legendStartMargin + legendSize / 2) - border / 2;
+ // We don't want to display our border above our border area.
+ if (off > nscoord(0)) {
+ nscoord marginBoxSize = legendStartMargin + legendSize + legendEndMargin;
+ if (marginBoxSize > border) {
+ // We don't want to display our border below the legend's margin-box,
+ // so we align it to the block-axis end if that happens.
+ nscoord overflow = off + border - marginBoxSize;
+ if (overflow > nscoord(0)) {
+ off -= overflow;
+ }
+ r.BStart(wm) += off;
+ r.BSize(wm) -= off;
+ }
+ }
+ }
+ return r.GetPhysicalRect(wm, containerSize);
+}
+
+nsContainerFrame* nsFieldSetFrame::GetInner() const {
+ for (nsIFrame* child : mFrames) {
+ if (child->Style()->GetPseudoType() == PseudoStyleType::fieldsetContent) {
+ return static_cast<nsContainerFrame*>(child);
+ }
+ }
+ return nullptr;
+}
+
+nsIFrame* nsFieldSetFrame::GetLegend() const {
+ for (nsIFrame* child : mFrames) {
+ if (child->Style()->GetPseudoType() != PseudoStyleType::fieldsetContent) {
+ return child;
+ }
+ }
+ return nullptr;
+}
+
+namespace mozilla {
+
+class nsDisplayFieldSetBorder final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayFieldSetBorder(nsDisplayListBuilder* aBuilder,
+ nsFieldSetFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayFieldSetBorder);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayFieldSetBorder)
+
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+ bool CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override;
+ NS_DISPLAY_DECL_NAME("FieldSetBorder", TYPE_FIELDSET_BORDER_BACKGROUND)
+};
+
+void nsDisplayFieldSetBorder::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ Unused << static_cast<nsFieldSetFrame*>(mFrame)->PaintBorder(
+ aBuilder, *aCtx, ToReferenceFrame(), GetPaintRect(aBuilder, aCtx));
+}
+
+nsRect nsDisplayFieldSetBorder::GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const {
+ // Just go ahead and claim our frame's overflow rect as the bounds, because we
+ // may have border-image-outset or other features that cause borders to extend
+ // outside the border rect. We could try to duplicate all the complexity
+ // nsDisplayBorder has here, but keeping things in sync would be a pain, and
+ // this code is not typically performance-sensitive.
+ *aSnap = false;
+ return Frame()->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
+}
+
+bool nsDisplayFieldSetBorder::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ auto frame = static_cast<nsFieldSetFrame*>(mFrame);
+ auto offset = ToReferenceFrame();
+ Maybe<wr::SpaceAndClipChainHelper> clipOut;
+
+ nsRect rect = frame->VisualBorderRectRelativeToSelf() + offset;
+ nsDisplayBoxShadowInner::CreateInsetBoxShadowWebRenderCommands(
+ aBuilder, aSc, rect, mFrame, rect);
+
+ if (nsIFrame* legend = frame->GetLegend()) {
+ nsRect legendRect = legend->GetNormalRect() + offset;
+
+ // Make sure we clip all of the border in case the legend is smaller.
+ nscoord borderTopWidth = frame->GetUsedBorder().top;
+ if (legendRect.height < borderTopWidth) {
+ legendRect.height = borderTopWidth;
+ legendRect.y = offset.y;
+ }
+
+ if (!legendRect.IsEmpty()) {
+ // We need to clip out the part of the border where the legend would go
+ auto appUnitsPerDevPixel = frame->PresContext()->AppUnitsPerDevPixel();
+ auto layoutRect = wr::ToLayoutRect(LayoutDeviceRect::FromAppUnits(
+ frame->InkOverflowRectRelativeToSelf() + offset,
+ appUnitsPerDevPixel));
+
+ wr::ComplexClipRegion region;
+ region.rect = wr::ToLayoutRect(
+ LayoutDeviceRect::FromAppUnits(legendRect, appUnitsPerDevPixel));
+ region.mode = wr::ClipMode::ClipOut;
+ region.radii = wr::EmptyBorderRadius();
+
+ auto rect_clip = aBuilder.DefineRectClip(Nothing(), layoutRect);
+ auto complex_clip = aBuilder.DefineRoundedRectClip(Nothing(), region);
+ auto clipChain =
+ aBuilder.DefineClipChain({rect_clip, complex_clip}, true);
+ clipOut.emplace(aBuilder, clipChain);
+ }
+ } else {
+ rect = nsRect(offset, frame->GetRect().Size());
+ }
+
+ ImgDrawResult drawResult = nsCSSRendering::CreateWebRenderCommandsForBorder(
+ this, mFrame, rect, aBuilder, aResources, aSc, aManager,
+ aDisplayListBuilder);
+ if (drawResult == ImgDrawResult::NOT_SUPPORTED) {
+ return false;
+ }
+ return true;
+};
+
+} // namespace mozilla
+
+void nsFieldSetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ // Paint our background and border in a special way.
+ // REVIEW: We don't really need to check frame emptiness here; if it's empty,
+ // the background/border display item won't do anything, and if it isn't
+ // empty, we need to paint the outline
+ if (!HasAnyStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER) &&
+ IsVisibleForPainting()) {
+ DisplayOutsetBoxShadowUnconditional(aBuilder, aLists.BorderBackground());
+
+ const nsRect rect =
+ VisualBorderRectRelativeToSelf() + aBuilder->ToReferenceFrame(this);
+
+ nsDisplayBackgroundImage::AppendBackgroundItemsToTop(
+ aBuilder, this, rect, aLists.BorderBackground(),
+ /* aAllowWillPaintBorderOptimization = */ false);
+
+ aLists.BorderBackground()->AppendNewToTop<nsDisplayFieldSetBorder>(aBuilder,
+ this);
+
+ DisplayOutlineUnconditional(aBuilder, aLists);
+
+ DO_GLOBAL_REFLOW_COUNT_DSP("nsFieldSetFrame");
+ }
+
+ if (GetPrevInFlow()) {
+ DisplayOverflowContainers(aBuilder, aLists);
+ }
+
+ nsDisplayListCollection contentDisplayItems(aBuilder);
+ if (nsIFrame* inner = GetInner()) {
+ // Collect the inner frame's display items into their own collection.
+ // We need to be calling BuildDisplayList on it before the legend in
+ // case it contains out-of-flow frames whose placeholders are in the
+ // legend. However, we want the inner frame's display items to be
+ // after the legend's display items in z-order, so we need to save them
+ // and append them later.
+ BuildDisplayListForChild(aBuilder, inner, contentDisplayItems);
+ }
+ if (nsIFrame* legend = GetLegend()) {
+ // The legend's background goes on our BlockBorderBackgrounds list because
+ // it's a block child.
+ nsDisplayListSet set(aLists, aLists.BlockBorderBackgrounds());
+ BuildDisplayListForChild(aBuilder, legend, set);
+ }
+ // Put the inner frame's display items on the master list. Note that this
+ // moves its border/background display items to our BorderBackground() list,
+ // which isn't really correct, but it's OK because the inner frame is
+ // anonymous and can't have its own border and background.
+ contentDisplayItems.MoveTo(aLists);
+}
+
+ImgDrawResult nsFieldSetFrame::PaintBorder(nsDisplayListBuilder* aBuilder,
+ gfxContext& aRenderingContext,
+ nsPoint aPt,
+ const nsRect& aDirtyRect) {
+ // If the border is smaller than the legend, move the border down
+ // to be centered on the legend. We call VisualBorderRectRelativeToSelf() to
+ // compute the border positioning.
+ // FIXME: This means border-radius clamping is incorrect; we should
+ // override nsIFrame::GetBorderRadii.
+ nsRect rect = VisualBorderRectRelativeToSelf() + aPt;
+ nsPresContext* presContext = PresContext();
+
+ const auto skipSides = GetSkipSides();
+ PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages()
+ ? PaintBorderFlags::SyncDecodeImages
+ : PaintBorderFlags();
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ nsCSSRendering::PaintBoxShadowInner(presContext, aRenderingContext, this,
+ rect);
+
+ if (nsIFrame* legend = GetLegend()) {
+ // We want to avoid drawing our border under the legend, so clip out the
+ // legend while drawing our border. We don't want to use mLegendRect here,
+ // because we do want to draw our border under the legend's inline-start and
+ // -end margins. And we use GetNormalRect(), not GetRect(), because we do
+ // not want relative positioning applied to the legend to change how our
+ // border looks.
+ nsRect legendRect = legend->GetNormalRect() + aPt;
+
+ // Make sure we clip all of the border in case the legend is smaller.
+ nscoord borderTopWidth = GetUsedBorder().top;
+ if (legendRect.height < borderTopWidth) {
+ legendRect.height = borderTopWidth;
+ legendRect.y = aPt.y;
+ }
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+ // We set up a clip path which has our rect clockwise and the legend rect
+ // counterclockwise, with FILL_WINDING as the fill rule. That will allow us
+ // to paint within our rect but outside the legend rect. For "our rect" we
+ // use our ink overflow rect (relative to ourselves, so it's not affected
+ // by transforms), because we can have borders sticking outside our border
+ // box (e.g. due to border-image-outset).
+ RefPtr<PathBuilder> pathBuilder =
+ drawTarget->CreatePathBuilder(FillRule::FILL_WINDING);
+ int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
+ AppendRectToPath(pathBuilder,
+ NSRectToSnappedRect(InkOverflowRectRelativeToSelf() + aPt,
+ appUnitsPerDevPixel, *drawTarget),
+ true);
+ AppendRectToPath(
+ pathBuilder,
+ NSRectToSnappedRect(legendRect, appUnitsPerDevPixel, *drawTarget),
+ false);
+ RefPtr<Path> clipPath = pathBuilder->Finish();
+
+ aRenderingContext.Save();
+ aRenderingContext.Clip(clipPath);
+ result &= nsCSSRendering::PaintBorder(presContext, aRenderingContext, this,
+ aDirtyRect, rect, mComputedStyle,
+ borderFlags, skipSides);
+ aRenderingContext.Restore();
+ } else {
+ result &= nsCSSRendering::PaintBorder(
+ presContext, aRenderingContext, this, aDirtyRect,
+ nsRect(aPt, mRect.Size()), mComputedStyle, borderFlags, skipSides);
+ }
+
+ return result;
+}
+
+nscoord nsFieldSetFrame::GetIntrinsicISize(gfxContext* aRenderingContext,
+ IntrinsicISizeType aType) {
+ // Both inner and legend are children, and if the fieldset is
+ // size-contained they should not contribute to the intrinsic size.
+ if (Maybe<nscoord> containISize = ContainIntrinsicISize()) {
+ return *containISize;
+ }
+
+ nscoord legendWidth = 0;
+ if (nsIFrame* legend = GetLegend()) {
+ legendWidth =
+ nsLayoutUtils::IntrinsicForContainer(aRenderingContext, legend, aType);
+ }
+
+ nscoord contentWidth = 0;
+ if (nsIFrame* inner = GetInner()) {
+ // Ignore padding on the inner, since the padding will be applied to the
+ // outer instead, and the padding computed for the inner is wrong
+ // for percentage padding.
+ contentWidth = nsLayoutUtils::IntrinsicForContainer(
+ aRenderingContext, inner, aType, nsLayoutUtils::IGNORE_PADDING);
+ }
+
+ return std::max(legendWidth, contentWidth);
+}
+
+nscoord nsFieldSetFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result = 0;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+
+ result = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize);
+ return result;
+}
+
+nscoord nsFieldSetFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result = 0;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+
+ result = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize);
+ return result;
+}
+
+/* virtual */
+void nsFieldSetFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ using LegendAlignValue = mozilla::dom::HTMLLegendElement::LegendAlignValue;
+
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsFieldSetFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE,
+ "Should have a precomputed inline-size!");
+
+ OverflowAreas ocBounds;
+ nsReflowStatus ocStatus;
+ auto* prevInFlow = static_cast<nsFieldSetFrame*>(GetPrevInFlow());
+ if (prevInFlow) {
+ ReflowOverflowContainerChildren(aPresContext, aReflowInput, ocBounds,
+ ReflowChildFlags::Default, ocStatus);
+
+ AutoFrameListPtr prevOverflowFrames(PresContext(),
+ prevInFlow->StealOverflowFrames());
+ if (prevOverflowFrames) {
+ nsContainerFrame::ReparentFrameViewList(*prevOverflowFrames, prevInFlow,
+ this);
+ mFrames.InsertFrames(this, nullptr, std::move(*prevOverflowFrames));
+ }
+ }
+
+ bool reflowInner;
+ bool reflowLegend;
+ nsIFrame* legend = GetLegend();
+ nsContainerFrame* inner = GetInner();
+ if (!legend || !inner) {
+ if (DrainSelfOverflowList()) {
+ legend = GetLegend();
+ inner = GetInner();
+ }
+ }
+ if (aReflowInput.ShouldReflowAllKids() || GetNextInFlow() ||
+ aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
+ reflowInner = inner != nullptr;
+ reflowLegend = legend != nullptr;
+ } else {
+ reflowInner = inner && inner->IsSubtreeDirty();
+ reflowLegend = legend && legend->IsSubtreeDirty();
+ }
+
+ // @note |this| frame applies borders but not any padding. Our anonymous
+ // inner frame applies the padding (but not borders).
+ const auto wm = GetWritingMode();
+ auto skipSides = PreReflowBlockLevelLogicalSkipSides();
+ LogicalMargin border =
+ aReflowInput.ComputedLogicalBorder(wm).ApplySkipSides(skipSides);
+ LogicalSize availSize(wm, aReflowInput.ComputedSize().ISize(wm),
+ aReflowInput.AvailableBSize());
+
+ // Figure out how big the legend is if there is one.
+ LogicalMargin legendMargin(wm);
+ Maybe<ReflowInput> legendReflowInput;
+ if (legend) {
+ const auto legendWM = legend->GetWritingMode();
+ LogicalSize legendAvailSize = availSize.ConvertTo(legendWM, wm);
+ ComputeSizeFlags sizeFlags;
+ if (legend->StylePosition()->ISize(wm).IsAuto()) {
+ sizeFlags = ComputeSizeFlag::ShrinkWrap;
+ }
+ ReflowInput::InitFlags initFlags; // intentionally empty
+ StyleSizeOverrides sizeOverrides; // intentionally empty
+ legendReflowInput.emplace(aPresContext, aReflowInput, legend,
+ legendAvailSize, Nothing(), initFlags,
+ sizeOverrides, sizeFlags);
+ }
+ const bool avoidBreakInside = ShouldAvoidBreakInside(aReflowInput);
+ if (reflowLegend) {
+ ReflowOutput legendDesiredSize(aReflowInput);
+
+ // We'll move the legend to its proper place later, so the position
+ // and containerSize passed here are unimportant.
+ const nsSize dummyContainerSize;
+ ReflowChild(legend, aPresContext, legendDesiredSize, *legendReflowInput, wm,
+ LogicalPoint(wm), dummyContainerSize,
+ ReflowChildFlags::NoMoveFrame, aStatus);
+
+ if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE &&
+ !(HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) &&
+ aReflowInput.mStyleDisplay->IsAbsolutelyPositionedStyle()) &&
+ !prevInFlow && !aReflowInput.mFlags.mIsTopOfPage) {
+ // Propagate break-before from the legend to the fieldset.
+ if (legend->StyleDisplay()->BreakBefore() ||
+ aStatus.IsInlineBreakBefore()) {
+ aStatus.SetInlineLineBreakBeforeAndReset();
+ return;
+ }
+ // Honor break-inside:avoid by breaking before instead.
+ if (MOZ_UNLIKELY(avoidBreakInside) && !aStatus.IsFullyComplete()) {
+ aStatus.SetInlineLineBreakBeforeAndReset();
+ return;
+ }
+ }
+
+ // Calculate the legend's margin-box rectangle.
+ legendMargin = legend->GetLogicalUsedMargin(wm);
+ mLegendRect = LogicalRect(
+ wm, 0, 0, legendDesiredSize.ISize(wm) + legendMargin.IStartEnd(wm),
+ legendDesiredSize.BSize(wm) + legendMargin.BStartEnd(wm));
+ // We subtract mLegendSpace from inner's content-box block-size below.
+ nscoord oldSpace = mLegendSpace;
+ mLegendSpace = 0;
+ nscoord borderBStart = border.BStart(wm);
+ if (!prevInFlow) {
+ if (mLegendRect.BSize(wm) > borderBStart) {
+ mLegendSpace = mLegendRect.BSize(wm) - borderBStart;
+ } else {
+ // Calculate the border-box position that would center the legend's
+ // border-box within the fieldset border:
+ nscoord off = (borderBStart - legendDesiredSize.BSize(wm)) / 2;
+ off -= legendMargin.BStart(wm); // convert to a margin-box position
+ if (off > nscoord(0)) {
+ // Align the legend to the end if center-aligning it would overflow.
+ nscoord overflow = off + mLegendRect.BSize(wm) - borderBStart;
+ if (overflow > nscoord(0)) {
+ off -= overflow;
+ }
+ mLegendRect.BStart(wm) += off;
+ }
+ }
+ } else {
+ mLegendSpace = mLegendRect.BSize(wm);
+ }
+
+ // If mLegendSpace changes then we need to reflow |inner| as well.
+ if (mLegendSpace != oldSpace && inner) {
+ reflowInner = true;
+ }
+
+ FinishReflowChild(legend, aPresContext, legendDesiredSize,
+ legendReflowInput.ptr(), wm, LogicalPoint(wm),
+ dummyContainerSize, ReflowChildFlags::NoMoveFrame);
+ EnsureChildContinuation(legend, aStatus);
+ if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE &&
+ !legend->GetWritingMode().IsOrthogonalTo(wm) &&
+ legend->StyleDisplay()->BreakAfter() &&
+ (!legendReflowInput->mFlags.mIsTopOfPage ||
+ mLegendRect.BSize(wm) > 0) &&
+ aStatus.IsComplete()) {
+ // Pretend that we ran out of space to push children of |inner|.
+ // XXX(mats) perhaps pushing the inner frame would be more correct,
+ // but we don't support that yet.
+ availSize.BSize(wm) = nscoord(0);
+ aStatus.Reset();
+ aStatus.SetIncomplete();
+ }
+ } else if (!legend) {
+ mLegendRect.SetEmpty();
+ mLegendSpace = 0;
+ } else {
+ // mLegendSpace and mLegendRect haven't changed, but we need
+ // the used margin when placing the legend.
+ legendMargin = legend->GetLogicalUsedMargin(wm);
+ }
+
+ // This containerSize is incomplete as yet: it does not include the size
+ // of the |inner| frame itself.
+ nsSize containerSize =
+ (LogicalSize(wm, 0, mLegendSpace) + border.Size(wm)).GetPhysicalSize(wm);
+ if (reflowInner) {
+ LogicalSize innerAvailSize = availSize;
+ innerAvailSize.ISize(wm) =
+ aReflowInput.ComputedSizeWithPadding(wm).ISize(wm);
+ nscoord remainingComputedBSize = aReflowInput.ComputedBSize();
+ if (prevInFlow && remainingComputedBSize != NS_UNCONSTRAINEDSIZE) {
+ // Subtract the consumed BSize associated with the legend.
+ for (nsIFrame* prev = prevInFlow; prev; prev = prev->GetPrevInFlow()) {
+ auto* prevFieldSet = static_cast<nsFieldSetFrame*>(prev);
+ remainingComputedBSize -= prevFieldSet->mLegendSpace;
+ }
+ remainingComputedBSize = std::max(0, remainingComputedBSize);
+ }
+ if (innerAvailSize.BSize(wm) != NS_UNCONSTRAINEDSIZE) {
+ innerAvailSize.BSize(wm) -=
+ std::max(mLegendRect.BSize(wm), border.BStart(wm));
+ if (StyleBorder()->mBoxDecorationBreak ==
+ StyleBoxDecorationBreak::Clone &&
+ (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE ||
+ remainingComputedBSize +
+ aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(
+ wm) >=
+ availSize.BSize(wm))) {
+ innerAvailSize.BSize(wm) -= border.BEnd(wm);
+ }
+ innerAvailSize.BSize(wm) = std::max(0, innerAvailSize.BSize(wm));
+ }
+ ReflowInput kidReflowInput(aPresContext, aReflowInput, inner,
+ innerAvailSize, Nothing(),
+ ReflowInput::InitFlag::CallerWillInit);
+ // Override computed padding, in case it's percentage padding
+ kidReflowInput.Init(
+ aPresContext, Nothing(), Nothing(),
+ Some(aReflowInput.ComputedLogicalPadding(inner->GetWritingMode())));
+
+ // Propagate the aspect-ratio flag to |inner| (i.e. the container frame
+ // wrapped by nsFieldSetFrame), so we can let |inner|'s reflow code handle
+ // automatic content-based minimum.
+ // Note: Init() resets this flag, so we have to copy it again here.
+ if (aReflowInput.mFlags.mIsBSizeSetByAspectRatio) {
+ kidReflowInput.mFlags.mIsBSizeSetByAspectRatio = true;
+ }
+
+ if (kidReflowInput.mFlags.mIsTopOfPage) {
+ // Prevent break-before from |inner| if we have a legend.
+ kidReflowInput.mFlags.mIsTopOfPage = !legend;
+ }
+ // Our child is "height:100%" but we actually want its height to be reduced
+ // by the amount of content-height the legend is eating up, unless our
+ // height is unconstrained (in which case the child's will be too).
+ if (aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) {
+ kidReflowInput.SetComputedBSize(
+ std::max(0, remainingComputedBSize - mLegendSpace));
+ }
+
+ if (aReflowInput.ComputedMinBSize() > 0) {
+ kidReflowInput.SetComputedMinBSize(
+ std::max(0, aReflowInput.ComputedMinBSize() - mLegendSpace));
+ }
+
+ if (aReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE) {
+ kidReflowInput.SetComputedMaxBSize(
+ std::max(0, aReflowInput.ComputedMaxBSize() - mLegendSpace));
+ }
+
+ ReflowOutput kidDesiredSize(kidReflowInput);
+ NS_ASSERTION(
+ kidReflowInput.ComputedPhysicalMargin() == nsMargin(0, 0, 0, 0),
+ "Margins on anonymous fieldset child not supported!");
+ LogicalPoint pt(wm, border.IStart(wm), border.BStart(wm) + mLegendSpace);
+
+ // We don't know the correct containerSize until we have reflowed |inner|,
+ // so we use a dummy value for now; FinishReflowChild will fix the position
+ // if necessary.
+ const nsSize dummyContainerSize;
+ nsReflowStatus status;
+ // If our legend needs a continuation then *this* frame will have
+ // a continuation as well so we should keep our inner frame continuations
+ // too (even if 'inner' ends up being COMPLETE here). This ensures that
+ // our continuation will have a reasonable inline-size.
+ ReflowChildFlags flags = aStatus.IsFullyComplete()
+ ? ReflowChildFlags::Default
+ : ReflowChildFlags::NoDeleteNextInFlowChild;
+ ReflowChild(inner, aPresContext, kidDesiredSize, kidReflowInput, wm, pt,
+ dummyContainerSize, flags, status);
+
+ // Honor break-inside:avoid when possible by returning a BreakBefore status.
+ if (MOZ_UNLIKELY(avoidBreakInside) && !prevInFlow &&
+ !aReflowInput.mFlags.mIsTopOfPage &&
+ availSize.BSize(wm) != NS_UNCONSTRAINEDSIZE) {
+ if (status.IsInlineBreakBefore() || !status.IsFullyComplete()) {
+ aStatus.SetInlineLineBreakBeforeAndReset();
+ return;
+ }
+ }
+
+ // Update containerSize to account for size of the inner frame, so that
+ // FinishReflowChild can position it correctly.
+ containerSize += kidDesiredSize.PhysicalSize();
+ FinishReflowChild(inner, aPresContext, kidDesiredSize, &kidReflowInput, wm,
+ pt, containerSize, ReflowChildFlags::Default);
+ EnsureChildContinuation(inner, status);
+ aStatus.MergeCompletionStatusFrom(status);
+ NS_FRAME_TRACE_REFLOW_OUT("FieldSet::Reflow", aStatus);
+ } else if (inner) {
+ // |inner| didn't need to be reflowed but we do need to include its size
+ // in containerSize.
+ containerSize += inner->GetSize();
+ } else {
+ // No |inner| means it was already complete in an earlier continuation.
+ MOZ_ASSERT(prevInFlow, "first-in-flow should always have an inner frame");
+ for (nsIFrame* prev = prevInFlow; prev; prev = prev->GetPrevInFlow()) {
+ auto* prevFieldSet = static_cast<nsFieldSetFrame*>(prev);
+ if (auto* prevInner = prevFieldSet->GetInner()) {
+ containerSize += prevInner->GetSize();
+ break;
+ }
+ }
+ }
+
+ LogicalRect contentRect(wm);
+ if (inner) {
+ // We don't support margins on inner, so our content rect is just the
+ // inner's border-box. (We don't really care about container size at this
+ // point, as we'll figure out the actual positioning later.)
+ contentRect = inner->GetLogicalRect(wm, containerSize);
+ } else if (prevInFlow) {
+ auto size = prevInFlow->GetPaddingRectRelativeToSelf().Size();
+ contentRect.ISize(wm) = wm.IsVertical() ? size.height : size.width;
+ }
+
+ if (legend) {
+ // The legend is positioned inline-wards within the inner's content rect
+ // (so that padding on the fieldset affects the legend position).
+ LogicalRect innerContentRect = contentRect;
+ innerContentRect.Deflate(wm, aReflowInput.ComputedLogicalPadding(wm));
+ // If the inner content rect is larger than the legend, we can align the
+ // legend.
+ if (innerContentRect.ISize(wm) > mLegendRect.ISize(wm)) {
+ // NOTE legend @align values are: left/right/center
+ // GetLogicalAlign converts left/right to start/end for the given WM.
+ // @see HTMLLegendElement::ParseAttribute/LogicalAlign
+ auto* legendElement =
+ dom::HTMLLegendElement::FromNode(legend->GetContent());
+ switch (legendElement->LogicalAlign(wm)) {
+ case LegendAlignValue::InlineEnd:
+ mLegendRect.IStart(wm) =
+ innerContentRect.IEnd(wm) - mLegendRect.ISize(wm);
+ break;
+ case LegendAlignValue::Center:
+ // Note: rounding removed; there doesn't seem to be any need
+ mLegendRect.IStart(wm) =
+ innerContentRect.IStart(wm) +
+ (innerContentRect.ISize(wm) - mLegendRect.ISize(wm)) / 2;
+ break;
+ case LegendAlignValue::InlineStart:
+ mLegendRect.IStart(wm) = innerContentRect.IStart(wm);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected GetLogicalAlign value");
+ }
+ } else {
+ // otherwise just start-align it.
+ mLegendRect.IStart(wm) = innerContentRect.IStart(wm);
+ }
+
+ // place the legend
+ LogicalRect actualLegendRect = mLegendRect;
+ actualLegendRect.Deflate(wm, legendMargin);
+ LogicalPoint actualLegendPos(actualLegendRect.Origin(wm));
+
+ // Note that legend's writing mode may be different from the fieldset's,
+ // so we need to convert offsets before applying them to it (bug 1134534).
+ LogicalMargin offsets = legendReflowInput->ComputedLogicalOffsets(wm);
+ ReflowInput::ApplyRelativePositioning(legend, wm, offsets, &actualLegendPos,
+ containerSize);
+
+ legend->SetPosition(wm, actualLegendPos, containerSize);
+ nsContainerFrame::PositionFrameView(legend);
+ nsContainerFrame::PositionChildViews(legend);
+ }
+
+ // Skip our block-end border if we're INCOMPLETE.
+ if (!aStatus.IsComplete() &&
+ StyleBorder()->mBoxDecorationBreak != StyleBoxDecorationBreak::Clone) {
+ border.BEnd(wm) = nscoord(0);
+ }
+
+ // Return our size and our result.
+ LogicalSize finalSize(
+ wm, contentRect.ISize(wm) + border.IStartEnd(wm),
+ mLegendSpace + border.BStartEnd(wm) + (inner ? inner->BSize(wm) : 0));
+ if (Maybe<nscoord> containBSize =
+ aReflowInput.mFrame->ContainIntrinsicBSize()) {
+ // If we're size-contained in block axis, then we must set finalSize
+ // according to contain-intrinsic-block-size, disregarding legend and inner.
+ // Note: normally the fieldset's own padding (which we still must honor)
+ // would be accounted for as part of inner's size (see kidReflowInput.Init()
+ // call above). So: since we're disregarding sizing information from
+ // 'inner', we need to account for that padding ourselves here.
+ nscoord contentBoxBSize =
+ aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE
+ ? aReflowInput.ApplyMinMaxBSize(*containBSize)
+ : aReflowInput.ComputedBSize();
+ finalSize.BSize(wm) =
+ contentBoxBSize +
+ aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(wm);
+ }
+
+ if (aStatus.IsComplete() &&
+ aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE &&
+ finalSize.BSize(wm) > aReflowInput.AvailableBSize() &&
+ border.BEnd(wm) > 0 && aReflowInput.AvailableBSize() > border.BEnd(wm)) {
+ // Our end border doesn't fit but it should fit in the next column/page.
+ if (MOZ_UNLIKELY(avoidBreakInside)) {
+ aStatus.SetInlineLineBreakBeforeAndReset();
+ return;
+ } else {
+ if (StyleBorder()->mBoxDecorationBreak ==
+ StyleBoxDecorationBreak::Slice) {
+ finalSize.BSize(wm) -= border.BEnd(wm);
+ }
+ aStatus.SetIncomplete();
+ }
+ }
+
+ if (!aStatus.IsComplete()) {
+ MOZ_ASSERT(aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE,
+ "must be Complete in an unconstrained available block-size");
+ // Stretch our BSize to fill the fragmentainer.
+ finalSize.BSize(wm) =
+ std::max(finalSize.BSize(wm), aReflowInput.AvailableBSize());
+ }
+ aDesiredSize.SetSize(wm, finalSize);
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+ if (legend) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, legend);
+ }
+ if (inner) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inner);
+ }
+
+ // Merge overflow container bounds and status.
+ aDesiredSize.mOverflowAreas.UnionWith(ocBounds);
+ aStatus.MergeCompletionStatusFrom(ocStatus);
+
+ FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput,
+ aStatus);
+ InvalidateFrame();
+}
+
+void nsFieldSetFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
+ if (nsBlockFrame* legend = do_QueryFrame(GetLegend())) {
+ // A rendered legend always establish a new formatting context.
+ // https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend
+ legend->AddStateBits(NS_BLOCK_STATIC_BFC);
+ }
+ MOZ_ASSERT(
+ aListID != FrameChildListID::Principal || GetInner() || GetLegend(),
+ "Setting principal child list should populate our inner frame "
+ "or our rendered legend");
+}
+
+void nsFieldSetFrame::AppendFrames(ChildListID aListID,
+ nsFrameList&& aFrameList) {
+ MOZ_ASSERT(aListID == FrameChildListID::NoReflowPrincipal &&
+ HasAnyStateBits(NS_FRAME_FIRST_REFLOW),
+ "AppendFrames should only be used from "
+ "nsCSSFrameConstructor::ConstructFieldSetFrame");
+ nsContainerFrame::AppendFrames(aListID, std::move(aFrameList));
+ MOZ_ASSERT(GetInner(), "at this point we should have an inner frame");
+}
+
+void nsFieldSetFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) {
+ MOZ_ASSERT(
+ aListID == FrameChildListID::Principal && !aPrevFrame && !GetLegend(),
+ "InsertFrames should only be used to prepend a rendered legend "
+ "from nsCSSFrameConstructor::ConstructFramesFromItemList");
+ nsContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine,
+ std::move(aFrameList));
+ MOZ_ASSERT(GetLegend());
+ if (nsBlockFrame* legend = do_QueryFrame(GetLegend())) {
+ // A rendered legend always establish a new formatting context.
+ // https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend
+ legend->AddStateBits(NS_BLOCK_STATIC_BFC);
+ }
+}
+
+#ifdef DEBUG
+void nsFieldSetFrame::RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) {
+ MOZ_CRASH("nsFieldSetFrame::RemoveFrame not supported");
+}
+#endif
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsFieldSetFrame::AccessibleType() {
+ return a11y::eHTMLGroupboxType;
+}
+#endif
+
+BaselineSharingGroup nsFieldSetFrame::GetDefaultBaselineSharingGroup() const {
+ switch (StyleDisplay()->DisplayInside()) {
+ case mozilla::StyleDisplayInside::Grid:
+ case mozilla::StyleDisplayInside::Flex:
+ return BaselineSharingGroup::First;
+ default:
+ return BaselineSharingGroup::Last;
+ }
+}
+
+nscoord nsFieldSetFrame::SynthesizeFallbackBaseline(
+ WritingMode aWM, BaselineSharingGroup aBaselineGroup) const {
+ return Baseline::SynthesizeBOffsetFromMarginBox(this, aWM, aBaselineGroup);
+}
+
+Maybe<nscoord> nsFieldSetFrame::GetNaturalBaselineBOffset(
+ WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext aExportContext) const {
+ if (StyleDisplay()->IsContainLayout()) {
+ // If we are layout-contained, our child 'inner' should not
+ // affect how we calculate our baseline.
+ return Nothing{};
+ }
+ nsIFrame* inner = GetInner();
+ if (MOZ_UNLIKELY(!inner)) {
+ return Nothing{};
+ }
+ MOZ_ASSERT(!inner->GetWritingMode().IsOrthogonalTo(aWM));
+ const auto result =
+ inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext);
+ if (!result) {
+ return Nothing{};
+ }
+ nscoord innerBStart = inner->BStart(aWM, GetSize());
+ if (aBaselineGroup == BaselineSharingGroup::First) {
+ return Some(*result + innerBStart);
+ }
+ return Some(*result + BSize(aWM) - (innerBStart + inner->BSize(aWM)));
+}
+
+nsIScrollableFrame* nsFieldSetFrame::GetScrollTargetFrame() const {
+ return do_QueryFrame(GetInner());
+}
+
+void nsFieldSetFrame::AppendDirectlyOwnedAnonBoxes(
+ nsTArray<OwnedAnonBox>& aResult) {
+ if (nsIFrame* kid = GetInner()) {
+ aResult.AppendElement(OwnedAnonBox(kid));
+ }
+}
+
+void nsFieldSetFrame::EnsureChildContinuation(nsIFrame* aChild,
+ const nsReflowStatus& aStatus) {
+ MOZ_ASSERT(aChild == GetLegend() || aChild == GetInner(),
+ "unexpected child frame");
+ nsIFrame* nif = aChild->GetNextInFlow();
+ if (aStatus.IsFullyComplete()) {
+ if (nif) {
+ // NOTE: we want to avoid our DEBUG version of RemoveFrame above.
+ DestroyContext context(PresShell());
+ nsContainerFrame::RemoveFrame(context,
+ FrameChildListID::NoReflowPrincipal, nif);
+ MOZ_ASSERT(!aChild->GetNextInFlow());
+ }
+ } else {
+ nsFrameList nifs;
+ if (!nif) {
+ auto* fc = PresShell()->FrameConstructor();
+ nif = fc->CreateContinuingFrame(aChild, this);
+ if (aStatus.IsOverflowIncomplete()) {
+ nif->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER);
+ }
+ nifs = nsFrameList(nif, nif);
+ } else {
+ // Steal all nifs and push them again in case they are currently on
+ // the wrong list.
+ for (nsIFrame* n = nif; n; n = n->GetNextInFlow()) {
+ n->GetParent()->StealFrame(n);
+ nifs.AppendFrame(this, n);
+ if (aStatus.IsOverflowIncomplete()) {
+ n->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER);
+ } else {
+ n->RemoveStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER);
+ }
+ }
+ }
+ if (aStatus.IsOverflowIncomplete()) {
+ if (nsFrameList* eoc = GetExcessOverflowContainers()) {
+ eoc->AppendFrames(nullptr, std::move(nifs));
+ } else {
+ SetExcessOverflowContainers(std::move(nifs));
+ }
+ } else {
+ if (nsFrameList* oc = GetOverflowFrames()) {
+ oc->AppendFrames(nullptr, std::move(nifs));
+ } else {
+ SetOverflowFrames(std::move(nifs));
+ }
+ }
+ }
+}
diff --git a/layout/forms/nsFieldSetFrame.h b/layout/forms/nsFieldSetFrame.h
new file mode 100644
index 0000000000..741b3bef52
--- /dev/null
+++ b/layout/forms/nsFieldSetFrame.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsFieldSetFrame_h___
+#define nsFieldSetFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "ImgDrawResult.h"
+#include "nsContainerFrame.h"
+#include "nsIScrollableFrame.h"
+
+class nsFieldSetFrame final : public nsContainerFrame {
+ typedef mozilla::image::ImgDrawResult ImgDrawResult;
+
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsFieldSetFrame)
+ NS_DECL_QUERYFRAME
+
+ explicit nsFieldSetFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+
+ nscoord GetIntrinsicISize(gfxContext* aRenderingContext,
+ mozilla::IntrinsicISizeType);
+ nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ /**
+ * The area to paint box-shadows around. It's the border rect except
+ * when there's a <legend> we offset the y-position to the center of it.
+ */
+ nsRect VisualBorderRectRelativeToSelf() const override;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ nscoord SynthesizeFallbackBaseline(
+ mozilla::WritingMode aWM,
+ BaselineSharingGroup aBaselineGroup) const override;
+ BaselineSharingGroup GetDefaultBaselineSharingGroup() const override;
+ Maybe<nscoord> GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext aExportContext) const override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ ImgDrawResult PaintBorder(nsDisplayListBuilder* aBuilder,
+ gfxContext& aRenderingContext, nsPoint aPt,
+ const nsRect& aDirtyRect);
+
+ void SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) override;
+ void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override;
+ void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) override;
+#ifdef DEBUG
+ void RemoveFrame(DestroyContext&, ChildListID aListID,
+ nsIFrame* aOldFrame) override;
+#endif
+
+ nsIScrollableFrame* GetScrollTargetFrame() const override;
+
+ // Return the block wrapper around our kids.
+ void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override;
+
+#ifdef ACCESSIBILITY
+ virtual mozilla::a11y::AccType AccessibleType() override;
+#endif
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"FieldSet"_ns, aResult);
+ }
+#endif
+
+ /**
+ * Return the anonymous frame that contains all descendants except the legend
+ * frame. This can be a block/grid/flex/scroll frame. It always has
+ * the pseudo type nsCSSAnonBoxes::fieldsetContent. If it's a scroll frame,
+ * the scrolled frame can be a block/grid/flex frame.
+ * This may return null, for example for a fieldset that is a true overflow
+ * container.
+ */
+ nsContainerFrame* GetInner() const;
+
+ /**
+ * Return the frame that represents the rendered legend if any.
+ * https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend
+ */
+ nsIFrame* GetLegend() const;
+
+ /** @see mLegendSpace below */
+ nscoord LegendSpace() const { return mLegendSpace; }
+
+ protected:
+ /**
+ * Convenience method to create a continuation for aChild after we've
+ * reflowed it and got the reflow status aStatus.
+ */
+ void EnsureChildContinuation(nsIFrame* aChild, const nsReflowStatus& aStatus);
+
+ mozilla::LogicalRect mLegendRect;
+
+ // This is how much to subtract from our inner frame's content-box block-size
+ // to account for a protruding legend. It's zero if there's no legend or
+ // the legend fits entirely inside our start border.
+ nscoord mLegendSpace;
+};
+
+#endif // nsFieldSetFrame_h___
diff --git a/layout/forms/nsFileControlFrame.cpp b/layout/forms/nsFileControlFrame.cpp
new file mode 100644
index 0000000000..d24158d9dc
--- /dev/null
+++ b/layout/forms/nsFileControlFrame.cpp
@@ -0,0 +1,422 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsFileControlFrame.h"
+
+#include "nsGkAtoms.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/NodeInfo.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/DragEvent.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/FileList.h"
+#include "mozilla/dom/HTMLButtonElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/TextEditor.h"
+#include "MiddleCroppingBlockFrame.h"
+#include "nsIFrame.h"
+#include "nsNodeInfoManager.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsContentUtils.h"
+#include "nsIFile.h"
+#include "nsLayoutUtils.h"
+#include "nsTextNode.h"
+#include "nsTextFrame.h"
+#include "gfxContext.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame* NS_NewFileControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsFileControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame)
+
+nsFileControlFrame::nsFileControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBlockFrame(aStyle, aPresContext, kClassID) {}
+
+void nsFileControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
+
+ mMouseListener = new DnDListener(this);
+}
+
+void nsFileControlFrame::Destroy(DestroyContext& aContext) {
+ NS_ENSURE_TRUE_VOID(mContent);
+
+ // Remove the events.
+ if (mContent) {
+ mContent->RemoveSystemEventListener(u"drop"_ns, mMouseListener, false);
+ mContent->RemoveSystemEventListener(u"dragover"_ns, mMouseListener, false);
+ }
+
+ aContext.AddAnonymousContent(mTextContent.forget());
+ aContext.AddAnonymousContent(mBrowseFilesOrDirs.forget());
+
+ mMouseListener->ForgetFrame();
+ nsBlockFrame::Destroy(aContext);
+}
+
+static already_AddRefed<Element> MakeAnonButton(
+ Document* aDoc, const char* labelKey, HTMLInputElement* aInputElement) {
+ RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button);
+ // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any
+ // attribute.
+ button->SetIsNativeAnonymousRoot();
+ button->SetPseudoElementType(PseudoStyleType::fileSelectorButton);
+
+ // Set the file picking button text depending on the current locale.
+ nsAutoString buttonTxt;
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ labelKey, aDoc, buttonTxt);
+
+ auto* nim = aDoc->NodeInfoManager();
+ // Set the browse button text. It's a bit of a pain to do because we want to
+ // make sure we are not notifying.
+ RefPtr textContent = new (nim) nsTextNode(nim);
+ textContent->SetText(buttonTxt, false);
+
+ IgnoredErrorResult error;
+ button->AppendChildTo(textContent, false, error);
+ if (error.Failed()) {
+ return nullptr;
+ }
+
+ auto* buttonElement = HTMLButtonElement::FromNode(button);
+ // We allow tabbing over the input itself, not the button.
+ buttonElement->SetTabIndex(-1, IgnoreErrors());
+ return button.forget();
+}
+
+nsresult nsFileControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ nsCOMPtr<Document> doc = mContent->GetComposedDoc();
+ RefPtr fileContent = HTMLInputElement::FromNode(mContent);
+
+ mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent);
+ if (!mBrowseFilesOrDirs) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aElements.AppendElement(mBrowseFilesOrDirs);
+
+ // Create and setup the text showing the selected files.
+ mTextContent = doc->CreateHTMLElement(nsGkAtoms::label);
+ // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any
+ // attribute.
+ mTextContent->SetIsNativeAnonymousRoot();
+ RefPtr<nsTextNode> text =
+ new (doc->NodeInfoManager()) nsTextNode(doc->NodeInfoManager());
+ mTextContent->AppendChildTo(text, false, IgnoreErrors());
+
+ aElements.AppendElement(mTextContent);
+
+ // We should be able to interact with the element by doing drag and drop.
+ mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false);
+ mContent->AddSystemEventListener(u"dragover"_ns, mMouseListener, false);
+
+ SyncDisabledState();
+
+ return NS_OK;
+}
+
+void nsFileControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mBrowseFilesOrDirs) {
+ aElements.AppendElement(mBrowseFilesOrDirs);
+ }
+
+ if (mTextContent) {
+ aElements.AppendElement(mTextContent);
+ }
+}
+
+NS_QUERYFRAME_HEAD(nsFileControlFrame)
+ NS_QUERYFRAME_ENTRY(nsFileControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
+
+void nsFileControlFrame::SetFocus(bool aOn, bool aRepaint) {}
+
+static void AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray,
+ BlobImpl* aBlobImpl,
+ nsIContent* aContent) {
+ MOZ_ASSERT(aBlobImpl);
+ MOZ_ASSERT(aBlobImpl->IsDirectory());
+
+ nsAutoString fullpath;
+ ErrorResult err;
+ aBlobImpl->GetMozFullPath(fullpath, SystemCallerGuarantee(), err);
+ if (err.Failed()) {
+ err.SuppressException();
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(fullpath, true, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow();
+ if (!inner || !inner->IsCurrentInnerWindow()) {
+ return;
+ }
+
+ RefPtr<Directory> directory = Directory::Create(inner->AsGlobal(), file);
+ MOZ_ASSERT(directory);
+
+ OwningFileOrDirectory* element = aArray.AppendElement();
+ element->SetAsDirectory() = directory;
+}
+
+/**
+ * This is called when we receive a drop or a dragover.
+ */
+NS_IMETHODIMP
+nsFileControlFrame::DnDListener::HandleEvent(Event* aEvent) {
+ NS_ASSERTION(mFrame, "We should have been unregistered");
+
+ if (aEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+
+ DragEvent* dragEvent = aEvent->AsDragEvent();
+ if (!dragEvent) {
+ return NS_OK;
+ }
+
+ RefPtr<DataTransfer> dataTransfer = dragEvent->GetDataTransfer();
+ if (!IsValidDropData(dataTransfer)) {
+ return NS_OK;
+ }
+
+ RefPtr<HTMLInputElement> inputElement =
+ HTMLInputElement::FromNode(mFrame->GetContent());
+ bool supportsMultiple = inputElement->HasAttr(nsGkAtoms::multiple);
+ if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) {
+ dataTransfer->SetDropEffect(u"none"_ns);
+ aEvent->StopPropagation();
+ return NS_OK;
+ }
+
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("dragover")) {
+ // Prevent default if we can accept this drag data
+ aEvent->PreventDefault();
+ return NS_OK;
+ }
+
+ if (eventType.EqualsLiteral("drop")) {
+ aEvent->StopPropagation();
+ aEvent->PreventDefault();
+
+ RefPtr<FileList> fileList =
+ dataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal());
+
+ RefPtr<BlobImpl> webkitDir;
+ nsresult rv =
+ GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir));
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsTArray<OwningFileOrDirectory> array;
+ if (webkitDir) {
+ AppendBlobImplAsDirectory(array, webkitDir, inputElement);
+ inputElement->MozSetDndFilesAndDirectories(array);
+ } else {
+ bool blinkFileSystemEnabled =
+ StaticPrefs::dom_webkitBlink_filesystem_enabled();
+ if (blinkFileSystemEnabled) {
+ FileList* files = static_cast<FileList*>(fileList.get());
+ if (files) {
+ for (uint32_t i = 0; i < files->Length(); ++i) {
+ File* file = files->Item(i);
+ if (file) {
+ if (file->Impl() && file->Impl()->IsDirectory()) {
+ AppendBlobImplAsDirectory(array, file->Impl(), inputElement);
+ } else {
+ OwningFileOrDirectory* element = array.AppendElement();
+ element->SetAsFile() = file;
+ }
+ }
+ }
+ }
+ }
+
+ // Entries API.
+ if (blinkFileSystemEnabled) {
+ // This is rather ugly. Pass the directories as Files using SetFiles,
+ // but then if blink filesystem API is enabled, it wants
+ // FileOrDirectory array.
+ inputElement->SetFiles(fileList, true);
+ inputElement->UpdateEntries(array);
+ }
+ // Normal DnD
+ else {
+ inputElement->SetFiles(fileList, true);
+ }
+
+ RefPtr<TextEditor> textEditor;
+ DebugOnly<nsresult> rvIgnored =
+ nsContentUtils::DispatchInputEvent(inputElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ nsContentUtils::DispatchTrustedEvent(inputElement->OwnerDoc(),
+ inputElement, u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory(
+ FileList* aFileList, BlobImpl** aBlobImpl) {
+ *aBlobImpl = nullptr;
+
+ HTMLInputElement* inputElement =
+ HTMLInputElement::FromNode(mFrame->GetContent());
+ bool webkitDirPicker = StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ inputElement->HasAttr(nsGkAtoms::webkitdirectory);
+ if (!webkitDirPicker) {
+ return NS_OK;
+ }
+
+ if (!aFileList) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // webkitdirectory doesn't care about the length of the file list but
+ // only about the first item on it.
+ uint32_t len = aFileList->Length();
+ if (len) {
+ File* file = aFileList->Item(0);
+ if (file) {
+ BlobImpl* impl = file->Impl();
+ if (impl && impl->IsDirectory()) {
+ RefPtr<BlobImpl> retVal = impl;
+ retVal.swap(*aBlobImpl);
+ return NS_OK;
+ }
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+bool nsFileControlFrame::DnDListener::IsValidDropData(
+ DataTransfer* aDataTransfer) {
+ if (!aDataTransfer) {
+ return false;
+ }
+
+ // We only support dropping files onto a file upload control
+ return aDataTransfer->HasFile();
+}
+
+bool nsFileControlFrame::DnDListener::CanDropTheseFiles(
+ DataTransfer* aDataTransfer, bool aSupportsMultiple) {
+ RefPtr<FileList> fileList =
+ aDataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal());
+
+ RefPtr<BlobImpl> webkitDir;
+ nsresult rv =
+ GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir));
+ // Just check if either there isn't webkitdirectory attribute, or
+ // fileList has a directory which can be dropped to the element.
+ // No need to use webkitDir for anything here.
+ NS_ENSURE_SUCCESS(rv, false);
+
+ uint32_t listLength = 0;
+ if (fileList) {
+ listLength = fileList->Length();
+ }
+ return listLength <= 1 || aSupportsMultiple;
+}
+
+void nsFileControlFrame::SyncDisabledState() {
+ if (mContent->AsElement()->State().HasState(ElementState::DISABLED)) {
+ mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns,
+ true);
+ } else {
+ mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
+ }
+}
+
+void nsFileControlFrame::ElementStateChanged(ElementState aStates) {
+ if (aStates.HasState(ElementState::DISABLED)) {
+ nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this));
+ }
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsFileControlFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"FileControl"_ns, aResult);
+}
+#endif
+
+nsresult nsFileControlFrame::SetFormProperty(nsAtom* aName,
+ const nsAString& aValue) {
+ if (nsGkAtoms::value == aName) {
+ if (MiddleCroppingBlockFrame* f =
+ do_QueryFrame(mTextContent->GetPrimaryFrame())) {
+ f->UpdateDisplayedValueToUncroppedValue(true);
+ }
+ }
+ return NS_OK;
+}
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsFileControlFrame::AccessibleType() {
+ return a11y::eHTMLFileInputType;
+}
+#endif
+
+NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, nsIDOMEventListener)
+
+class FileControlLabelFrame final : public MiddleCroppingBlockFrame {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(FileControlLabelFrame)
+
+ FileControlLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : MiddleCroppingBlockFrame(aStyle, aPresContext, kClassID) {}
+
+ HTMLInputElement& FileInput() const {
+ return *HTMLInputElement::FromNode(mContent->GetParent());
+ }
+
+ void GetUncroppedValue(nsAString& aValue) override {
+ return FileInput().GetDisplayFileName(aValue);
+ }
+};
+
+NS_QUERYFRAME_HEAD(FileControlLabelFrame)
+ NS_QUERYFRAME_ENTRY(FileControlLabelFrame)
+NS_QUERYFRAME_TAIL_INHERITING(MiddleCroppingBlockFrame)
+NS_IMPL_FRAMEARENA_HELPERS(FileControlLabelFrame)
+
+nsIFrame* NS_NewFileControlLabelFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ FileControlLabelFrame(aStyle, aPresShell->GetPresContext());
+}
diff --git a/layout/forms/nsFileControlFrame.h b/layout/forms/nsFileControlFrame.h
new file mode 100644
index 0000000000..c58dccc763
--- /dev/null
+++ b/layout/forms/nsFileControlFrame.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsFileControlFrame_h___
+#define nsFileControlFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsBlockFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsIDOMEventListener.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla::dom {
+class FileList;
+class BlobImpl;
+class DataTransfer;
+} // namespace mozilla::dom
+
+class nsFileControlFrame final : public nsBlockFrame,
+ public nsIFormControlFrame,
+ public nsIAnonymousContentCreator {
+ using Element = mozilla::dom::Element;
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsFileControlFrame)
+
+ explicit nsFileControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ // nsIFormControlFrame
+ nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override;
+ void SetFocus(bool aOn, bool aRepaint) override;
+
+ void Destroy(DestroyContext&) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ void ElementStateChanged(mozilla::dom::ElementState aStates) override;
+
+ // nsIAnonymousContentCreator
+ nsresult CreateAnonymousContent(nsTArray<ContentInfo>&) override;
+ void AppendAnonymousContentTo(nsTArray<nsIContent*>&,
+ uint32_t aFilter) override;
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() override;
+#endif
+
+ protected:
+ class MouseListener;
+ friend class MouseListener;
+ class MouseListener : public nsIDOMEventListener {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit MouseListener(nsFileControlFrame* aFrame) : mFrame(aFrame) {}
+
+ void ForgetFrame() { mFrame = nullptr; }
+
+ protected:
+ virtual ~MouseListener() = default;
+
+ nsFileControlFrame* mFrame;
+ };
+
+ class SyncDisabledStateEvent;
+ friend class SyncDisabledStateEvent;
+ class SyncDisabledStateEvent : public mozilla::Runnable {
+ public:
+ explicit SyncDisabledStateEvent(nsFileControlFrame* aFrame)
+ : mozilla::Runnable("nsFileControlFrame::SyncDisabledStateEvent"),
+ mFrame(aFrame) {}
+
+ NS_IMETHOD Run() override {
+ nsFileControlFrame* frame =
+ static_cast<nsFileControlFrame*>(mFrame.GetFrame());
+ NS_ENSURE_STATE(frame);
+
+ frame->SyncDisabledState();
+ return NS_OK;
+ }
+
+ private:
+ WeakFrame mFrame;
+ };
+
+ class DnDListener : public MouseListener {
+ public:
+ explicit DnDListener(nsFileControlFrame* aFrame) : MouseListener(aFrame) {}
+
+ // nsIDOMEventListener
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD HandleEvent(mozilla::dom::Event* aEvent) override;
+
+ nsresult GetBlobImplForWebkitDirectory(mozilla::dom::FileList* aFileList,
+ mozilla::dom::BlobImpl** aBlobImpl);
+
+ bool IsValidDropData(mozilla::dom::DataTransfer* aDataTransfer);
+ bool CanDropTheseFiles(mozilla::dom::DataTransfer* aDataTransfer,
+ bool aSupportsMultiple);
+ };
+
+ /**
+ * The text box input.
+ * @see nsFileControlFrame::CreateAnonymousContent
+ */
+ RefPtr<Element> mTextContent;
+ /**
+ * The button to open a file or directory picker.
+ * @see nsFileControlFrame::CreateAnonymousContent
+ */
+ RefPtr<Element> mBrowseFilesOrDirs;
+
+ /**
+ * Drag and drop mouse listener.
+ * This makes sure we don't get used after destruction.
+ */
+ RefPtr<DnDListener> mMouseListener;
+
+ protected:
+ /**
+ * Sync the disabled state of the content with anonymous children.
+ */
+ void SyncDisabledState();
+};
+
+#endif // nsFileControlFrame_h___
diff --git a/layout/forms/nsGfxButtonControlFrame.cpp b/layout/forms/nsGfxButtonControlFrame.cpp
new file mode 100644
index 0000000000..37aa996c27
--- /dev/null
+++ b/layout/forms/nsGfxButtonControlFrame.cpp
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsGfxButtonControlFrame.h"
+#include "nsIFormControl.h"
+#include "nsGkAtoms.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "nsContentUtils.h"
+#include "nsTextNode.h"
+
+using namespace mozilla;
+
+nsGfxButtonControlFrame::nsGfxButtonControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {}
+
+nsContainerFrame* NS_NewGfxButtonControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsGfxButtonControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame)
+
+void nsGfxButtonControlFrame::Destroy(DestroyContext& aContext) {
+ aContext.AddAnonymousContent(mTextContent.forget());
+ nsHTMLButtonControlFrame::Destroy(aContext);
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsGfxButtonControlFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"ButtonControl"_ns, aResult);
+}
+#endif
+
+// Create the text content used as label for the button.
+// The frame will be generated by the frame constructor.
+nsresult nsGfxButtonControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ nsAutoString label;
+ nsresult rv = GetLabel(label);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add a child text content node for the label
+ mTextContent = new (mContent->NodeInfo()->NodeInfoManager())
+ nsTextNode(mContent->NodeInfo()->NodeInfoManager());
+
+ // set the value of the text node and add it to the child list
+ mTextContent->SetText(label, false);
+ aElements.AppendElement(mTextContent);
+
+ return NS_OK;
+}
+
+void nsGfxButtonControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mTextContent) {
+ aElements.AppendElement(mTextContent);
+ }
+}
+
+NS_QUERYFRAME_HEAD(nsGfxButtonControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame)
+
+// Initially we hardcoded the default strings here.
+// Next, we used html.css to store the default label for various types
+// of buttons. (nsGfxButtonControlFrame::DoNavQuirksReflow rev 1.20)
+// However, since html.css is not internationalized, we now grab the default
+// label from a string bundle as is done for all other UI strings.
+// See bug 16999 for further details.
+nsresult nsGfxButtonControlFrame::GetDefaultLabel(nsAString& aString) const {
+ nsCOMPtr<nsIFormControl> form = do_QueryInterface(mContent);
+ NS_ENSURE_TRUE(form, NS_ERROR_UNEXPECTED);
+
+ auto type = form->ControlType();
+ const char* prop;
+ if (type == FormControlType::InputReset) {
+ prop = "Reset";
+ } else if (type == FormControlType::InputSubmit) {
+ prop = "Submit";
+ } else {
+ aString.Truncate();
+ return NS_OK;
+ }
+
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eFORMS_PROPERTIES, prop, mContent->OwnerDoc(), aString);
+}
+
+nsresult nsGfxButtonControlFrame::GetLabel(nsString& aLabel) {
+ // Get the text from the "value" property on our content if there is
+ // one; otherwise set it to a default value (localized).
+ auto* elt = dom::HTMLInputElement::FromNode(mContent);
+ if (elt && elt->HasAttr(nsGkAtoms::value)) {
+ elt->GetValue(aLabel, dom::CallerType::System);
+ } else {
+ // Generate localized label.
+ // We can't make any assumption as to what the default would be
+ // because the value is localized for non-english platforms, thus
+ // it might not be the string "Reset", "Submit Query", or "Browse..."
+ nsresult rv;
+ rv = GetDefaultLabel(aLabel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Compress whitespace out of label if needed.
+ if (!StyleText()->WhiteSpaceIsSignificant()) {
+ aLabel.CompressWhitespace();
+ } else if (aLabel.Length() > 2 && aLabel.First() == ' ' &&
+ aLabel.CharAt(aLabel.Length() - 1) == ' ') {
+ // This is a bit of a hack. The reason this is here is as follows: we now
+ // have default padding on our buttons to make them non-ugly.
+ // Unfortunately, IE-windows does not have such padding, so people will
+ // stick values like " ok " (with the spaces) in the buttons in an attempt
+ // to make them look decent. Unfortunately, if they do this the button
+ // looks way too big in Mozilla. Worse yet, if they do this _and_ set a
+ // fixed width for the button we run into trouble because our focus-rect
+ // border/padding and outer border take up 10px of the horizontal button
+ // space or so; the result is that the text is misaligned, even with the
+ // recentering we do in nsHTMLButtonControlFrame::Reflow. So to solve
+ // this, even if the whitespace is significant, single leading and trailing
+ // _spaces_ (and not other whitespace) are removed. The proper solution,
+ // of course, is to not have the focus rect painting taking up 6px of
+ // horizontal space. We should do that instead (changing the renderer) and
+ // remove this.
+ aLabel.Cut(0, 1);
+ aLabel.Truncate(aLabel.Length() - 1);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsGfxButtonControlFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv = NS_OK;
+
+ // If the value attribute is set, update the text of the label
+ if (nsGkAtoms::value == aAttribute) {
+ if (mTextContent && mContent) {
+ nsAutoString label;
+ rv = GetLabel(label);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mTextContent->SetText(label, true);
+ } else {
+ rv = NS_ERROR_UNEXPECTED;
+ }
+
+ // defer to HTMLButtonControlFrame
+ } else {
+ rv = nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute,
+ aModType);
+ }
+ return rv;
+}
+
+nsContainerFrame* nsGfxButtonControlFrame::GetContentInsertionFrame() {
+ return this;
+}
+
+nsresult nsGfxButtonControlFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ // Override the HandleEvent to prevent the nsIFrame::HandleEvent
+ // from being called. The nsIFrame::HandleEvent causes the button label
+ // to be selected (Drawn with an XOR rectangle over the label)
+
+ if (IsContentDisabled()) {
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+ return NS_OK;
+}
diff --git a/layout/forms/nsGfxButtonControlFrame.h b/layout/forms/nsGfxButtonControlFrame.h
new file mode 100644
index 0000000000..32d4689559
--- /dev/null
+++ b/layout/forms/nsGfxButtonControlFrame.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsGfxButtonControlFrame_h___
+#define nsGfxButtonControlFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsHTMLButtonControlFrame.h"
+#include "nsCOMPtr.h"
+#include "nsIAnonymousContentCreator.h"
+
+class nsTextNode;
+
+// Class which implements the input[type=button, reset, submit] and
+// browse button for input[type=file].
+// The label for button is specified through generated content
+// in the ua.css file.
+
+class nsGfxButtonControlFrame final : public nsHTMLButtonControlFrame,
+ public nsIAnonymousContentCreator {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame)
+
+ explicit nsGfxButtonControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ void Destroy(DestroyContext&) override;
+
+ virtual nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ NS_DECL_QUERYFRAME
+
+ // nsIAnonymousContentCreator
+ virtual nsresult CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) override;
+ virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual nsContainerFrame* GetContentInsertionFrame() override;
+
+ protected:
+ nsresult GetDefaultLabel(nsAString& aLabel) const;
+
+ nsresult GetLabel(nsString& aLabel);
+
+ private:
+ RefPtr<nsTextNode> mTextContent;
+};
+
+#endif
diff --git a/layout/forms/nsHTMLButtonControlFrame.cpp b/layout/forms/nsHTMLButtonControlFrame.cpp
new file mode 100644
index 0000000000..7a599f093b
--- /dev/null
+++ b/layout/forms/nsHTMLButtonControlFrame.cpp
@@ -0,0 +1,394 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsHTMLButtonControlFrame.h"
+
+#include "mozilla/Baseline.h"
+#include "mozilla/PresShell.h"
+#include "nsIFrameInlines.h"
+#include "nsContainerFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsPresContext.h"
+#include "nsLayoutUtils.h"
+#include "nsGkAtoms.h"
+#include "nsButtonFrameRenderer.h"
+#include "nsDisplayList.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+nsContainerFrame* NS_NewHTMLButtonControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsHTMLButtonControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame)
+
+nsHTMLButtonControlFrame::nsHTMLButtonControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext,
+ nsIFrame::ClassID aID)
+ : nsContainerFrame(aStyle, aPresContext, aID) {}
+
+nsHTMLButtonControlFrame::~nsHTMLButtonControlFrame() = default;
+
+void nsHTMLButtonControlFrame::Init(nsIContent* aContent,
+ nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
+ mRenderer.SetFrame(this, PresContext());
+}
+
+NS_QUERYFRAME_HEAD(nsHTMLButtonControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsHTMLButtonControlFrame::AccessibleType() {
+ return a11y::eHTMLButtonType;
+}
+#endif
+
+void nsHTMLButtonControlFrame::SetFocus(bool aOn, bool aRepaint) {}
+
+nsresult nsHTMLButtonControlFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ // if disabled do nothing
+ if (mRenderer.isDisabled()) {
+ return NS_OK;
+ }
+
+ // mouse clicks are handled by content
+ // we don't want our children to get any events. So just pass it to frame.
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+bool nsHTMLButtonControlFrame::ShouldClipPaintingToBorderBox() {
+ // FIXME(emilio): probably should account for per-axis clipping...
+ return StyleDisplay()->mOverflowX != StyleOverflow::Visible;
+}
+
+void nsHTMLButtonControlFrame::BuildDisplayList(
+ nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) {
+ nsDisplayList onTop(aBuilder);
+ if (IsVisibleForPainting()) {
+ // Clip the button itself to its border area for event hit testing.
+ Maybe<DisplayListClipState::AutoSaveRestore> eventClipState;
+ if (aBuilder->IsForEventDelivery()) {
+ eventClipState.emplace(aBuilder);
+ nsRect rect(aBuilder->ToReferenceFrame(this), GetSize());
+ nscoord radii[8];
+ bool hasRadii = GetBorderRadii(radii);
+ eventClipState->ClipContainingBlockDescendants(
+ rect, hasRadii ? radii : nullptr);
+ }
+
+ mRenderer.DisplayButton(aBuilder, aLists.BorderBackground(), &onTop);
+ }
+
+ nsDisplayListCollection set(aBuilder);
+
+ {
+ DisplayListClipState::AutoSaveRestore clipState(aBuilder);
+
+ if (ShouldClipPaintingToBorderBox()) {
+ nsMargin border = StyleBorder()->GetComputedBorder();
+ nsRect rect(aBuilder->ToReferenceFrame(this), GetSize());
+ rect.Deflate(border);
+ nscoord radii[8];
+ bool hasRadii = GetPaddingBoxBorderRadii(radii);
+ clipState.ClipContainingBlockDescendants(rect,
+ hasRadii ? radii : nullptr);
+ }
+
+ BuildDisplayListForChild(aBuilder, mFrames.FirstChild(), set,
+ DisplayChildFlag::ForcePseudoStackingContext);
+ }
+
+ // Put the foreground outline and focus rects on top of the children
+ set.Content()->AppendToTop(&onTop);
+ set.MoveTo(aLists);
+
+ DisplayOutline(aBuilder, aLists);
+
+ // to draw border when selected in editor
+ DisplaySelectionOverlay(aBuilder, aLists.Content());
+}
+
+nscoord nsHTMLButtonControlFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+ if (Maybe<nscoord> containISize = ContainIntrinsicISize()) {
+ result = *containISize;
+ } else {
+ nsIFrame* kid = mFrames.FirstChild();
+ result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, kid,
+ IntrinsicISizeType::MinISize);
+ }
+ return result;
+}
+
+nscoord nsHTMLButtonControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+ if (Maybe<nscoord> containISize = ContainIntrinsicISize()) {
+ result = *containISize;
+ } else {
+ nsIFrame* kid = mFrames.FirstChild();
+ result = nsLayoutUtils::IntrinsicForContainer(
+ aRenderingContext, kid, IntrinsicISizeType::PrefISize);
+ }
+ return result;
+}
+
+void nsHTMLButtonControlFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsHTMLButtonControlFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ // Reflow the child
+ nsIFrame* firstKid = mFrames.FirstChild();
+
+ MOZ_ASSERT(firstKid, "Button should have a child frame for its contents");
+ MOZ_ASSERT(!firstKid->GetNextSibling(),
+ "Button should have exactly one child frame");
+ MOZ_ASSERT(
+ firstKid->Style()->GetPseudoType() == PseudoStyleType::buttonContent,
+ "Button's child frame has unexpected pseudo type!");
+
+ // XXXbz Eventually we may want to check-and-bail if
+ // !aReflowInput.ShouldReflowAllKids() &&
+ // !firstKid->IsSubtreeDirty().
+ // We'd need to cache our ascent for that, of course.
+
+ // Reflow the contents of the button.
+ // (This populates our aDesiredSize, too.)
+ ReflowButtonContents(aPresContext, aDesiredSize, aReflowInput, firstKid);
+
+ if (!ShouldClipPaintingToBorderBox()) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, firstKid);
+ }
+ // else, we ignore child overflow -- anything that overflows beyond our
+ // own border-box will get clipped when painting.
+
+ FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput,
+ aStatus);
+
+ // We're always complete and we don't support overflow containers
+ // so we shouldn't have a next-in-flow ever.
+ aStatus.Reset();
+ MOZ_ASSERT(!GetNextInFlow());
+}
+
+void nsHTMLButtonControlFrame::ReflowButtonContents(
+ nsPresContext* aPresContext, ReflowOutput& aButtonDesiredSize,
+ const ReflowInput& aButtonReflowInput, nsIFrame* aFirstKid) {
+ WritingMode wm = GetWritingMode();
+ LogicalSize availSize = aButtonReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+
+ // shorthand for a value we need to use in a bunch of places
+ const LogicalMargin& clbp =
+ aButtonReflowInput.ComputedLogicalBorderPadding(wm);
+
+ LogicalPoint childPos(wm);
+ childPos.I(wm) = clbp.IStart(wm);
+ availSize.ISize(wm) = std::max(availSize.ISize(wm), 0);
+
+ ReflowInput contentsReflowInput(aPresContext, aButtonReflowInput, aFirstKid,
+ availSize);
+
+ nsReflowStatus contentsReflowStatus;
+ ReflowOutput contentsDesiredSize(aButtonReflowInput);
+ childPos.B(wm) = 0; // This will be set properly later, after reflowing the
+ // child to determine its size.
+
+ if (aFirstKid->IsFlexOrGridContainer()) {
+ // XXX: Should we use ResetResizeFlags::Yes?
+ contentsReflowInput.SetComputedBSize(aButtonReflowInput.ComputedBSize(),
+ ReflowInput::ResetResizeFlags::No);
+ contentsReflowInput.SetComputedMinBSize(
+ aButtonReflowInput.ComputedMinBSize());
+ contentsReflowInput.SetComputedMaxBSize(
+ aButtonReflowInput.ComputedMaxBSize());
+ }
+
+ // We just pass a dummy containerSize here, as the child will be
+ // repositioned later by FinishReflowChild.
+ nsSize dummyContainerSize;
+ ReflowChild(aFirstKid, aPresContext, contentsDesiredSize, contentsReflowInput,
+ wm, childPos, dummyContainerSize, ReflowChildFlags::Default,
+ contentsReflowStatus);
+ MOZ_ASSERT(contentsReflowStatus.IsComplete(),
+ "We gave button-contents frame unconstrained available height, "
+ "so it should be complete");
+
+ // Compute the button's content-box size:
+ LogicalSize buttonContentBox(wm);
+ if (aButtonReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) {
+ // Button has a fixed block-size -- that's its content-box bSize.
+ buttonContentBox.BSize(wm) = aButtonReflowInput.ComputedBSize();
+ } else {
+ // Button is intrinsically sized -- it should shrinkwrap the
+ // button-contents' bSize. But if it has size containment in block axis,
+ // ignore the contents and use contain-intrinsic-block-size.
+ nscoord bSize = aButtonReflowInput.mFrame->ContainIntrinsicBSize().valueOr(
+ contentsDesiredSize.BSize(wm));
+
+ // Make sure we obey min/max-bSize in the case when we're doing intrinsic
+ // sizing (we get it for free when we have a non-intrinsic
+ // aButtonReflowInput.ComputedBSize()). Note that we do this before
+ // adjusting for borderpadding, since mComputedMaxBSize and
+ // mComputedMinBSize are content bSizes.
+ buttonContentBox.BSize(wm) = aButtonReflowInput.ApplyMinMaxBSize(bSize);
+ }
+ if (aButtonReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE) {
+ buttonContentBox.ISize(wm) = aButtonReflowInput.ComputedISize();
+ } else {
+ nscoord iSize = aButtonReflowInput.mFrame->ContainIntrinsicISize().valueOr(
+ contentsDesiredSize.ISize(wm));
+ buttonContentBox.ISize(wm) = aButtonReflowInput.ApplyMinMaxISize(iSize);
+ }
+
+ // Center child in the block-direction in the button
+ // (technically, inside of the button's focus-padding area)
+ nscoord extraSpace =
+ buttonContentBox.BSize(wm) - contentsDesiredSize.BSize(wm);
+
+ childPos.B(wm) = std::max(0, extraSpace / 2);
+
+ // Adjust childPos.B() to be in terms of the button's frame-rect:
+ childPos.B(wm) += clbp.BStart(wm);
+
+ nsSize containerSize = (buttonContentBox + clbp.Size(wm)).GetPhysicalSize(wm);
+
+ // Place the child
+ FinishReflowChild(aFirstKid, aPresContext, contentsDesiredSize,
+ &contentsReflowInput, wm, childPos, containerSize,
+ ReflowChildFlags::Default);
+
+ // Make sure we have a useful 'ascent' value for the child
+ if (contentsDesiredSize.BlockStartAscent() ==
+ ReflowOutput::ASK_FOR_BASELINE) {
+ WritingMode wm = aButtonReflowInput.GetWritingMode();
+ contentsDesiredSize.SetBlockStartAscent(aFirstKid->GetLogicalBaseline(wm));
+ }
+
+ // OK, we're done with the child frame.
+ // Use what we learned to populate the button frame's reflow metrics.
+ // * Button's height & width are content-box size + border-box contribution:
+ aButtonDesiredSize.SetSize(
+ wm,
+ LogicalSize(wm, aButtonReflowInput.ComputedISize() + clbp.IStartEnd(wm),
+ buttonContentBox.BSize(wm) + clbp.BStartEnd(wm)));
+
+ // * Button's ascent is its child's ascent, plus the child's block-offset
+ // within our frame... unless it's orthogonal, in which case we'll use the
+ // contents inline-size as an approximation for now.
+ // XXX is there a better strategy? should we include border-padding?
+ if (!aButtonReflowInput.mStyleDisplay->IsContainLayout()) {
+ if (aButtonDesiredSize.GetWritingMode().IsOrthogonalTo(wm)) {
+ aButtonDesiredSize.SetBlockStartAscent(
+ wm.IsAlphabeticalBaseline() ? contentsDesiredSize.ISize(wm)
+ : contentsDesiredSize.ISize(wm) / 2);
+ } else {
+ aButtonDesiredSize.SetBlockStartAscent(
+ contentsDesiredSize.BlockStartAscent() + childPos.B(wm));
+ }
+ } // else: we're layout-contained, and so we have no baseline.
+
+ aButtonDesiredSize.SetOverflowAreasToDesiredBounds();
+}
+
+Maybe<nscoord> nsHTMLButtonControlFrame::GetNaturalBaselineBOffset(
+ WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext aExportContext) const {
+ if (StyleDisplay()->IsContainLayout()) {
+ return Nothing{};
+ }
+
+ nsIFrame* inner = mFrames.FirstChild();
+ if (MOZ_UNLIKELY(inner->GetWritingMode().IsOrthogonalTo(aWM))) {
+ return Nothing{};
+ }
+ auto result =
+ inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext)
+ .valueOrFrom([inner, aWM, aBaselineGroup]() {
+ return Baseline::SynthesizeBOffsetFromBorderBox(inner, aWM,
+ aBaselineGroup);
+ });
+
+ nscoord innerBStart = inner->BStart(aWM, GetSize());
+ if (aBaselineGroup == BaselineSharingGroup::First) {
+ return Some(result + innerBStart);
+ }
+ return Some(result + BSize(aWM) - (innerBStart + inner->BSize(aWM)));
+}
+
+BaselineSharingGroup nsHTMLButtonControlFrame::GetDefaultBaselineSharingGroup()
+ const {
+ nsIFrame* firstKid = mFrames.FirstChild();
+
+ MOZ_ASSERT(firstKid, "Button should have a child frame for its contents");
+ MOZ_ASSERT(!firstKid->GetNextSibling(),
+ "Button should have exactly one child frame");
+ return firstKid->GetDefaultBaselineSharingGroup();
+}
+
+nscoord nsHTMLButtonControlFrame::SynthesizeFallbackBaseline(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup) const {
+ return Baseline::SynthesizeBOffsetFromMarginBox(this, aWM, aBaselineGroup);
+}
+
+nsresult nsHTMLButtonControlFrame::SetFormProperty(nsAtom* aName,
+ const nsAString& aValue) {
+ if (nsGkAtoms::value == aName) {
+ return mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::value,
+ aValue, true);
+ }
+ return NS_OK;
+}
+
+ComputedStyle* nsHTMLButtonControlFrame::GetAdditionalComputedStyle(
+ int32_t aIndex) const {
+ return mRenderer.GetComputedStyle(aIndex);
+}
+
+void nsHTMLButtonControlFrame::SetAdditionalComputedStyle(
+ int32_t aIndex, ComputedStyle* aComputedStyle) {
+ mRenderer.SetComputedStyle(aIndex, aComputedStyle);
+}
+
+void nsHTMLButtonControlFrame::AppendDirectlyOwnedAnonBoxes(
+ nsTArray<OwnedAnonBox>& aResult) {
+ MOZ_ASSERT(mFrames.FirstChild(), "Must have our button-content anon box");
+ MOZ_ASSERT(!mFrames.FirstChild()->GetNextSibling(),
+ "Must only have our button-content anon box");
+ aResult.AppendElement(OwnedAnonBox(mFrames.FirstChild()));
+}
+
+#ifdef DEBUG
+void nsHTMLButtonControlFrame::AppendFrames(ChildListID aListID,
+ nsFrameList&& aFrameList) {
+ MOZ_CRASH("unsupported operation");
+}
+
+void nsHTMLButtonControlFrame::InsertFrames(
+ ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) {
+ MOZ_CRASH("unsupported operation");
+}
+
+void nsHTMLButtonControlFrame::RemoveFrame(DestroyContext&, ChildListID,
+ nsIFrame*) {
+ MOZ_CRASH("unsupported operation");
+}
+#endif
diff --git a/layout/forms/nsHTMLButtonControlFrame.h b/layout/forms/nsHTMLButtonControlFrame.h
new file mode 100644
index 0000000000..b4409d66a7
--- /dev/null
+++ b/layout/forms/nsHTMLButtonControlFrame.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsHTMLButtonControlFrame_h___
+#define nsHTMLButtonControlFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsButtonFrameRenderer.h"
+
+class gfxContext;
+class nsPresContext;
+
+class nsHTMLButtonControlFrame : public nsContainerFrame,
+ public nsIFormControlFrame {
+ public:
+ explicit nsHTMLButtonControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {}
+
+ ~nsHTMLButtonControlFrame();
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame)
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ nscoord GetMinISize(gfxContext* aRenderingContext) override;
+
+ nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ Maybe<nscoord> GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext aExportContext) const override;
+
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ ComputedStyle* GetAdditionalComputedStyle(int32_t aIndex) const override;
+ void SetAdditionalComputedStyle(int32_t aIndex,
+ ComputedStyle* aComputedStyle) override;
+
+#ifdef DEBUG
+ void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override;
+ void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) override;
+ void RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) override;
+#endif
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() override;
+#endif
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"HTMLButtonControl"_ns, aResult);
+ }
+#endif
+
+ // nsIFormControlFrame
+ void SetFocus(bool aOn, bool aRepaint) override;
+ nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override;
+
+ // Inserted child content gets its frames parented by our child block
+ nsContainerFrame* GetContentInsertionFrame() override {
+ return PrincipalChildList().FirstChild()->GetContentInsertionFrame();
+ }
+
+ // Return the ::-moz-button-content anonymous box.
+ void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override;
+
+ protected:
+ nsHTMLButtonControlFrame(ComputedStyle* aStyle, nsPresContext* aPresContext,
+ nsIFrame::ClassID aID);
+
+ // Indicates whether we should clip our children's painting to our
+ // border-box (either because of "overflow" or because of legacy reasons
+ // about how <input>-flavored buttons work).
+ bool ShouldClipPaintingToBorderBox();
+
+ // Reflows the button's sole child frame, and computes the desired size
+ // of the button itself from the results.
+ void ReflowButtonContents(nsPresContext* aPresContext,
+ ReflowOutput& aButtonDesiredSize,
+ const ReflowInput& aButtonReflowInput,
+ nsIFrame* aFirstKid);
+
+ BaselineSharingGroup GetDefaultBaselineSharingGroup() const override;
+ nscoord SynthesizeFallbackBaseline(
+ mozilla::WritingMode aWM,
+ BaselineSharingGroup aBaselineGroup) const override;
+
+ nsButtonFrameRenderer mRenderer;
+};
+
+#endif
diff --git a/layout/forms/nsIFormControlFrame.h b/layout/forms/nsIFormControlFrame.h
new file mode 100644
index 0000000000..45b7c63aa4
--- /dev/null
+++ b/layout/forms/nsIFormControlFrame.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIFormControlFrame_h___
+#define nsIFormControlFrame_h___
+
+#include "nsQueryFrame.h"
+#include "nsStringFwd.h"
+
+class nsAtom;
+
+/**
+ * nsIFormControlFrame is the common interface for frames of form controls. It
+ * provides a uniform way of creating widgets, resizing, and painting.
+ * @see nsLeafFrame and its base classes for more info
+ */
+class nsIFormControlFrame : public nsQueryFrame {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsIFormControlFrame)
+
+ /**
+ *
+ * @param aOn
+ * @param aRepaint
+ */
+ virtual void SetFocus(bool aOn = true, bool aRepaint = false) = 0;
+
+ /**
+ * Set a property on the form control frame.
+ *
+ * @param aName name of the property to set
+ * @param aValue value of the property
+ * @returns NS_OK if the property name is valid, otherwise an error code
+ */
+ virtual nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) = 0;
+};
+
+#endif
diff --git a/layout/forms/nsISelectControlFrame.h b/layout/forms/nsISelectControlFrame.h
new file mode 100644
index 0000000000..b849f04d52
--- /dev/null
+++ b/layout/forms/nsISelectControlFrame.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsISelectControlFrame_h___
+#define nsISelectControlFrame_h___
+
+#include "nsQueryFrame.h"
+
+/**
+ * nsISelectControlFrame is the interface for combo boxes and listboxes
+ */
+class nsISelectControlFrame : public nsQueryFrame {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsISelectControlFrame)
+
+ /**
+ * Adds an option to the list at index
+ */
+
+ NS_IMETHOD AddOption(int32_t index) = 0;
+
+ /**
+ * Removes the option at index. The caller must have a live script
+ * blocker while calling this method.
+ */
+ NS_IMETHOD RemoveOption(int32_t index) = 0;
+
+ /**
+ * Sets whether the parser is done adding children
+ * @param aIsDone whether the parser is done adding children
+ */
+ NS_IMETHOD DoneAddingChildren(bool aIsDone) = 0;
+
+ /**
+ * Notify the frame when an option is selected
+ */
+ NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) = 0;
+
+ /**
+ * Notify the frame when selectedIndex was changed. This might
+ * destroy the frame.
+ */
+ NS_IMETHOD_(void)
+ OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) = 0;
+};
+
+#endif
diff --git a/layout/forms/nsITextControlFrame.h b/layout/forms/nsITextControlFrame.h
new file mode 100644
index 0000000000..a1c1df1e2e
--- /dev/null
+++ b/layout/forms/nsITextControlFrame.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsITextControlFrame_h___
+#define nsITextControlFrame_h___
+
+#include "nsIFormControlFrame.h"
+#include "mozilla/AlreadyAddRefed.h"
+
+class nsISelectionController;
+class nsFrameSelection;
+
+namespace mozilla {
+class TextEditor;
+} // namespace mozilla
+
+// FIXME(emilio): This has only one implementation, seems it could be removed...
+class nsITextControlFrame : public nsIFormControlFrame {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsITextControlFrame)
+
+ enum class SelectionDirection : uint8_t { None, Forward, Backward };
+
+ virtual already_AddRefed<mozilla::TextEditor> GetTextEditor() = 0;
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD
+ SetSelectionRange(uint32_t aSelectionStart, uint32_t aSelectionEnd,
+ SelectionDirection = SelectionDirection::None) = 0;
+
+ NS_IMETHOD GetOwnedSelectionController(nsISelectionController** aSelCon) = 0;
+ virtual nsFrameSelection* GetOwnedFrameSelection() = 0;
+
+ /**
+ * Ensure editor is initialized with the proper flags and the default value.
+ * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created
+ * @throws various and sundry other things
+ */
+ virtual nsresult EnsureEditorInitialized() = 0;
+};
+
+#endif
diff --git a/layout/forms/nsImageControlFrame.cpp b/layout/forms/nsImageControlFrame.cpp
new file mode 100644
index 0000000000..17e3893a2f
--- /dev/null
+++ b/layout/forms/nsImageControlFrame.cpp
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsImageFrame.h"
+
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "nsIFormControlFrame.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsLayoutUtils.h"
+#include "nsIContent.h"
+
+using namespace mozilla;
+
+class nsImageControlFrame final : public nsImageFrame,
+ public nsIFormControlFrame {
+ public:
+ explicit nsImageControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+ ~nsImageControlFrame() final;
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) final;
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsImageControlFrame)
+
+ void Reflow(nsPresContext*, ReflowOutput&, const ReflowInput&,
+ nsReflowStatus&) final;
+
+ nsresult HandleEvent(nsPresContext*, WidgetGUIEvent*, nsEventStatus*) final;
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() final;
+#endif
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const final {
+ return MakeFrameName(u"ImageControl"_ns, aResult);
+ }
+#endif
+
+ Cursor GetCursor(const nsPoint&) final;
+
+ // nsIFormContromFrame
+ void SetFocus(bool aOn, bool aRepaint) final;
+ nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final;
+};
+
+nsImageControlFrame::nsImageControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsImageFrame(aStyle, aPresContext, kClassID) {}
+
+nsImageControlFrame::~nsImageControlFrame() = default;
+
+nsIFrame* NS_NewImageControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsImageControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsImageControlFrame)
+
+void nsImageControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsImageFrame::Init(aContent, aParent, aPrevInFlow);
+
+ if (aPrevInFlow) {
+ return;
+ }
+
+ mContent->SetProperty(nsGkAtoms::imageClickedPoint, new CSSIntPoint(0, 0),
+ nsINode::DeleteProperty<CSSIntPoint>);
+}
+
+NS_QUERYFRAME_HEAD(nsImageControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsImageFrame)
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsImageControlFrame::AccessibleType() {
+ if (mContent->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input)) {
+ return a11y::eHTMLButtonType;
+ }
+
+ return a11y::eNoType;
+}
+#endif
+
+void nsImageControlFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ DO_GLOBAL_REFLOW_COUNT("nsImageControlFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ return nsImageFrame::Reflow(aPresContext, aDesiredSize, aReflowInput,
+ aStatus);
+}
+
+nsresult nsImageControlFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ // Don't do anything if the event has already been handled by someone
+ if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ if (IsContentDisabled()) {
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+
+ *aEventStatus = nsEventStatus_eIgnore;
+
+ if (aEvent->mMessage == eMouseUp &&
+ aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) {
+ // Store click point for HTMLInputElement::SubmitNamesValues
+ // Do this on MouseUp because the specs don't say and that's what IE does
+ auto* lastClickedPoint = static_cast<CSSIntPoint*>(
+ mContent->GetProperty(nsGkAtoms::imageClickedPoint));
+ if (lastClickedPoint) {
+ // normally lastClickedPoint is not null, as it's allocated in Init()
+ nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, RelativeTo{this});
+ *lastClickedPoint = TranslateEventCoords(pt);
+ }
+ }
+ return nsImageFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+void nsImageControlFrame::SetFocus(bool aOn, bool aRepaint) {}
+
+nsIFrame::Cursor nsImageControlFrame::GetCursor(const nsPoint&) {
+ StyleCursorKind kind = StyleUI()->Cursor().keyword;
+ if (kind == StyleCursorKind::Auto) {
+ kind = StyleCursorKind::Pointer;
+ }
+ return Cursor{kind, AllowCustomCursorImage::Yes};
+}
+
+nsresult nsImageControlFrame::SetFormProperty(nsAtom* aName,
+ const nsAString& aValue) {
+ return NS_OK;
+}
diff --git a/layout/forms/nsListControlFrame.cpp b/layout/forms/nsListControlFrame.cpp
new file mode 100644
index 0000000000..44ce9fde13
--- /dev/null
+++ b/layout/forms/nsListControlFrame.cpp
@@ -0,0 +1,1211 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nscore.h"
+#include "nsCOMPtr.h"
+#include "nsUnicharUtils.h"
+#include "nsListControlFrame.h"
+#include "HTMLSelectEventListener.h"
+#include "nsGkAtoms.h"
+#include "nsComboboxControlFrame.h"
+#include "nsFontMetrics.h"
+#include "nsIScrollableFrame.h"
+#include "nsCSSRendering.h"
+#include "nsLayoutUtils.h"
+#include "nsDisplayList.h"
+#include "nsContentUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/HTMLOptGroupElement.h"
+#include "mozilla/dom/HTMLOptionsCollection.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEvents.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// Static members
+nsListControlFrame* nsListControlFrame::mFocused = nullptr;
+
+//---------------------------------------------------------
+nsListControlFrame* NS_NewListControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ nsListControlFrame* it =
+ new (aPresShell) nsListControlFrame(aStyle, aPresShell->GetPresContext());
+
+ it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION);
+
+ return it;
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame)
+
+nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsHTMLScrollFrame(aStyle, aPresContext, kClassID, false),
+ mMightNeedSecondPass(false),
+ mHasPendingInterruptAtStartOfReflow(false),
+ mForceSelection(false) {
+ mChangesSinceDragStart = false;
+
+ mIsAllContentHere = false;
+ mIsAllFramesHere = false;
+ mHasBeenInitialized = false;
+ mNeedToReset = true;
+ mPostChildrenLoadedReset = false;
+}
+
+nsListControlFrame::~nsListControlFrame() = default;
+
+Maybe<nscoord> nsListControlFrame::GetNaturalBaselineBOffset(
+ WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext) const {
+ // Unlike scroll frames which we inherit from, we don't export a baseline.
+ return Nothing{};
+}
+// for Bug 47302 (remove this comment later)
+void nsListControlFrame::Destroy(DestroyContext& aContext) {
+ // get the receiver interface from the browser button's content node
+ NS_ENSURE_TRUE_VOID(mContent);
+
+ // Clear the frame pointer on our event listener, just in case the
+ // event listener can outlive the frame.
+
+ mEventListener->Detach();
+ nsHTMLScrollFrame::Destroy(aContext);
+}
+
+void nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ // We allow visibility:hidden <select>s to contain visible options.
+
+ // Don't allow painting of list controls when painting is suppressed.
+ // XXX why do we need this here? we should never reach this. Maybe
+ // because these can have widgets? Hmm
+ if (aBuilder->IsBackgroundOnly()) return;
+
+ DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame");
+
+ nsHTMLScrollFrame::BuildDisplayList(aBuilder, aLists);
+}
+
+HTMLOptionElement* nsListControlFrame::GetCurrentOption() const {
+ return mEventListener->GetCurrentOption();
+}
+
+/**
+ * This is called by the SelectsAreaFrame, which is the same
+ * as the frame returned by GetOptionsContainer. It's the frame which is
+ * scrolled by us.
+ * @param aPt the offset of this frame, relative to the rendering reference
+ * frame
+ */
+void nsListControlFrame::PaintFocus(DrawTarget* aDrawTarget, nsPoint aPt) {
+ if (mFocused != this) return;
+
+ nsIFrame* containerFrame = GetOptionsContainer();
+ if (!containerFrame) return;
+
+ nsIFrame* childframe = nullptr;
+ nsCOMPtr<nsIContent> focusedContent = GetCurrentOption();
+ if (focusedContent) {
+ childframe = focusedContent->GetPrimaryFrame();
+ }
+
+ nsRect fRect;
+ if (childframe) {
+ // get the child rect
+ fRect = childframe->GetRect();
+ // get it into our coordinates
+ fRect.MoveBy(childframe->GetParent()->GetOffsetTo(this));
+ } else {
+ float inflation = nsLayoutUtils::FontSizeInflationFor(this);
+ fRect.x = fRect.y = 0;
+ if (GetWritingMode().IsVertical()) {
+ fRect.width = GetScrollPortRect().width;
+ fRect.height = CalcFallbackRowBSize(inflation);
+ } else {
+ fRect.width = CalcFallbackRowBSize(inflation);
+ fRect.height = GetScrollPortRect().height;
+ }
+ fRect.MoveBy(containerFrame->GetOffsetTo(this));
+ }
+ fRect += aPt;
+
+ const auto* domOpt = HTMLOptionElement::FromNodeOrNull(focusedContent);
+ const bool isSelected = domOpt && domOpt->Selected();
+
+ // Set up back stop colors and then ask L&F service for the real colors
+ nscolor color =
+ LookAndFeel::Color(isSelected ? LookAndFeel::ColorID::Selecteditemtext
+ : LookAndFeel::ColorID::Selecteditem,
+ this);
+
+ nsCSSRendering::PaintFocus(PresContext(), aDrawTarget, fRect, color);
+}
+
+void nsListControlFrame::InvalidateFocus() {
+ if (mFocused != this) return;
+
+ nsIFrame* containerFrame = GetOptionsContainer();
+ if (containerFrame) {
+ containerFrame->InvalidateFrame();
+ }
+}
+
+NS_QUERYFRAME_HEAD(nsListControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+ NS_QUERYFRAME_ENTRY(nsISelectControlFrame)
+ NS_QUERYFRAME_ENTRY(nsListControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame)
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsListControlFrame::AccessibleType() {
+ return a11y::eHTMLSelectListType;
+}
+#endif
+
+// Return true if we found at least one <option> or non-empty <optgroup> label
+// that has a frame. aResult will be the maximum BSize of those.
+static bool GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM,
+ nscoord* aResult) {
+ bool found = false;
+ for (nsIFrame* child : aContainer->PrincipalChildList()) {
+ if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) {
+ // An optgroup; drill through any scroll frame and recurse. |inner| might
+ // be null here though if |inner| is an anonymous leaf frame of some sort.
+ auto inner = child->GetContentInsertionFrame();
+ if (inner && GetMaxRowBSize(inner, aWM, aResult)) {
+ found = true;
+ }
+ } else {
+ // an option or optgroup label
+ bool isOptGroupLabel =
+ child->Style()->IsPseudoElement() &&
+ aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup);
+ nscoord childBSize = child->BSize(aWM);
+ // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now
+ if (!isOptGroupLabel || childBSize > nscoord(0)) {
+ found = true;
+ *aResult = std::max(childBSize, *aResult);
+ }
+ }
+ }
+ return found;
+}
+
+//-----------------------------------------------------------------
+// Main Reflow for ListBox/Dropdown
+//-----------------------------------------------------------------
+
+nscoord nsListControlFrame::CalcBSizeOfARow() {
+ // Calculate the block size in our writing mode of a single row in the
+ // listbox or dropdown list by using the tallest thing in the subtree,
+ // since there may be option groups in addition to option elements,
+ // either of which may be visible or invisible, may use different
+ // fonts, etc.
+ nscoord rowBSize(0);
+ if (GetContainSizeAxes().mBContained ||
+ !GetMaxRowBSize(GetOptionsContainer(), GetWritingMode(), &rowBSize)) {
+ // We don't have any <option>s or <optgroup> labels with a frame.
+ // (Or we're size-contained in block axis, which has the same outcome for
+ // our sizing.)
+ float inflation = nsLayoutUtils::FontSizeInflationFor(this);
+ rowBSize = CalcFallbackRowBSize(inflation);
+ }
+ return rowBSize;
+}
+
+nscoord nsListControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+
+ // Always add scrollbar inline sizes to the pref-inline-size of the
+ // scrolled content. Combobox frames depend on this happening in the
+ // dropdown, and standalone listboxes are overflow:scroll so they need
+ // it too.
+ WritingMode wm = GetWritingMode();
+ Maybe<nscoord> containISize = ContainIntrinsicISize();
+ result = containISize ? *containISize
+ : GetScrolledFrame()->GetPrefISize(aRenderingContext);
+ LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes());
+ result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm));
+ return result;
+}
+
+nscoord nsListControlFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+
+ // Always add scrollbar inline sizes to the min-inline-size of the
+ // scrolled content. Combobox frames depend on this happening in the
+ // dropdown, and standalone listboxes are overflow:scroll so they need
+ // it too.
+ WritingMode wm = GetWritingMode();
+ Maybe<nscoord> containISize = ContainIntrinsicISize();
+ result = containISize ? *containISize
+ : GetScrolledFrame()->GetMinISize(aRenderingContext);
+ LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes());
+ result += scrollbarSize.IStartEnd(wm);
+
+ return result;
+}
+
+void nsListControlFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE,
+ "Must have a computed inline size");
+
+ SchedulePaint();
+
+ mHasPendingInterruptAtStartOfReflow = aPresContext->HasPendingInterrupt();
+
+ // If all the content and frames are here
+ // then initialize it before reflow
+ if (mIsAllContentHere && !mHasBeenInitialized) {
+ if (!mIsAllFramesHere) {
+ CheckIfAllFramesHere();
+ }
+ if (mIsAllFramesHere && !mHasBeenInitialized) {
+ mHasBeenInitialized = true;
+ }
+ }
+
+ MarkInReflow();
+ // Due to the fact that our intrinsic block size depends on the block
+ // sizes of our kids, we end up having to do two-pass reflow, in
+ // general -- the first pass to find the intrinsic block size and a
+ // second pass to reflow the scrollframe at that block size (which
+ // will size the scrollbars correctly, etc).
+ //
+ // Naturally, we want to avoid doing the second reflow as much as
+ // possible. We can skip it in the following cases (in all of which the first
+ // reflow is already happening at the right block size):
+ bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE);
+ Maybe<nscoord> containBSize = ContainIntrinsicBSize(NS_UNCONSTRAINEDSIZE);
+ bool usingContainBSize =
+ autoBSize && containBSize && *containBSize != NS_UNCONSTRAINEDSIZE;
+
+ mMightNeedSecondPass = [&] {
+ if (!autoBSize) {
+ // We're reflowing with a constrained computed block size -- just use that
+ // block size.
+ return false;
+ }
+ if (!IsSubtreeDirty() && !aReflowInput.ShouldReflowAllKids()) {
+ // We're not dirty and have no dirty kids and shouldn't be reflowing all
+ // kids. In this case, our cached max block size of a child is not going
+ // to change.
+ return false;
+ }
+ if (usingContainBSize) {
+ // We're size-contained in the block axis. In this case the size of a row
+ // doesn't depend on our children (it's the "fallback" size).
+ return false;
+ }
+ // We might need to do a second pass. If we do our first reflow using our
+ // cached max block size of a child, then compute the new max block size,
+ // and it's the same as the old one, we might still skip it (see the
+ // IsScrollbarUpdateSuppressed() check).
+ return true;
+ }();
+
+ ReflowInput state(aReflowInput);
+ int32_t length = GetNumberOfRows();
+
+ nscoord oldBSizeOfARow = BSizeOfARow();
+
+ if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && autoBSize) {
+ // When not doing an initial reflow, and when the block size is
+ // auto, start off with our computed block size set to what we'd
+ // expect our block size to be.
+ nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length);
+ computedBSize = state.ApplyMinMaxBSize(computedBSize);
+ state.SetComputedBSize(computedBSize);
+ }
+
+ if (usingContainBSize) {
+ state.SetComputedBSize(*containBSize);
+ }
+
+ nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
+
+ if (!mMightNeedSecondPass) {
+ NS_ASSERTION(!autoBSize || BSizeOfARow() == oldBSizeOfARow,
+ "How did our BSize of a row change if nothing was dirty?");
+ NS_ASSERTION(!autoBSize || !HasAnyStateBits(NS_FRAME_FIRST_REFLOW) ||
+ usingContainBSize,
+ "How do we not need a second pass during initial reflow at "
+ "auto BSize?");
+ NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
+ "Shouldn't be suppressing if we don't need a second pass!");
+ if (!autoBSize || usingContainBSize) {
+ // Update our mNumDisplayRows based on our new row block size now
+ // that we know it. Note that if autoBSize and we landed in this
+ // code then we already set mNumDisplayRows in CalcIntrinsicBSize.
+ // Also note that we can't use BSizeOfARow() here because that
+ // just uses a cached value that we didn't compute.
+ nscoord rowBSize = CalcBSizeOfARow();
+ if (rowBSize == 0) {
+ // Just pick something
+ mNumDisplayRows = 1;
+ } else {
+ mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize);
+ }
+ }
+
+ return;
+ }
+
+ mMightNeedSecondPass = false;
+
+ // Now see whether we need a second pass. If we do, our
+ // nsSelectsAreaFrame will have suppressed the scrollbar update.
+ if (!IsScrollbarUpdateSuppressed()) {
+ // All done. No need to do more reflow.
+ return;
+ }
+
+ SetSuppressScrollbarUpdate(false);
+
+ // Gotta reflow again.
+ // XXXbz We're just changing the block size here; do we need to dirty
+ // ourselves or anything like that? We might need to, per the letter
+ // of the reflow protocol, but things seem to work fine without it...
+ // Is that just an implementation detail of nsHTMLScrollFrame that
+ // we're depending on?
+ nsHTMLScrollFrame::DidReflow(aPresContext, &state);
+
+ // Now compute the block size we want to have
+ nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length);
+ computedBSize = state.ApplyMinMaxBSize(computedBSize);
+ state.SetComputedBSize(computedBSize);
+
+ // XXXbz to make the ascent really correct, we should add our
+ // mComputedPadding.top to it (and subtract it from descent). Need that
+ // because nsGfxScrollFrame just adds in the border....
+ aStatus.Reset();
+ nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
+}
+
+bool nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const {
+ return true;
+}
+
+//---------------------------------------------------------
+nsContainerFrame* nsListControlFrame::GetContentInsertionFrame() {
+ return GetOptionsContainer()->GetContentInsertionFrame();
+}
+
+//---------------------------------------------------------
+bool nsListControlFrame::ExtendedSelection(int32_t aStartIndex,
+ int32_t aEndIndex, bool aClearAll) {
+ return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex, true, aClearAll);
+}
+
+//---------------------------------------------------------
+bool nsListControlFrame::SingleSelection(int32_t aClickedIndex,
+ bool aDoToggle) {
+#ifdef ACCESSIBILITY
+ nsCOMPtr<nsIContent> prevOption = mEventListener->GetCurrentOption();
+#endif
+ bool wasChanged = false;
+ // Get Current selection
+ if (aDoToggle) {
+ wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex);
+ } else {
+ wasChanged =
+ SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex, true, true);
+ }
+ AutoWeakFrame weakFrame(this);
+ ScrollToIndex(aClickedIndex);
+ if (!weakFrame.IsAlive()) {
+ return wasChanged;
+ }
+
+ mStartSelectionIndex = aClickedIndex;
+ mEndSelectionIndex = aClickedIndex;
+ InvalidateFocus();
+
+#ifdef ACCESSIBILITY
+ FireMenuItemActiveEvent(prevOption);
+#endif
+
+ return wasChanged;
+}
+
+void nsListControlFrame::InitSelectionRange(int32_t aClickedIndex) {
+ //
+ // If nothing is selected, set the start selection depending on where
+ // the user clicked and what the initial selection is:
+ // - if the user clicked *before* selectedIndex, set the start index to
+ // the end of the first contiguous selection.
+ // - if the user clicked *after* the end of the first contiguous
+ // selection, set the start index to selectedIndex.
+ // - if the user clicked *within* the first contiguous selection, set the
+ // start index to selectedIndex.
+ // The last two rules, of course, boil down to the same thing: if the user
+ // clicked >= selectedIndex, return selectedIndex.
+ //
+ // This makes it so that shift click works properly when you first click
+ // in a multiple select.
+ //
+ int32_t selectedIndex = GetSelectedIndex();
+ if (selectedIndex >= 0) {
+ // Get the end of the contiguous selection
+ RefPtr<dom::HTMLOptionsCollection> options = GetOptions();
+ NS_ASSERTION(options, "Collection of options is null!");
+ uint32_t numOptions = options->Length();
+ // Push i to one past the last selected index in the group.
+ uint32_t i;
+ for (i = selectedIndex + 1; i < numOptions; i++) {
+ if (!options->ItemAsOption(i)->Selected()) {
+ break;
+ }
+ }
+
+ if (aClickedIndex < selectedIndex) {
+ // User clicked before selection, so start selection at end of
+ // contiguous selection
+ mStartSelectionIndex = i - 1;
+ mEndSelectionIndex = selectedIndex;
+ } else {
+ // User clicked after selection, so start selection at start of
+ // contiguous selection
+ mStartSelectionIndex = selectedIndex;
+ mEndSelectionIndex = i - 1;
+ }
+ }
+}
+
+static uint32_t CountOptionsAndOptgroups(nsIFrame* aFrame) {
+ uint32_t count = 0;
+ for (nsIFrame* child : aFrame->PrincipalChildList()) {
+ nsIContent* content = child->GetContent();
+ if (content) {
+ if (content->IsHTMLElement(nsGkAtoms::option)) {
+ ++count;
+ } else {
+ RefPtr<HTMLOptGroupElement> optgroup =
+ HTMLOptGroupElement::FromNode(content);
+ if (optgroup) {
+ nsAutoString label;
+ optgroup->GetLabel(label);
+ if (label.Length() > 0) {
+ ++count;
+ }
+ count += CountOptionsAndOptgroups(child);
+ }
+ }
+ }
+ }
+ return count;
+}
+
+uint32_t nsListControlFrame::GetNumberOfRows() {
+ return ::CountOptionsAndOptgroups(GetContentInsertionFrame());
+}
+
+//---------------------------------------------------------
+bool nsListControlFrame::PerformSelection(int32_t aClickedIndex, bool aIsShift,
+ bool aIsControl) {
+ bool wasChanged = false;
+
+ if (aClickedIndex == kNothingSelected && !mForceSelection) {
+ // Ignore kNothingSelected unless the selection is forced
+ } else if (GetMultiple()) {
+ if (aIsShift) {
+ // Make sure shift+click actually does something expected when
+ // the user has never clicked on the select
+ if (mStartSelectionIndex == kNothingSelected) {
+ InitSelectionRange(aClickedIndex);
+ }
+
+ // Get the range from beginning (low) to end (high)
+ // Shift *always* works, even if the current option is disabled
+ int32_t startIndex;
+ int32_t endIndex;
+ if (mStartSelectionIndex == kNothingSelected) {
+ startIndex = aClickedIndex;
+ endIndex = aClickedIndex;
+ } else if (mStartSelectionIndex <= aClickedIndex) {
+ startIndex = mStartSelectionIndex;
+ endIndex = aClickedIndex;
+ } else {
+ startIndex = aClickedIndex;
+ endIndex = mStartSelectionIndex;
+ }
+
+ // Clear only if control was not pressed
+ wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl);
+ AutoWeakFrame weakFrame(this);
+ ScrollToIndex(aClickedIndex);
+ if (!weakFrame.IsAlive()) {
+ return wasChanged;
+ }
+
+ if (mStartSelectionIndex == kNothingSelected) {
+ mStartSelectionIndex = aClickedIndex;
+ }
+#ifdef ACCESSIBILITY
+ nsCOMPtr<nsIContent> prevOption = GetCurrentOption();
+#endif
+ mEndSelectionIndex = aClickedIndex;
+ InvalidateFocus();
+
+#ifdef ACCESSIBILITY
+ FireMenuItemActiveEvent(prevOption);
+#endif
+ } else if (aIsControl) {
+ wasChanged = SingleSelection(aClickedIndex, true); // might destroy us
+ } else {
+ wasChanged = SingleSelection(aClickedIndex, false); // might destroy us
+ }
+ } else {
+ wasChanged = SingleSelection(aClickedIndex, false); // might destroy us
+ }
+
+ return wasChanged;
+}
+
+//---------------------------------------------------------
+bool nsListControlFrame::HandleListSelection(dom::Event* aEvent,
+ int32_t aClickedIndex) {
+ MouseEvent* mouseEvent = aEvent->AsMouseEvent();
+ bool isControl;
+#ifdef XP_MACOSX
+ isControl = mouseEvent->MetaKey();
+#else
+ isControl = mouseEvent->CtrlKey();
+#endif
+ bool isShift = mouseEvent->ShiftKey();
+ return PerformSelection(aClickedIndex, isShift,
+ isControl); // might destroy us
+}
+
+//---------------------------------------------------------
+void nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents) {
+ if (aGrabMouseEvents) {
+ PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState);
+ } else {
+ nsIContent* capturingContent = PresShell::GetCapturingContent();
+ if (capturingContent == mContent) {
+ // only clear the capturing content if *we* are the ones doing the
+ // capturing (or if the dropdown is hidden, in which case NO-ONE should
+ // be capturing anything - it could be a scrollbar inside this listbox
+ // which is actually grabbing
+ // This shouldn't be necessary. We should simply ensure that events
+ // targeting scrollbars are never visible to DOM consumers.
+ PresShell::ReleaseCapturingContent();
+ }
+ }
+}
+
+//---------------------------------------------------------
+nsresult nsListControlFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ /*const char * desc[] = {"eMouseMove",
+ "NS_MOUSE_LEFT_BUTTON_UP",
+ "NS_MOUSE_LEFT_BUTTON_DOWN",
+ "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
+ "NS_MOUSE_MIDDLE_BUTTON_UP",
+ "NS_MOUSE_MIDDLE_BUTTON_DOWN",
+ "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
+ "NS_MOUSE_RIGHT_BUTTON_UP",
+ "NS_MOUSE_RIGHT_BUTTON_DOWN",
+ "eMouseOver",
+ "eMouseOut",
+ "NS_MOUSE_LEFT_DOUBLECLICK",
+ "NS_MOUSE_MIDDLE_DOUBLECLICK",
+ "NS_MOUSE_RIGHT_DOUBLECLICK",
+ "NS_MOUSE_LEFT_CLICK",
+ "NS_MOUSE_MIDDLE_CLICK",
+ "NS_MOUSE_RIGHT_CLICK"};
+ int inx = aEvent->mMessage - eMouseEventFirst;
+ if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) {
+ printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage);
+ } else {
+ printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage);
+ }*/
+
+ if (nsEventStatus_eConsumeNoDefault == *aEventStatus) return NS_OK;
+
+ // disabled state affects how we're selected, but we don't want to go through
+ // nsHTMLScrollFrame if we're disabled.
+ if (IsContentDisabled()) {
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+
+ return nsHTMLScrollFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+//---------------------------------------------------------
+void nsListControlFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ if (aListID == FrameChildListID::Principal) {
+ // First check to see if all the content has been added
+ mIsAllContentHere = Select().IsDoneAddingChildren();
+ if (!mIsAllContentHere) {
+ mIsAllFramesHere = false;
+ mHasBeenInitialized = false;
+ }
+ }
+ nsHTMLScrollFrame::SetInitialChildList(aListID, std::move(aChildList));
+}
+
+HTMLSelectElement& nsListControlFrame::Select() const {
+ return *static_cast<HTMLSelectElement*>(GetContent());
+}
+
+//---------------------------------------------------------
+void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsHTMLScrollFrame::Init(aContent, aParent, aPrevInFlow);
+
+ // we shouldn't have to unregister this listener because when
+ // our frame goes away all these content node go away as well
+ // because our frame is the only one who references them.
+ // we need to hook up our listeners before the editor is initialized
+ mEventListener = new HTMLSelectEventListener(
+ Select(), HTMLSelectEventListener::SelectType::Listbox);
+
+ mStartSelectionIndex = kNothingSelected;
+ mEndSelectionIndex = kNothingSelected;
+}
+
+dom::HTMLOptionsCollection* nsListControlFrame::GetOptions() const {
+ return Select().Options();
+}
+
+dom::HTMLOptionElement* nsListControlFrame::GetOption(uint32_t aIndex) const {
+ return Select().Item(aIndex);
+}
+
+NS_IMETHODIMP
+nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) {
+ if (aSelected) {
+ ScrollToIndex(aIndex);
+ }
+ return NS_OK;
+}
+
+void nsListControlFrame::OnContentReset() { ResetList(true); }
+
+void nsListControlFrame::ResetList(bool aAllowScrolling) {
+ // if all the frames aren't here
+ // don't bother reseting
+ if (!mIsAllFramesHere) {
+ return;
+ }
+
+ if (aAllowScrolling) {
+ mPostChildrenLoadedReset = true;
+
+ // Scroll to the selected index
+ int32_t indexToSelect = kNothingSelected;
+
+ HTMLSelectElement* selectElement = HTMLSelectElement::FromNode(mContent);
+ if (selectElement) {
+ indexToSelect = selectElement->SelectedIndex();
+ AutoWeakFrame weakFrame(this);
+ ScrollToIndex(indexToSelect);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ }
+ }
+
+ mStartSelectionIndex = kNothingSelected;
+ mEndSelectionIndex = kNothingSelected;
+ InvalidateFocus();
+ // Combobox will redisplay itself with the OnOptionSelected event
+}
+
+void nsListControlFrame::SetFocus(bool aOn, bool aRepaint) {
+ InvalidateFocus();
+
+ if (aOn) {
+ mFocused = this;
+ } else {
+ mFocused = nullptr;
+ }
+
+ InvalidateFocus();
+}
+
+void nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) {
+ aStr.Truncate();
+ if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) {
+ optionElement->GetRenderedLabel(aStr);
+ }
+}
+
+int32_t nsListControlFrame::GetSelectedIndex() {
+ dom::HTMLSelectElement* select =
+ dom::HTMLSelectElement::FromNodeOrNull(mContent);
+ return select->SelectedIndex();
+}
+
+uint32_t nsListControlFrame::GetNumberOfOptions() {
+ dom::HTMLOptionsCollection* options = GetOptions();
+ if (!options) {
+ return 0;
+ }
+
+ return options->Length();
+}
+
+//----------------------------------------------------------------------
+// nsISelectControlFrame
+//----------------------------------------------------------------------
+bool nsListControlFrame::CheckIfAllFramesHere() {
+ // XXX Need to find a fail proof way to determine that
+ // all the frames are there
+ mIsAllFramesHere = true;
+
+ // now make sure we have a frame each piece of content
+
+ return mIsAllFramesHere;
+}
+
+NS_IMETHODIMP
+nsListControlFrame::DoneAddingChildren(bool aIsDone) {
+ mIsAllContentHere = aIsDone;
+ if (mIsAllContentHere) {
+ // Here we check to see if all the frames have been created
+ // for all the content.
+ // If so, then we can initialize;
+ if (!mIsAllFramesHere) {
+ // if all the frames are now present we can initialize
+ if (CheckIfAllFramesHere()) {
+ mHasBeenInitialized = true;
+ ResetList(true);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsListControlFrame::AddOption(int32_t aIndex) {
+#ifdef DO_REFLOW_DEBUG
+ printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId, this, aIndex);
+#endif
+
+ if (!mIsAllContentHere) {
+ mIsAllContentHere = Select().IsDoneAddingChildren();
+ if (!mIsAllContentHere) {
+ mIsAllFramesHere = false;
+ mHasBeenInitialized = false;
+ } else {
+ mIsAllFramesHere =
+ (aIndex == static_cast<int32_t>(GetNumberOfOptions() - 1));
+ }
+ }
+
+ // Make sure we scroll to the selected option as needed
+ mNeedToReset = true;
+
+ if (!mHasBeenInitialized) {
+ return NS_OK;
+ }
+
+ mPostChildrenLoadedReset = mIsAllContentHere;
+ return NS_OK;
+}
+
+static int32_t DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength) {
+ return aLength == 0 ? nsListControlFrame::kNothingSelected
+ : std::max(0, aSelectionIndex - 1);
+}
+
+NS_IMETHODIMP
+nsListControlFrame::RemoveOption(int32_t aIndex) {
+ MOZ_ASSERT(aIndex >= 0, "negative <option> index");
+
+ // Need to reset if we're a dropdown
+ if (mStartSelectionIndex != kNothingSelected) {
+ NS_ASSERTION(mEndSelectionIndex != kNothingSelected, "");
+ int32_t numOptions = GetNumberOfOptions();
+ // NOTE: numOptions is the new number of options whereas aIndex is the
+ // unadjusted index of the removed option (hence the <= below).
+ NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index");
+
+ int32_t forward = mEndSelectionIndex - mStartSelectionIndex;
+ int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex;
+ int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex;
+ if (aIndex < *low) *low = ::DecrementAndClamp(*low, numOptions);
+ if (aIndex <= *high) *high = ::DecrementAndClamp(*high, numOptions);
+ if (forward == 0) *low = *high;
+ } else
+ NS_ASSERTION(mEndSelectionIndex == kNothingSelected, "");
+
+ InvalidateFocus();
+ return NS_OK;
+}
+
+//---------------------------------------------------------
+// Set the option selected in the DOM. This method is named
+// as it is because it indicates that the frame is the source
+// of this event rather than the receiver.
+bool nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex,
+ int32_t aEndIndex,
+ bool aValue,
+ bool aClearAll) {
+ using OptionFlag = HTMLSelectElement::OptionFlag;
+ RefPtr<HTMLSelectElement> selectElement =
+ HTMLSelectElement::FromNode(mContent);
+
+ HTMLSelectElement::OptionFlags mask = OptionFlag::Notify;
+ if (mForceSelection) {
+ mask += OptionFlag::SetDisabled;
+ }
+ if (aValue) {
+ mask += OptionFlag::IsSelected;
+ }
+ if (aClearAll) {
+ mask += OptionFlag::ClearAll;
+ }
+
+ return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask);
+}
+
+bool nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex) {
+ RefPtr<HTMLOptionElement> option = GetOption(static_cast<uint32_t>(aIndex));
+ NS_ENSURE_TRUE(option, false);
+
+ RefPtr<HTMLSelectElement> selectElement =
+ HTMLSelectElement::FromNode(mContent);
+
+ HTMLSelectElement::OptionFlags mask = HTMLSelectElement::OptionFlag::Notify;
+ if (!option->Selected()) {
+ mask += HTMLSelectElement::OptionFlag::IsSelected;
+ }
+
+ return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask);
+}
+
+// Dispatch event and such
+bool nsListControlFrame::UpdateSelection() {
+ if (mIsAllFramesHere) {
+ // if it's a combobox, display the new text. Note that after
+ // FireOnInputAndOnChange we might be dead, as that can run script.
+ AutoWeakFrame weakFrame(this);
+ if (mIsAllContentHere) {
+ RefPtr listener = mEventListener;
+ listener->FireOnInputAndOnChange();
+ }
+ return weakFrame.IsAlive();
+ }
+ return true;
+}
+
+NS_IMETHODIMP_(void)
+nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) {
+#ifdef ACCESSIBILITY
+ nsCOMPtr<nsIContent> prevOption = GetCurrentOption();
+#endif
+
+ AutoWeakFrame weakFrame(this);
+ ScrollToIndex(aNewIndex);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ mStartSelectionIndex = aNewIndex;
+ mEndSelectionIndex = aNewIndex;
+ InvalidateFocus();
+
+#ifdef ACCESSIBILITY
+ if (aOldIndex != aNewIndex) {
+ FireMenuItemActiveEvent(prevOption);
+ }
+#endif
+}
+
+//----------------------------------------------------------------------
+// End nsISelectControlFrame
+//----------------------------------------------------------------------
+
+class AsyncReset final : public Runnable {
+ public:
+ AsyncReset(nsListControlFrame* aFrame, bool aScroll)
+ : Runnable("AsyncReset"), mFrame(aFrame), mScroll(aScroll) {}
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ if (mFrame.IsAlive()) {
+ static_cast<nsListControlFrame*>(mFrame.GetFrame())->ResetList(mScroll);
+ }
+ return NS_OK;
+ }
+
+ private:
+ WeakFrame mFrame;
+ bool mScroll;
+};
+
+nsresult nsListControlFrame::SetFormProperty(nsAtom* aName,
+ const nsAString& aValue) {
+ if (nsGkAtoms::selected == aName) {
+ return NS_ERROR_INVALID_ARG; // Selected is readonly according to spec.
+ } else if (nsGkAtoms::selectedindex == aName) {
+ // You shouldn't be calling me for this!!!
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // We should be told about selectedIndex by the DOM element through
+ // OnOptionSelected
+
+ return NS_OK;
+}
+
+void nsListControlFrame::DidReflow(nsPresContext* aPresContext,
+ const ReflowInput* aReflowInput) {
+ bool wasInterrupted = !mHasPendingInterruptAtStartOfReflow &&
+ aPresContext->HasPendingInterrupt();
+
+ nsHTMLScrollFrame::DidReflow(aPresContext, aReflowInput);
+
+ if (mNeedToReset && !wasInterrupted) {
+ mNeedToReset = false;
+ // Suppress scrolling to the selected element if we restored scroll
+ // history state AND the list contents have not changed since we loaded
+ // all the children AND nothing else forced us to scroll by calling
+ // ResetList(true). The latter two conditions are folded into
+ // mPostChildrenLoadedReset.
+ //
+ // The idea is that we want scroll history restoration to trump ResetList
+ // scrolling to the selected element, when the ResetList was probably only
+ // caused by content loading normally.
+ const bool scroll = !DidHistoryRestore() || mPostChildrenLoadedReset;
+ nsContentUtils::AddScriptRunner(new AsyncReset(this, scroll));
+ }
+
+ mHasPendingInterruptAtStartOfReflow = false;
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsListControlFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"ListControl"_ns, aResult);
+}
+#endif
+
+nscoord nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); }
+
+bool nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex) const {
+ auto& select = Select();
+ if (HTMLOptionElement* item = select.Item(aIndex)) {
+ return IsOptionInteractivelySelectable(&select, item);
+ }
+ return false;
+}
+
+bool nsListControlFrame::IsOptionInteractivelySelectable(
+ HTMLSelectElement* aSelect, HTMLOptionElement* aOption) {
+ return !aSelect->IsOptionDisabled(aOption) && aOption->GetPrimaryFrame();
+}
+
+nscoord nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) {
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation);
+ return fontMet->MaxHeight();
+}
+
+nscoord nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow,
+ int32_t aNumberOfOptions) {
+ mNumDisplayRows = Select().Size();
+ if (mNumDisplayRows < 1) {
+ mNumDisplayRows = 4;
+ }
+ return mNumDisplayRows * aBSizeOfARow;
+}
+
+#ifdef ACCESSIBILITY
+void nsListControlFrame::FireMenuItemActiveEvent(nsIContent* aPreviousOption) {
+ if (mFocused != this) {
+ return;
+ }
+
+ nsIContent* optionContent = GetCurrentOption();
+ if (aPreviousOption == optionContent) {
+ // No change
+ return;
+ }
+
+ if (aPreviousOption) {
+ FireDOMEvent(u"DOMMenuItemInactive"_ns, aPreviousOption);
+ }
+
+ if (optionContent) {
+ FireDOMEvent(u"DOMMenuItemActive"_ns, optionContent);
+ }
+}
+#endif
+
+nsresult nsListControlFrame::GetIndexFromDOMEvent(dom::Event* aMouseEvent,
+ int32_t& aCurIndex) {
+ if (PresShell::GetCapturingContent() != mContent) {
+ // If we're not capturing, then ignore movement in the border
+ nsPoint pt =
+ nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this);
+ nsRect borderInnerEdge = GetScrollPortRect();
+ if (!borderInnerEdge.Contains(pt)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ RefPtr<dom::HTMLOptionElement> option;
+ for (nsCOMPtr<nsIContent> content =
+ PresContext()->EventStateManager()->GetEventTargetContent(nullptr);
+ content && !option; content = content->GetParent()) {
+ option = dom::HTMLOptionElement::FromNode(content);
+ }
+
+ if (option) {
+ aCurIndex = option->Index();
+ MOZ_ASSERT(aCurIndex >= 0);
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsListControlFrame::HandleLeftButtonMouseDown(
+ dom::Event* aMouseEvent) {
+ int32_t selectedIndex;
+ if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
+ // Handle Like List
+ CaptureMouseEvents(true);
+ AutoWeakFrame weakFrame(this);
+ bool change =
+ HandleListSelection(aMouseEvent, selectedIndex); // might destroy us
+ if (!weakFrame.IsAlive()) {
+ return NS_OK;
+ }
+ mChangesSinceDragStart = change;
+ }
+ return NS_OK;
+}
+
+nsresult nsListControlFrame::HandleLeftButtonMouseUp(dom::Event* aMouseEvent) {
+ if (!StyleVisibility()->IsVisible()) {
+ return NS_OK;
+ }
+ // Notify
+ if (mChangesSinceDragStart) {
+ // reset this so that future MouseUps without a prior MouseDown
+ // won't fire onchange
+ mChangesSinceDragStart = false;
+ RefPtr listener = mEventListener;
+ listener->FireOnInputAndOnChange();
+ // Note that `this` may be dead now, as the above call runs script.
+ }
+ return NS_OK;
+}
+
+nsresult nsListControlFrame::DragMove(dom::Event* aMouseEvent) {
+ NS_ASSERTION(aMouseEvent, "aMouseEvent is null.");
+
+ int32_t selectedIndex;
+ if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
+ // Don't waste cycles if we already dragged over this item
+ if (selectedIndex == mEndSelectionIndex) {
+ return NS_OK;
+ }
+ MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ NS_ASSERTION(mouseEvent, "aMouseEvent is not a MouseEvent!");
+ bool isControl;
+#ifdef XP_MACOSX
+ isControl = mouseEvent->MetaKey();
+#else
+ isControl = mouseEvent->CtrlKey();
+#endif
+ AutoWeakFrame weakFrame(this);
+ // Turn SHIFT on when you are dragging, unless control is on.
+ bool wasChanged = PerformSelection(selectedIndex, !isControl, isControl);
+ if (!weakFrame.IsAlive()) {
+ return NS_OK;
+ }
+ mChangesSinceDragStart = mChangesSinceDragStart || wasChanged;
+ }
+ return NS_OK;
+}
+
+//----------------------------------------------------------------------
+// Scroll helpers.
+//----------------------------------------------------------------------
+void nsListControlFrame::ScrollToIndex(int32_t aIndex) {
+ if (aIndex < 0) {
+ // XXX shouldn't we just do nothing if we're asked to scroll to
+ // kNothingSelected?
+ ScrollTo(nsPoint(0, 0), ScrollMode::Instant);
+ } else {
+ RefPtr<dom::HTMLOptionElement> option =
+ GetOption(AssertedCast<uint32_t>(aIndex));
+ if (option) {
+ ScrollToFrame(*option);
+ }
+ }
+}
+
+void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) {
+ // otherwise we find the content's frame and scroll to it
+ if (nsIFrame* childFrame = aOptElement.GetPrimaryFrame()) {
+ RefPtr<mozilla::PresShell> presShell = PresShell();
+ presShell->ScrollFrameIntoView(childFrame, Nothing(), ScrollAxis(),
+ ScrollAxis(),
+ ScrollFlags::ScrollOverflowHidden |
+ ScrollFlags::ScrollFirstAncestorOnly);
+ }
+}
+
+void nsListControlFrame::UpdateSelectionAfterKeyEvent(
+ int32_t aNewIndex, uint32_t aCharCode, bool aIsShift, bool aIsControlOrMeta,
+ bool aIsControlSelectMode) {
+ // If you hold control, but not shift, no key will actually do anything
+ // except space.
+ AutoWeakFrame weakFrame(this);
+ bool wasChanged = false;
+ if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') {
+#ifdef ACCESSIBILITY
+ nsCOMPtr<nsIContent> prevOption = GetCurrentOption();
+#endif
+ mStartSelectionIndex = aNewIndex;
+ mEndSelectionIndex = aNewIndex;
+ InvalidateFocus();
+ ScrollToIndex(aNewIndex);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+
+#ifdef ACCESSIBILITY
+ FireMenuItemActiveEvent(prevOption);
+#endif
+ } else if (aIsControlSelectMode && aCharCode == ' ') {
+ wasChanged = SingleSelection(aNewIndex, true);
+ } else {
+ wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta);
+ }
+ if (wasChanged && weakFrame.IsAlive()) {
+ // dispatch event, update combobox, etc.
+ UpdateSelection();
+ }
+}
diff --git a/layout/forms/nsListControlFrame.h b/layout/forms/nsListControlFrame.h
new file mode 100644
index 0000000000..7c43eb6f2e
--- /dev/null
+++ b/layout/forms/nsListControlFrame.h
@@ -0,0 +1,350 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsListControlFrame_h___
+#define nsListControlFrame_h___
+
+#ifdef DEBUG_evaughan
+// #define DEBUG_rods
+#endif
+
+#ifdef DEBUG_rods
+// #define DO_REFLOW_DEBUG
+// #define DO_REFLOW_COUNTER
+// #define DO_UNCONSTRAINED_CHECK
+// #define DO_PIXELS
+#endif
+
+#include "mozilla/Attributes.h"
+#include "mozilla/StaticPtr.h"
+#include "nsGfxScrollFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsISelectControlFrame.h"
+#include "nsSelectsAreaFrame.h"
+
+class nsComboboxControlFrame;
+class nsPresContext;
+
+namespace mozilla {
+class PresShell;
+class HTMLSelectEventListener;
+
+namespace dom {
+class Event;
+class HTMLOptionElement;
+class HTMLSelectElement;
+class HTMLOptionsCollection;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * Frame-based listbox.
+ */
+
+class nsListControlFrame final : public nsHTMLScrollFrame,
+ public nsIFormControlFrame,
+ public nsISelectControlFrame {
+ public:
+ typedef mozilla::dom::HTMLOptionElement HTMLOptionElement;
+
+ friend nsListControlFrame* NS_NewListControlFrame(
+ mozilla::PresShell* aPresShell, ComputedStyle* aStyle);
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsListControlFrame)
+
+ Maybe<nscoord> GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext) const override;
+
+ // nsIFrame
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) final;
+
+ void SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) final;
+
+ nscoord GetPrefISize(gfxContext* aRenderingContext) final;
+ nscoord GetMinISize(gfxContext* aRenderingContext) final;
+
+ void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final;
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) final;
+
+ void DidReflow(nsPresContext* aPresContext,
+ const ReflowInput* aReflowInput) final;
+ void Destroy(DestroyContext&) override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) final;
+
+ nsContainerFrame* GetContentInsertionFrame() final;
+
+ int32_t GetEndSelectionIndex() const { return mEndSelectionIndex; }
+
+ mozilla::dom::HTMLOptionElement* GetCurrentOption() const;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const final;
+#endif
+
+ // nsIFormControlFrame
+ nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void SetFocus(bool aOn = true, bool aRepaint = false) final;
+
+ bool ShouldPropagateComputedBSizeToScrolledContent() const final;
+
+ // for accessibility purposes
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() final;
+#endif
+
+ int32_t GetSelectedIndex();
+
+ /**
+ * Gets the text of the currently selected item.
+ * If the there are zero items then an empty string is returned
+ * If there is nothing selected, then the 0th item's text is returned.
+ */
+ void GetOptionText(uint32_t aIndex, nsAString& aStr);
+
+ void CaptureMouseEvents(bool aGrabMouseEvents);
+ nscoord GetBSizeOfARow();
+ uint32_t GetNumberOfOptions();
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnContentReset();
+
+ // nsISelectControlFrame
+ NS_IMETHOD AddOption(int32_t index) final;
+ NS_IMETHOD RemoveOption(int32_t index) final;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD DoneAddingChildren(bool aIsDone) final;
+
+ /**
+ * Gets the content (an option) by index and then set it as
+ * being selected or not selected.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) final;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD_(void)
+ OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) final;
+
+ /**
+ * Mouse event listeners.
+ * @note These methods might destroy the frame, pres shell and other objects.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult HandleLeftButtonMouseDown(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT
+ nsresult HandleLeftButtonMouseUp(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT
+ nsresult DragMove(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT
+
+ MOZ_CAN_RUN_SCRIPT
+ bool PerformSelection(int32_t aClickedIndex, bool aIsShift, bool aIsControl);
+ MOZ_CAN_RUN_SCRIPT
+ void UpdateSelectionAfterKeyEvent(int32_t aNewIndex, uint32_t aCharCode,
+ bool aIsShift, bool aIsControlOrMeta,
+ bool aIsControlSelectMode);
+
+ /**
+ * Returns the options collection for mContent, if any.
+ */
+ mozilla::dom::HTMLOptionsCollection* GetOptions() const;
+ /**
+ * Returns the HTMLOptionElement for a given index in mContent's collection.
+ */
+ HTMLOptionElement* GetOption(uint32_t aIndex) const;
+
+ // Helper
+ bool IsFocused() { return this == mFocused; }
+
+ /**
+ * Function to paint the focus rect when our nsSelectsAreaFrame is painting.
+ * @param aPt the offset of this frame, relative to the rendering reference
+ * frame
+ */
+ void PaintFocus(mozilla::gfx::DrawTarget* aDrawTarget, nsPoint aPt);
+
+ /**
+ * If this frame IsFocused(), invalidates an area that includes anything
+ * that PaintFocus will or could have painted --- basically the whole
+ * GetOptionsContainer, plus some extra stuff if there are no options. This
+ * must be called every time mEndSelectionIndex changes.
+ */
+ void InvalidateFocus();
+
+ /**
+ * Function to calculate the block size of a row, for use with the
+ * "size" attribute.
+ * Can't be const because GetNumberOfOptions() isn't const.
+ */
+ nscoord CalcBSizeOfARow();
+
+ /**
+ * Function to ask whether we're currently in what might be the
+ * first pass of a two-pass reflow.
+ */
+ bool MightNeedSecondPass() const { return mMightNeedSecondPass; }
+
+ void SetSuppressScrollbarUpdate(bool aSuppress) {
+ nsHTMLScrollFrame::SetSuppressScrollbarUpdate(aSuppress);
+ }
+
+ /**
+ * Return the number of displayed rows in the list.
+ */
+ uint32_t GetNumDisplayRows() const { return mNumDisplayRows; }
+
+#ifdef ACCESSIBILITY
+ /**
+ * Post a custom DOM event for the change, so that accessibility can
+ * fire a native focus event for accessibility
+ * (Some 3rd party products need to track our focus)
+ */
+ void FireMenuItemActiveEvent(
+ nsIContent* aPreviousOption); // Inform assistive tech what got focused
+#endif
+
+ protected:
+ /**
+ * Updates the selected text in a combobox and then calls FireOnChange().
+ * @note This method might destroy the frame, pres shell and other objects.
+ * Returns false if calling it destroyed |this|.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ bool UpdateSelection();
+
+ /**
+ * Returns whether mContent supports multiple selection.
+ */
+ bool GetMultiple() const {
+ return mContent->AsElement()->HasAttr(nsGkAtoms::multiple);
+ }
+
+ mozilla::dom::HTMLSelectElement& Select() const;
+
+ /**
+ * @return true if the <option> at aIndex is selectable by the user.
+ */
+ bool IsOptionInteractivelySelectable(int32_t aIndex) const;
+ /**
+ * @return true if aOption in aSelect is selectable by the user.
+ */
+ static bool IsOptionInteractivelySelectable(
+ mozilla::dom::HTMLSelectElement* aSelect,
+ mozilla::dom::HTMLOptionElement* aOption);
+
+ MOZ_CAN_RUN_SCRIPT void ScrollToFrame(HTMLOptionElement& aOptElement);
+
+ MOZ_CAN_RUN_SCRIPT void ScrollToIndex(int32_t anIndex);
+
+ public:
+ /**
+ * Resets the select back to it's original default values;
+ * those values as determined by the original HTML
+ */
+ MOZ_CAN_RUN_SCRIPT void ResetList(bool aAllowScrolling);
+
+ protected:
+ explicit nsListControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+ virtual ~nsListControlFrame();
+
+ /**
+ * Sets the mSelectedIndex and mOldSelectedIndex from figuring out what
+ * item was selected using content
+ * @param aPoint the event point, in listcontrolframe coordinates
+ * @return NS_OK if it successfully found the selection
+ */
+ nsresult GetIndexFromDOMEvent(mozilla::dom::Event* aMouseEvent,
+ int32_t& aCurIndex);
+
+ bool CheckIfAllFramesHere();
+
+ // guess at a row block size based on our own style.
+ nscoord CalcFallbackRowBSize(float aFontSizeInflation);
+
+ // CalcIntrinsicBSize computes our intrinsic block size (taking the
+ // "size" attribute into account). This should only be called in
+ // non-dropdown mode.
+ nscoord CalcIntrinsicBSize(nscoord aBSizeOfARow, int32_t aNumberOfOptions);
+
+ // Dropped down stuff
+ void SetComboboxItem(int32_t aIndex);
+
+ // Selection
+ bool SetOptionsSelectedFromFrame(int32_t aStartIndex, int32_t aEndIndex,
+ bool aValue, bool aClearAll);
+ bool ToggleOptionSelectedFromFrame(int32_t aIndex);
+
+ MOZ_CAN_RUN_SCRIPT
+ bool SingleSelection(int32_t aClickedIndex, bool aDoToggle);
+ bool ExtendedSelection(int32_t aStartIndex, int32_t aEndIndex,
+ bool aClearAll);
+ MOZ_CAN_RUN_SCRIPT
+ bool HandleListSelection(mozilla::dom::Event* aDOMEvent,
+ int32_t selectedIndex);
+ void InitSelectionRange(int32_t aClickedIndex);
+
+ public:
+ nsSelectsAreaFrame* GetOptionsContainer() const {
+ return static_cast<nsSelectsAreaFrame*>(GetScrolledFrame());
+ }
+
+ static constexpr int32_t kNothingSelected = -1;
+
+ protected:
+ nscoord BSizeOfARow() { return GetOptionsContainer()->BSizeOfARow(); }
+
+ /**
+ * @return how many displayable options/optgroups this frame has.
+ */
+ uint32_t GetNumberOfRows();
+
+ // Data Members
+ int32_t mStartSelectionIndex;
+ int32_t mEndSelectionIndex;
+
+ uint32_t mNumDisplayRows;
+ bool mChangesSinceDragStart : 1;
+
+ // Has the user selected a visible item since we showed the dropdown?
+ bool mItemSelectionStarted : 1;
+
+ bool mIsAllContentHere : 1;
+ bool mIsAllFramesHere : 1;
+ bool mHasBeenInitialized : 1;
+ bool mNeedToReset : 1;
+ bool mPostChildrenLoadedReset : 1;
+
+ // True if we're in the middle of a reflow and might need a second
+ // pass. This only happens for auto heights.
+ bool mMightNeedSecondPass : 1;
+
+ /**
+ * Set to aPresContext->HasPendingInterrupt() at the start of Reflow.
+ * Set to false at the end of DidReflow.
+ */
+ bool mHasPendingInterruptAtStartOfReflow : 1;
+
+ // True if the selection can be set to nothing or disabled options.
+ bool mForceSelection : 1;
+
+ RefPtr<mozilla::HTMLSelectEventListener> mEventListener;
+
+ static nsListControlFrame* mFocused;
+
+#ifdef DO_REFLOW_COUNTER
+ int32_t mReflowId;
+#endif
+};
+
+#endif /* nsListControlFrame_h___ */
diff --git a/layout/forms/nsMeterFrame.cpp b/layout/forms/nsMeterFrame.cpp
new file mode 100644
index 0000000000..a72a6816d5
--- /dev/null
+++ b/layout/forms/nsMeterFrame.cpp
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMeterFrame.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLMeterElement.h"
+#include "nsIContent.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsNodeInfoManager.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsFontMetrics.h"
+#include <algorithm>
+
+using namespace mozilla;
+using mozilla::dom::Document;
+using mozilla::dom::Element;
+using mozilla::dom::HTMLMeterElement;
+
+nsIFrame* NS_NewMeterFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsMeterFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsMeterFrame)
+
+nsMeterFrame::nsMeterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID), mBarDiv(nullptr) {}
+
+nsMeterFrame::~nsMeterFrame() = default;
+
+void nsMeterFrame::Destroy(DestroyContext& aContext) {
+ NS_ASSERTION(!GetPrevContinuation(),
+ "nsMeterFrame should not have continuations; if it does we "
+ "need to call RegUnregAccessKey only for the first.");
+ aContext.AddAnonymousContent(mBarDiv.forget());
+ nsContainerFrame::Destroy(aContext);
+}
+
+nsresult nsMeterFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ // Get the NodeInfoManager and tag necessary to create the meter bar div.
+ nsCOMPtr<Document> doc = mContent->GetComposedDoc();
+
+ // Create the div.
+ mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div);
+
+ // Associate the right pseudo-element to the anonymous child.
+ if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) {
+ // TODO(emilio): Create also a slider-track pseudo-element.
+ mBarDiv->SetPseudoElementType(PseudoStyleType::sliderFill);
+ } else {
+ mBarDiv->SetPseudoElementType(PseudoStyleType::mozMeterBar);
+ }
+
+ aElements.AppendElement(mBarDiv);
+
+ return NS_OK;
+}
+
+void nsMeterFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) {
+ if (mBarDiv) {
+ aElements.AppendElement(mBarDiv);
+ }
+}
+
+NS_QUERYFRAME_HEAD(nsMeterFrame)
+ NS_QUERYFRAME_ENTRY(nsMeterFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+void nsMeterFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsMeterFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ NS_ASSERTION(mBarDiv, "Meter bar div must exist!");
+ NS_ASSERTION(!GetPrevContinuation(),
+ "nsMeterFrame should not have continuations; if it does we "
+ "need to call RegUnregAccessKey only for the first.");
+
+ nsIFrame* barFrame = mBarDiv->GetPrimaryFrame();
+ NS_ASSERTION(barFrame, "The meter frame should have a child with a frame!");
+
+ ReflowBarFrame(barFrame, aPresContext, aReflowInput, aStatus);
+
+ const auto wm = aReflowInput.GetWritingMode();
+ aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm));
+
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, barFrame);
+ FinishAndStoreOverflow(&aDesiredSize);
+
+ aStatus.Reset(); // This type of frame can't be split.
+}
+
+void nsMeterFrame::ReflowBarFrame(nsIFrame* aBarFrame,
+ nsPresContext* aPresContext,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ bool vertical = ResolvedOrientationIsVertical();
+ WritingMode wm = aBarFrame->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+ ReflowInput reflowInput(aPresContext, aReflowInput, aBarFrame, availSize);
+ nscoord size =
+ vertical ? aReflowInput.ComputedHeight() : aReflowInput.ComputedWidth();
+ nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left;
+ nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top;
+
+ auto* meterElement = static_cast<HTMLMeterElement*>(GetContent());
+ size = NSToCoordRound(size * meterElement->Position());
+
+ if (!vertical && wm.IsPhysicalRTL()) {
+ xoffset += aReflowInput.ComputedWidth() - size;
+ }
+
+ // The bar position is *always* constrained.
+ if (vertical) {
+ // We want the bar to begin at the bottom.
+ yoffset += aReflowInput.ComputedHeight() - size;
+
+ size -= reflowInput.ComputedPhysicalMargin().TopBottom() +
+ reflowInput.ComputedPhysicalBorderPadding().TopBottom();
+ size = std::max(size, 0);
+ reflowInput.SetComputedHeight(size);
+ } else {
+ size -= reflowInput.ComputedPhysicalMargin().LeftRight() +
+ reflowInput.ComputedPhysicalBorderPadding().LeftRight();
+ size = std::max(size, 0);
+ reflowInput.SetComputedWidth(size);
+ }
+
+ xoffset += reflowInput.ComputedPhysicalMargin().left;
+ yoffset += reflowInput.ComputedPhysicalMargin().top;
+
+ ReflowOutput barDesiredSize(reflowInput);
+ ReflowChild(aBarFrame, aPresContext, barDesiredSize, reflowInput, xoffset,
+ yoffset, ReflowChildFlags::Default, aStatus);
+ FinishReflowChild(aBarFrame, aPresContext, barDesiredSize, &reflowInput,
+ xoffset, yoffset, ReflowChildFlags::Default);
+}
+
+nsresult nsMeterFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {
+ NS_ASSERTION(mBarDiv, "Meter bar div must exist!");
+
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max ||
+ aAttribute == nsGkAtoms::min)) {
+ nsIFrame* barFrame = mBarDiv->GetPrimaryFrame();
+ NS_ASSERTION(barFrame, "The meter frame should have a child with a frame!");
+ PresShell()->FrameNeedsReflow(barFrame, IntrinsicDirty::None,
+ NS_FRAME_IS_DIRTY);
+ InvalidateFrame();
+ }
+
+ return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+}
+
+LogicalSize nsMeterFrame::ComputeAutoSize(
+ gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize,
+ nscoord aAvailableISize, const LogicalSize& aMargin,
+ const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
+ ComputeSizeFlags aFlags) {
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+
+ const WritingMode wm = GetWritingMode();
+ LogicalSize autoSize(wm);
+ autoSize.BSize(wm) = autoSize.ISize(wm) =
+ fontMet->Font().size.ToAppUnits(); // 1em
+
+ if (ResolvedOrientationIsVertical() == wm.IsVertical()) {
+ autoSize.ISize(wm) *= 5; // 5em
+ } else {
+ autoSize.BSize(wm) *= 5; // 5em
+ }
+
+ return autoSize.ConvertTo(aWM, wm);
+}
+
+nscoord nsMeterFrame::GetMinISize(gfxContext* aRenderingContext) {
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+
+ nscoord minISize = fontMet->Font().size.ToAppUnits(); // 1em
+
+ if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) {
+ // The orientation is inline
+ minISize *= 5; // 5em
+ }
+
+ return minISize;
+}
+
+nscoord nsMeterFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ return GetMinISize(aRenderingContext);
+}
+
+bool nsMeterFrame::ShouldUseNativeStyle() const {
+ nsIFrame* barFrame = mBarDiv->GetPrimaryFrame();
+
+ // Use the native style if these conditions are satisfied:
+ // - both frames use the native appearance;
+ // - neither frame has author specified rules setting the border or the
+ // background.
+ return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Meter &&
+ !Style()->HasAuthorSpecifiedBorderOrBackground() && barFrame &&
+ barFrame->StyleDisplay()->EffectiveAppearance() ==
+ StyleAppearance::Meterchunk &&
+ !barFrame->Style()->HasAuthorSpecifiedBorderOrBackground();
+}
diff --git a/layout/forms/nsMeterFrame.h b/layout/forms/nsMeterFrame.h
new file mode 100644
index 0000000000..4aa82c1384
--- /dev/null
+++ b/layout/forms/nsMeterFrame.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMeterFrame_h___
+#define nsMeterFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+#include "nsCSSPseudoElements.h"
+
+class nsMeterFrame final : public nsContainerFrame,
+ public nsIAnonymousContentCreator
+
+{
+ typedef mozilla::dom::Element Element;
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsMeterFrame)
+
+ explicit nsMeterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ virtual ~nsMeterFrame();
+
+ void Destroy(DestroyContext&) override;
+
+ virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"Meter"_ns, aResult);
+ }
+#endif
+
+ // nsIAnonymousContentCreator
+ virtual nsresult CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) override;
+ virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual mozilla::LogicalSize ComputeAutoSize(
+ gfxContext* aRenderingContext, mozilla::WritingMode aWM,
+ const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize,
+ const mozilla::LogicalSize& aMargin,
+ const mozilla::LogicalSize& aBorderPadding,
+ const mozilla::StyleSizeOverrides& aSizeOverrides,
+ mozilla::ComputeSizeFlags aFlags) override;
+
+ virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ /**
+ * Returns whether the frame and its child should use the native style.
+ */
+ bool ShouldUseNativeStyle() const;
+
+ protected:
+ // Helper function which reflow the anonymous div frame.
+ void ReflowBarFrame(nsIFrame* aBarFrame, nsPresContext* aPresContext,
+ const ReflowInput& aReflowInput, nsReflowStatus& aStatus);
+ /**
+ * The div used to show the meter bar.
+ * @see nsMeterFrame::CreateAnonymousContent
+ */
+ nsCOMPtr<Element> mBarDiv;
+};
+
+#endif
diff --git a/layout/forms/nsNumberControlFrame.cpp b/layout/forms/nsNumberControlFrame.cpp
new file mode 100644
index 0000000000..e8ed87c8f3
--- /dev/null
+++ b/layout/forms/nsNumberControlFrame.cpp
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNumberControlFrame.h"
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/PresShell.h"
+#include "HTMLInputElement.h"
+#include "nsGkAtoms.h"
+#include "nsNameSpaceManager.h"
+#include "nsStyleConsts.h"
+#include "nsContentUtils.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsCSSPseudoElements.h"
+#include "nsLayoutUtils.h"
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/AccTypes.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame* NS_NewNumberControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsNumberControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsNumberControlFrame)
+
+NS_QUERYFRAME_HEAD(nsNumberControlFrame)
+ NS_QUERYFRAME_ENTRY(nsNumberControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsTextControlFrame)
+
+nsNumberControlFrame::nsNumberControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsTextControlFrame(aStyle, aPresContext, kClassID) {}
+
+void nsNumberControlFrame::Destroy(DestroyContext& aContext) {
+ aContext.AddAnonymousContent(mSpinBox.forget());
+ nsTextControlFrame::Destroy(aContext);
+}
+
+nsresult nsNumberControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ // We create an anonymous tree for our input element that is structured as
+ // follows:
+ //
+ // input
+ // div - placeholder
+ // div - preview div
+ // div - editor root
+ // div - spin box wrapping up/down arrow buttons
+ // div - spin up (up arrow button)
+ // div - spin down (down arrow button)
+ //
+ // If you change this, be careful to change the order of stuff returned in
+ // AppendAnonymousContentTo.
+
+ nsTextControlFrame::CreateAnonymousContent(aElements);
+
+ // The author has elected to hide the spinner by setting this
+ // -moz-appearance. We will reframe if it changes.
+ if (StyleDisplay()->EffectiveAppearance() != StyleAppearance::Textfield) {
+ // Create the ::-moz-number-spin-box pseudo-element:
+ mSpinBox = MakeAnonElement(PseudoStyleType::mozNumberSpinBox);
+
+ // Create the ::-moz-number-spin-up pseudo-element:
+ mSpinUp = MakeAnonElement(PseudoStyleType::mozNumberSpinUp, mSpinBox);
+
+ // Create the ::-moz-number-spin-down pseudo-element:
+ mSpinDown = MakeAnonElement(PseudoStyleType::mozNumberSpinDown, mSpinBox);
+
+ aElements.AppendElement(mSpinBox);
+ }
+
+ return NS_OK;
+}
+
+/* static */
+nsNumberControlFrame* nsNumberControlFrame::GetNumberControlFrameForSpinButton(
+ nsIFrame* aFrame) {
+ // If aFrame is a spin button for an <input type=number> then we expect the
+ // frame of the NAC root parent to be that input's frame. We have to check for
+ // this via the content tree because we don't know whether extra frames will
+ // be wrapped around any of the elements between aFrame and the
+ // nsNumberControlFrame that we're looking for (e.g. flex wrappers).
+ nsIContent* content = aFrame->GetContent();
+ auto* nacHost = content->GetClosestNativeAnonymousSubtreeRootParentOrHost();
+ if (!nacHost) {
+ return nullptr;
+ }
+ auto* input = HTMLInputElement::FromNode(nacHost);
+ if (!input || input->ControlType() != FormControlType::InputNumber) {
+ return nullptr;
+ }
+ return do_QueryFrame(input->GetPrimaryFrame());
+}
+
+int32_t nsNumberControlFrame::GetSpinButtonForPointerEvent(
+ WidgetGUIEvent* aEvent) const {
+ MOZ_ASSERT(aEvent->mClass == eMouseEventClass, "Unexpected event type");
+
+ if (!mSpinBox) {
+ // we don't have a spinner
+ return eSpinButtonNone;
+ }
+ if (aEvent->mOriginalTarget == mSpinUp) {
+ return eSpinButtonUp;
+ }
+ if (aEvent->mOriginalTarget == mSpinDown) {
+ return eSpinButtonDown;
+ }
+ if (aEvent->mOriginalTarget == mSpinBox) {
+ // In the case that the up/down buttons are hidden (display:none) we use
+ // just the spin box element, spinning up if the pointer is over the top
+ // half of the element, or down if it's over the bottom half. This is
+ // important to handle since this is the state things are in for the
+ // default UA style sheet. See the comment in forms.css for why.
+ LayoutDeviceIntPoint absPoint = aEvent->mRefPoint;
+ nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, absPoint, RelativeTo{mSpinBox->GetPrimaryFrame()});
+ if (point != nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) {
+ if (point.y < mSpinBox->GetPrimaryFrame()->GetSize().height / 2) {
+ return eSpinButtonUp;
+ }
+ return eSpinButtonDown;
+ }
+ }
+ return eSpinButtonNone;
+}
+
+void nsNumberControlFrame::SpinnerStateChanged() const {
+ if (mSpinUp) {
+ nsIFrame* spinUpFrame = mSpinUp->GetPrimaryFrame();
+ if (spinUpFrame && spinUpFrame->IsThemed()) {
+ spinUpFrame->InvalidateFrame();
+ }
+ }
+ if (mSpinDown) {
+ nsIFrame* spinDownFrame = mSpinDown->GetPrimaryFrame();
+ if (spinDownFrame && spinDownFrame->IsThemed()) {
+ spinDownFrame->InvalidateFrame();
+ }
+ }
+}
+
+bool nsNumberControlFrame::SpinnerUpButtonIsDepressed() const {
+ return HTMLInputElement::FromNode(mContent)
+ ->NumberSpinnerUpButtonIsDepressed();
+}
+
+bool nsNumberControlFrame::SpinnerDownButtonIsDepressed() const {
+ return HTMLInputElement::FromNode(mContent)
+ ->NumberSpinnerDownButtonIsDepressed();
+}
+
+void nsNumberControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ nsTextControlFrame::AppendAnonymousContentTo(aElements, aFilter);
+ if (mSpinBox) {
+ aElements.AppendElement(mSpinBox);
+ }
+}
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsNumberControlFrame::AccessibleType() {
+ return a11y::eHTMLSpinnerType;
+}
+#endif
diff --git a/layout/forms/nsNumberControlFrame.h b/layout/forms/nsNumberControlFrame.h
new file mode 100644
index 0000000000..d7201ea1e1
--- /dev/null
+++ b/layout/forms/nsNumberControlFrame.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsNumberControlFrame_h__
+#define nsNumberControlFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsTextControlFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+
+class nsITextControlFrame;
+class nsPresContext;
+
+namespace mozilla {
+enum class PseudoStyleType : uint8_t;
+class PresShell;
+class WidgetEvent;
+class WidgetGUIEvent;
+namespace dom {
+class HTMLInputElement;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * This frame type is used for <input type=number>.
+ *
+ * TODO(emilio): Maybe merge with nsTextControlFrame?
+ */
+class nsNumberControlFrame final : public nsTextControlFrame {
+ friend nsIFrame* NS_NewNumberControlFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ typedef mozilla::PseudoStyleType PseudoStyleType;
+ typedef mozilla::dom::Element Element;
+ typedef mozilla::dom::HTMLInputElement HTMLInputElement;
+ typedef mozilla::WidgetEvent WidgetEvent;
+ typedef mozilla::WidgetGUIEvent WidgetGUIEvent;
+
+ explicit nsNumberControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsNumberControlFrame)
+
+ void Destroy(DestroyContext&) override;
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() override;
+#endif
+
+ // nsIAnonymousContentCreator
+ nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
+ void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"NumberControl"_ns, aResult);
+ }
+#endif
+
+ /**
+ * If the frame is the frame for an nsNumberControlFrame's up or down spin
+ * button, returns the nsNumberControlFrame. Else returns nullptr.
+ */
+ static nsNumberControlFrame* GetNumberControlFrameForSpinButton(
+ nsIFrame* aFrame);
+
+ enum SpinButtonEnum { eSpinButtonNone, eSpinButtonUp, eSpinButtonDown };
+
+ /**
+ * Returns one of the SpinButtonEnum values to depending on whether the
+ * pointer event is over the spin-up button, the spin-down button, or
+ * neither.
+ */
+ int32_t GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const;
+
+ void SpinnerStateChanged() const;
+
+ bool SpinnerUpButtonIsDepressed() const;
+ bool SpinnerDownButtonIsDepressed() const;
+
+ /**
+ * Our element had HTMLInputElement::Select() called on it.
+ */
+ void HandleSelectCall();
+
+ bool ShouldUseNativeStyleForSpinner() const;
+
+ private:
+ // See nsNumberControlFrame::CreateAnonymousContent for a description of
+ // these.
+ nsCOMPtr<Element> mSpinBox;
+ nsCOMPtr<Element> mSpinUp;
+ nsCOMPtr<Element> mSpinDown;
+};
+
+#endif // nsNumberControlFrame_h__
diff --git a/layout/forms/nsProgressFrame.cpp b/layout/forms/nsProgressFrame.cpp
new file mode 100644
index 0000000000..2f0d727473
--- /dev/null
+++ b/layout/forms/nsProgressFrame.cpp
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsProgressFrame.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLProgressElement.h"
+#include "nsIContent.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsNodeInfoManager.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsFontMetrics.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame* NS_NewProgressFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsProgressFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsProgressFrame)
+
+nsProgressFrame::nsProgressFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID), mBarDiv(nullptr) {}
+
+nsProgressFrame::~nsProgressFrame() = default;
+
+void nsProgressFrame::Destroy(DestroyContext& aContext) {
+ NS_ASSERTION(!GetPrevContinuation(),
+ "nsProgressFrame should not have continuations; if it does we "
+ "need to call RegUnregAccessKey only for the first.");
+ aContext.AddAnonymousContent(mBarDiv.forget());
+ nsContainerFrame::Destroy(aContext);
+}
+
+nsresult nsProgressFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ // Create the progress bar div.
+ nsCOMPtr<Document> doc = mContent->GetComposedDoc();
+ mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div);
+
+ // Associate ::-moz-progress-bar pseudo-element to the anonymous child.
+ if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) {
+ // TODO(emilio): Create also a slider-track pseudo-element.
+ mBarDiv->SetPseudoElementType(PseudoStyleType::sliderFill);
+ } else {
+ mBarDiv->SetPseudoElementType(PseudoStyleType::mozProgressBar);
+ }
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aElements.AppendElement(mBarDiv);
+
+ return NS_OK;
+}
+
+void nsProgressFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) {
+ if (mBarDiv) {
+ aElements.AppendElement(mBarDiv);
+ }
+}
+
+NS_QUERYFRAME_HEAD(nsProgressFrame)
+ NS_QUERYFRAME_ENTRY(nsProgressFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+void nsProgressFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ BuildDisplayListForInline(aBuilder, aLists);
+}
+
+void nsProgressFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsProgressFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ NS_ASSERTION(mBarDiv, "Progress bar div must exist!");
+ NS_ASSERTION(
+ PrincipalChildList().GetLength() == 1 &&
+ PrincipalChildList().FirstChild() == mBarDiv->GetPrimaryFrame(),
+ "unexpected child frames");
+ NS_ASSERTION(!GetPrevContinuation(),
+ "nsProgressFrame should not have continuations; if it does we "
+ "need to call RegUnregAccessKey only for the first.");
+
+ const auto wm = aReflowInput.GetWritingMode();
+ aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm));
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+ for (auto childFrame : PrincipalChildList()) {
+ ReflowChildFrame(childFrame, aPresContext, aReflowInput, aStatus);
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, childFrame);
+ }
+
+ FinishAndStoreOverflow(&aDesiredSize);
+
+ aStatus.Reset(); // This type of frame can't be split.
+}
+
+void nsProgressFrame::ReflowChildFrame(nsIFrame* aChild,
+ nsPresContext* aPresContext,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ bool vertical = ResolvedOrientationIsVertical();
+ WritingMode wm = aChild->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+ ReflowInput reflowInput(aPresContext, aReflowInput, aChild, availSize);
+ nscoord size =
+ vertical ? aReflowInput.ComputedHeight() : aReflowInput.ComputedWidth();
+ nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left;
+ nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top;
+
+ double position = static_cast<HTMLProgressElement*>(GetContent())->Position();
+
+ // Force the bar's size to match the current progress.
+ // When indeterminate, the progress' size will be 100%.
+ if (position >= 0.0) {
+ size *= position;
+ }
+
+ if (!vertical && wm.IsPhysicalRTL()) {
+ xoffset += aReflowInput.ComputedWidth() - size;
+ }
+
+ // The bar size is fixed in these cases:
+ // - the progress position is determined: the bar size is fixed according
+ // to it's value.
+ // - the progress position is indeterminate and the bar appearance should be
+ // shown as native: the bar size is forced to 100%.
+ // Otherwise (when the progress is indeterminate and the bar appearance isn't
+ // native), the bar size isn't fixed and can be set by the author.
+ if (position != -1 || ShouldUseNativeStyle()) {
+ if (vertical) {
+ // We want the bar to begin at the bottom.
+ yoffset += aReflowInput.ComputedHeight() - size;
+
+ size -= reflowInput.ComputedPhysicalMargin().TopBottom() +
+ reflowInput.ComputedPhysicalBorderPadding().TopBottom();
+ size = std::max(size, 0);
+ reflowInput.SetComputedHeight(size);
+ } else {
+ size -= reflowInput.ComputedPhysicalMargin().LeftRight() +
+ reflowInput.ComputedPhysicalBorderPadding().LeftRight();
+ size = std::max(size, 0);
+ reflowInput.SetComputedWidth(size);
+ }
+ } else if (vertical) {
+ // For vertical progress bars, we need to position the bar specificly when
+ // the width isn't constrained (position == -1 and !ShouldUseNativeStyle())
+ // because aReflowInput.ComputedHeight() - size == 0.
+ yoffset += aReflowInput.ComputedHeight() - reflowInput.ComputedHeight();
+ }
+
+ xoffset += reflowInput.ComputedPhysicalMargin().left;
+ yoffset += reflowInput.ComputedPhysicalMargin().top;
+
+ ReflowOutput barDesiredSize(aReflowInput);
+ ReflowChild(aChild, aPresContext, barDesiredSize, reflowInput, xoffset,
+ yoffset, ReflowChildFlags::Default, aStatus);
+ FinishReflowChild(aChild, aPresContext, barDesiredSize, &reflowInput, xoffset,
+ yoffset, ReflowChildFlags::Default);
+}
+
+nsresult nsProgressFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ NS_ASSERTION(mBarDiv, "Progress bar div must exist!");
+
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max)) {
+ auto presShell = PresShell();
+ for (auto childFrame : PrincipalChildList()) {
+ presShell->FrameNeedsReflow(childFrame, IntrinsicDirty::None,
+ NS_FRAME_IS_DIRTY);
+ }
+ InvalidateFrame();
+ }
+
+ return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+}
+
+LogicalSize nsProgressFrame::ComputeAutoSize(
+ gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize,
+ nscoord aAvailableISize, const LogicalSize& aMargin,
+ const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
+ ComputeSizeFlags aFlags) {
+ const WritingMode wm = GetWritingMode();
+ LogicalSize autoSize(wm);
+ autoSize.BSize(wm) = autoSize.ISize(wm) =
+ StyleFont()
+ ->mFont.size.ScaledBy(nsLayoutUtils::FontSizeInflationFor(this))
+ .ToAppUnits(); // 1em
+
+ if (ResolvedOrientationIsVertical() == wm.IsVertical()) {
+ autoSize.ISize(wm) *= 10; // 10em
+ } else {
+ autoSize.BSize(wm) *= 10; // 10em
+ }
+
+ return autoSize.ConvertTo(aWM, wm);
+}
+
+nscoord nsProgressFrame::GetMinISize(gfxContext* aRenderingContext) {
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+
+ nscoord minISize = fontMet->Font().size.ToAppUnits(); // 1em
+
+ if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) {
+ // The orientation is inline
+ minISize *= 10; // 10em
+ }
+
+ return minISize;
+}
+
+nscoord nsProgressFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ return GetMinISize(aRenderingContext);
+}
+
+bool nsProgressFrame::ShouldUseNativeStyle() const {
+ nsIFrame* barFrame = PrincipalChildList().FirstChild();
+
+ // Use the native style if these conditions are satisfied:
+ // - both frames use the native appearance;
+ // - neither frame has author specified rules setting the border or the
+ // background.
+ return StyleDisplay()->EffectiveAppearance() ==
+ StyleAppearance::ProgressBar &&
+ !Style()->HasAuthorSpecifiedBorderOrBackground() && barFrame &&
+ barFrame->StyleDisplay()->EffectiveAppearance() ==
+ StyleAppearance::Progresschunk &&
+ !barFrame->Style()->HasAuthorSpecifiedBorderOrBackground();
+}
diff --git a/layout/forms/nsProgressFrame.h b/layout/forms/nsProgressFrame.h
new file mode 100644
index 0000000000..7d3618ee96
--- /dev/null
+++ b/layout/forms/nsProgressFrame.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsProgressFrame_h___
+#define nsProgressFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+enum class PseudoStyleType : uint8_t;
+} // namespace mozilla
+
+class nsProgressFrame final : public nsContainerFrame,
+ public nsIAnonymousContentCreator {
+ typedef mozilla::PseudoStyleType PseudoStyleType;
+ typedef mozilla::dom::Element Element;
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsProgressFrame)
+
+ explicit nsProgressFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ virtual ~nsProgressFrame();
+
+ void Destroy(DestroyContext&) override;
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"Progress"_ns, aResult);
+ }
+#endif
+
+ // nsIAnonymousContentCreator
+ virtual nsresult CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) override;
+ virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual mozilla::LogicalSize ComputeAutoSize(
+ gfxContext* aRenderingContext, mozilla::WritingMode aWM,
+ const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize,
+ const mozilla::LogicalSize& aMargin,
+ const mozilla::LogicalSize& aBorderPadding,
+ const mozilla::StyleSizeOverrides& aSizeOverrides,
+ mozilla::ComputeSizeFlags aFlags) override;
+
+ virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ /**
+ * Returns whether the frame and its child should use the native style.
+ */
+ bool ShouldUseNativeStyle() const;
+
+ protected:
+ // Helper function to reflow a child frame.
+ void ReflowChildFrame(nsIFrame* aChild, nsPresContext* aPresContext,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus);
+
+ /**
+ * The div used to show the progress bar.
+ * @see nsProgressFrame::CreateAnonymousContent
+ */
+ nsCOMPtr<Element> mBarDiv;
+};
+
+#endif
diff --git a/layout/forms/nsRangeFrame.cpp b/layout/forms/nsRangeFrame.cpp
new file mode 100644
index 0000000000..2cccff715a
--- /dev/null
+++ b/layout/forms/nsRangeFrame.cpp
@@ -0,0 +1,780 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsRangeFrame.h"
+
+#include "ListMutationObserver.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TouchEvents.h"
+
+#include "gfxContext.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsCSSRendering.h"
+#include "nsDisplayList.h"
+#include "nsIContent.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/dom/Document.h"
+#include "nsGkAtoms.h"
+#include "mozilla/dom/HTMLDataListElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "nsPresContext.h"
+#include "nsNodeInfoManager.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/ServoStyleSet.h"
+#include "nsTArray.h"
+
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+#endif
+
+// Our intrinsic size is 12em in the main-axis and 1.3em in the cross-axis.
+#define MAIN_AXIS_EM_SIZE 12
+#define CROSS_AXIS_EM_SIZE 1.3f
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::image;
+
+nsIFrame* NS_NewRangeFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsRangeFrame(aStyle, aPresShell->GetPresContext());
+}
+
+nsRangeFrame::nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID) {}
+
+void nsRangeFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
+ if (InputElement().HasAttr(nsGkAtoms::list_)) {
+ mListMutationObserver = new ListMutationObserver(*this);
+ }
+}
+
+nsRangeFrame::~nsRangeFrame() = default;
+
+NS_IMPL_FRAMEARENA_HELPERS(nsRangeFrame)
+
+NS_QUERYFRAME_HEAD(nsRangeFrame)
+ NS_QUERYFRAME_ENTRY(nsRangeFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+void nsRangeFrame::Destroy(DestroyContext& aContext) {
+ NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(),
+ "nsRangeFrame should not have continuations; if it does we "
+ "need to call RegUnregAccessKey only for the first.");
+
+ if (mListMutationObserver) {
+ mListMutationObserver->Detach();
+ }
+ aContext.AddAnonymousContent(mTrackDiv.forget());
+ aContext.AddAnonymousContent(mProgressDiv.forget());
+ aContext.AddAnonymousContent(mThumbDiv.forget());
+ nsContainerFrame::Destroy(aContext);
+}
+
+static already_AddRefed<Element> MakeAnonymousDiv(
+ Document& aDoc, PseudoStyleType aOldPseudoType,
+ PseudoStyleType aModernPseudoType,
+ nsTArray<nsIAnonymousContentCreator::ContentInfo>& aElements) {
+ RefPtr<Element> result = aDoc.CreateHTMLElement(nsGkAtoms::div);
+
+ // Associate the pseudo-element with the anonymous child.
+ if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) {
+ result->SetPseudoElementType(aModernPseudoType);
+ } else {
+ result->SetPseudoElementType(aOldPseudoType);
+ }
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aElements.AppendElement(result);
+
+ return result.forget();
+}
+
+nsresult nsRangeFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ Document* doc = mContent->OwnerDoc();
+ // Create the ::-moz-range-track pseudo-element (a div):
+ mTrackDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeTrack,
+ PseudoStyleType::sliderTrack, aElements);
+ // Create the ::-moz-range-progress pseudo-element (a div):
+ mProgressDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeProgress,
+ PseudoStyleType::sliderFill, aElements);
+ // Create the ::-moz-range-thumb pseudo-element (a div):
+ mThumbDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeThumb,
+ PseudoStyleType::sliderThumb, aElements);
+ return NS_OK;
+}
+
+void nsRangeFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) {
+ if (mTrackDiv) {
+ aElements.AppendElement(mTrackDiv);
+ }
+
+ if (mProgressDiv) {
+ aElements.AppendElement(mProgressDiv);
+ }
+
+ if (mThumbDiv) {
+ aElements.AppendElement(mThumbDiv);
+ }
+}
+
+void nsRangeFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ const nsStyleDisplay* disp = StyleDisplay();
+ if (IsThemed(disp)) {
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+ // Only create items for the thumb. Specifically, we do not want the track
+ // to paint, since *our* background is used to paint the track, and we don't
+ // want the unthemed track painting over the top of the themed track.
+ // This logic is copied from
+ // nsContainerFrame::BuildDisplayListForNonBlockChildren as
+ // called by BuildDisplayListForInline.
+ if (nsIFrame* thumb = mThumbDiv->GetPrimaryFrame()) {
+ nsDisplayListSet set(aLists, aLists.Content());
+ BuildDisplayListForChild(aBuilder, thumb, set, DisplayChildFlag::Inline);
+ }
+ } else {
+ BuildDisplayListForInline(aBuilder, aLists);
+ }
+}
+
+void nsRangeFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsRangeFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ NS_ASSERTION(mTrackDiv, "::-moz-range-track div must exist!");
+ NS_ASSERTION(mProgressDiv, "::-moz-range-progress div must exist!");
+ NS_ASSERTION(mThumbDiv, "::-moz-range-thumb div must exist!");
+ NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(),
+ "nsRangeFrame should not have continuations; if it does we "
+ "need to call RegUnregAccessKey only for the first.");
+
+ WritingMode wm = aReflowInput.GetWritingMode();
+ nscoord computedBSize = aReflowInput.ComputedBSize();
+ if (computedBSize == NS_UNCONSTRAINEDSIZE) {
+ computedBSize = 0;
+ }
+ const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm);
+ LogicalSize finalSize(
+ wm, aReflowInput.ComputedISize() + borderPadding.IStartEnd(wm),
+ computedBSize + borderPadding.BStartEnd(wm));
+ aDesiredSize.SetSize(wm, finalSize);
+
+ ReflowAnonymousContent(aPresContext, aDesiredSize, aReflowInput);
+
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+ nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame();
+ if (trackFrame) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, trackFrame);
+ }
+
+ nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame();
+ if (rangeProgressFrame) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, rangeProgressFrame);
+ }
+
+ nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame();
+ if (thumbFrame) {
+ ConsiderChildOverflow(aDesiredSize.mOverflowAreas, thumbFrame);
+ }
+
+ FinishAndStoreOverflow(&aDesiredSize);
+
+ MOZ_ASSERT(aStatus.IsEmpty(), "This type of frame can't be split.");
+}
+
+void nsRangeFrame::ReflowAnonymousContent(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput) {
+ // The width/height of our content box, which is the available width/height
+ // for our anonymous content:
+ nscoord rangeFrameContentBoxWidth = aReflowInput.ComputedWidth();
+ nscoord rangeFrameContentBoxHeight = aReflowInput.ComputedHeight();
+ if (rangeFrameContentBoxHeight == NS_UNCONSTRAINEDSIZE) {
+ rangeFrameContentBoxHeight = 0;
+ }
+
+ nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame();
+
+ if (trackFrame) { // display:none?
+
+ // Position the track:
+ // The idea here is that we allow content authors to style the width,
+ // height, border and padding of the track, but we ignore margin and
+ // positioning properties and do the positioning ourself to keep the center
+ // of the track's border box on the center of the nsRangeFrame's content
+ // box.
+
+ WritingMode wm = trackFrame->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+ ReflowInput trackReflowInput(aPresContext, aReflowInput, trackFrame,
+ availSize);
+
+ // Find the x/y position of the track frame such that it will be positioned
+ // as described above. These coordinates are with respect to the
+ // nsRangeFrame's border-box.
+ nscoord trackX = rangeFrameContentBoxWidth / 2;
+ nscoord trackY = rangeFrameContentBoxHeight / 2;
+
+ // Account for the track's border and padding (we ignore its margin):
+ trackX -= trackReflowInput.ComputedPhysicalBorderPadding().left +
+ trackReflowInput.ComputedWidth() / 2;
+ trackY -= trackReflowInput.ComputedPhysicalBorderPadding().top +
+ trackReflowInput.ComputedHeight() / 2;
+
+ // Make relative to our border box instead of our content box:
+ trackX += aReflowInput.ComputedPhysicalBorderPadding().left;
+ trackY += aReflowInput.ComputedPhysicalBorderPadding().top;
+
+ nsReflowStatus frameStatus;
+ ReflowOutput trackDesiredSize(aReflowInput);
+ ReflowChild(trackFrame, aPresContext, trackDesiredSize, trackReflowInput,
+ trackX, trackY, ReflowChildFlags::Default, frameStatus);
+ MOZ_ASSERT(
+ frameStatus.IsFullyComplete(),
+ "We gave our child unconstrained height, so it should be complete");
+ FinishReflowChild(trackFrame, aPresContext, trackDesiredSize,
+ &trackReflowInput, trackX, trackY,
+ ReflowChildFlags::Default);
+ }
+
+ nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame();
+
+ if (thumbFrame) { // display:none?
+ WritingMode wm = thumbFrame->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+ ReflowInput thumbReflowInput(aPresContext, aReflowInput, thumbFrame,
+ availSize);
+
+ // Where we position the thumb depends on its size, so we first reflow
+ // the thumb at {0,0} to obtain its size, then position it afterwards.
+
+ nsReflowStatus frameStatus;
+ ReflowOutput thumbDesiredSize(aReflowInput);
+ ReflowChild(thumbFrame, aPresContext, thumbDesiredSize, thumbReflowInput, 0,
+ 0, ReflowChildFlags::Default, frameStatus);
+ MOZ_ASSERT(
+ frameStatus.IsFullyComplete(),
+ "We gave our child unconstrained height, so it should be complete");
+ FinishReflowChild(thumbFrame, aPresContext, thumbDesiredSize,
+ &thumbReflowInput, 0, 0, ReflowChildFlags::Default);
+ DoUpdateThumbPosition(thumbFrame,
+ nsSize(aDesiredSize.Width(), aDesiredSize.Height()));
+ }
+
+ nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame();
+
+ if (rangeProgressFrame) { // display:none?
+ WritingMode wm = rangeProgressFrame->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSize(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+ ReflowInput progressReflowInput(aPresContext, aReflowInput,
+ rangeProgressFrame, availSize);
+
+ // We first reflow the range-progress frame at {0,0} to obtain its
+ // unadjusted dimensions, then we adjust it to so that the appropriate edge
+ // ends at the thumb.
+
+ nsReflowStatus frameStatus;
+ ReflowOutput progressDesiredSize(aReflowInput);
+ ReflowChild(rangeProgressFrame, aPresContext, progressDesiredSize,
+ progressReflowInput, 0, 0, ReflowChildFlags::Default,
+ frameStatus);
+ MOZ_ASSERT(
+ frameStatus.IsFullyComplete(),
+ "We gave our child unconstrained height, so it should be complete");
+ FinishReflowChild(rangeProgressFrame, aPresContext, progressDesiredSize,
+ &progressReflowInput, 0, 0, ReflowChildFlags::Default);
+ DoUpdateRangeProgressFrame(
+ rangeProgressFrame,
+ nsSize(aDesiredSize.Width(), aDesiredSize.Height()));
+ }
+}
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsRangeFrame::AccessibleType() { return a11y::eHTMLRangeType; }
+#endif
+
+double nsRangeFrame::GetValueAsFractionOfRange() {
+ const auto& input = InputElement();
+ if (MOZ_UNLIKELY(!input.IsDoneCreating())) {
+ // Our element isn't done being created, so its values haven't yet been
+ // sanitized! (It's rare that we'd be reflowed when our element is in this
+ // state, but it can happen if the parser decides to yield while processing
+ // its tasks to build the element.) We can't trust that any of our numeric
+ // values will make sense until they've been sanitized; so for now, just
+ // use 0.0 as a fallback fraction-of-range value here (i.e. behave as if
+ // we're at our minimum, which is how the spec handles some edge cases).
+ return 0.0;
+ }
+ return GetDoubleAsFractionOfRange(input.GetValueAsDecimal());
+}
+
+double nsRangeFrame::GetDoubleAsFractionOfRange(const Decimal& aValue) {
+ auto& input = InputElement();
+
+ Decimal minimum = input.GetMinimum();
+ Decimal maximum = input.GetMaximum();
+
+ MOZ_ASSERT(aValue.isFinite() && minimum.isFinite() && maximum.isFinite(),
+ "type=range should have a default maximum/minimum");
+
+ if (maximum <= minimum) {
+ // Avoid rounding triggering the assert by checking against an epsilon.
+ MOZ_ASSERT((aValue - minimum).abs().toDouble() <
+ std::numeric_limits<float>::epsilon(),
+ "Unsanitized value");
+ return 0.0;
+ }
+
+ MOZ_ASSERT(aValue >= minimum && aValue <= maximum, "Unsanitized value");
+
+ return ((aValue - minimum) / (maximum - minimum)).toDouble();
+}
+
+Decimal nsRangeFrame::GetValueAtEventPoint(WidgetGUIEvent* aEvent) {
+ MOZ_ASSERT(
+ aEvent->mClass == eMouseEventClass || aEvent->mClass == eTouchEventClass,
+ "Unexpected event type - aEvent->mRefPoint may be meaningless");
+
+ MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast");
+ dom::HTMLInputElement* input =
+ static_cast<dom::HTMLInputElement*>(GetContent());
+
+ MOZ_ASSERT(input->ControlType() == FormControlType::InputRange);
+
+ Decimal minimum = input->GetMinimum();
+ Decimal maximum = input->GetMaximum();
+ MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(),
+ "type=range should have a default maximum/minimum");
+ if (maximum <= minimum) {
+ return minimum;
+ }
+ Decimal range = maximum - minimum;
+
+ LayoutDeviceIntPoint absPoint;
+ if (aEvent->mClass == eTouchEventClass) {
+ MOZ_ASSERT(aEvent->AsTouchEvent()->mTouches.Length() == 1,
+ "Unexpected number of mTouches");
+ absPoint = aEvent->AsTouchEvent()->mTouches[0]->mRefPoint;
+ } else {
+ absPoint = aEvent->mRefPoint;
+ }
+ nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, absPoint, RelativeTo{this});
+
+ if (point == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) {
+ // We don't want to change the current value for this error state.
+ return static_cast<dom::HTMLInputElement*>(GetContent())
+ ->GetValueAsDecimal();
+ }
+
+ nsRect rangeContentRect = GetContentRectRelativeToSelf();
+ nsSize thumbSize;
+
+ if (IsThemed()) {
+ // We need to get the size of the thumb from the theme.
+ nsPresContext* pc = PresContext();
+ LayoutDeviceIntSize size = pc->Theme()->GetMinimumWidgetSize(
+ pc, this, StyleAppearance::RangeThumb);
+ thumbSize =
+ LayoutDeviceIntSize::ToAppUnits(size, pc->AppUnitsPerDevPixel());
+ // For GTK, GetMinimumWidgetSize returns zero for the thumb dimension
+ // perpendicular to the orientation of the slider. That's okay since we
+ // only care about the dimension in the direction of the slider when using
+ // |thumbSize| below, but it means this assertion need to check
+ // IsHorizontal().
+ MOZ_ASSERT((IsHorizontal() && thumbSize.width > 0) ||
+ (!IsHorizontal() && thumbSize.height > 0),
+ "The thumb is expected to take up some slider space");
+ } else {
+ nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame();
+ if (thumbFrame) { // diplay:none?
+ thumbSize = thumbFrame->GetSize();
+ }
+ }
+
+ Decimal fraction;
+ if (IsHorizontal()) {
+ nscoord traversableDistance = rangeContentRect.width - thumbSize.width;
+ if (traversableDistance <= 0) {
+ return minimum;
+ }
+ nscoord posAtStart = rangeContentRect.x + thumbSize.width / 2;
+ nscoord posAtEnd = posAtStart + traversableDistance;
+ nscoord posOfPoint = mozilla::clamped(point.x, posAtStart, posAtEnd);
+ fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance);
+ if (IsRightToLeft()) {
+ fraction = Decimal(1) - fraction;
+ }
+ } else {
+ nscoord traversableDistance = rangeContentRect.height - thumbSize.height;
+ if (traversableDistance <= 0) {
+ return minimum;
+ }
+ nscoord posAtStart = rangeContentRect.y + thumbSize.height / 2;
+ nscoord posAtEnd = posAtStart + traversableDistance;
+ nscoord posOfPoint = mozilla::clamped(point.y, posAtStart, posAtEnd);
+ // For a vertical range, the top (posAtStart) is the highest value, so we
+ // subtract the fraction from 1.0 to get that polarity correct.
+ fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance);
+ if (IsUpwards()) {
+ fraction = Decimal(1) - fraction;
+ }
+ }
+
+ MOZ_ASSERT(fraction >= Decimal(0) && fraction <= Decimal(1));
+ return minimum + fraction * range;
+}
+
+void nsRangeFrame::UpdateForValueChange() {
+ if (IsSubtreeDirty()) {
+ return; // we're going to be updated when we reflow
+ }
+ nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame();
+ nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame();
+ if (!rangeProgressFrame && !thumbFrame) {
+ return; // diplay:none?
+ }
+ if (rangeProgressFrame) {
+ DoUpdateRangeProgressFrame(rangeProgressFrame, GetSize());
+ }
+ if (thumbFrame) {
+ DoUpdateThumbPosition(thumbFrame, GetSize());
+ }
+ if (IsThemed()) {
+ // We don't know the exact dimensions or location of the thumb when native
+ // theming is applied, so we just repaint the entire range.
+ InvalidateFrame();
+ }
+
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* accService = GetAccService()) {
+ accService->RangeValueChanged(PresShell(), mContent);
+ }
+#endif
+
+ SchedulePaint();
+}
+
+nsTArray<Decimal> nsRangeFrame::TickMarks() {
+ nsTArray<Decimal> tickMarks;
+ auto& input = InputElement();
+ auto* list = input.GetList();
+ if (!list) {
+ return tickMarks;
+ }
+ auto min = input.GetMinimum();
+ auto max = input.GetMaximum();
+ auto* options = list->Options();
+ nsAutoString label;
+ for (uint32_t i = 0; i < options->Length(); ++i) {
+ auto* item = options->Item(i);
+ auto* option = HTMLOptionElement::FromNode(item);
+ MOZ_ASSERT(option);
+ if (option->Disabled()) {
+ continue;
+ }
+ nsAutoString str;
+ option->GetValue(str);
+ auto tickMark = HTMLInputElement::StringToDecimal(str);
+ if (tickMark.isNaN() || tickMark < min || tickMark > max ||
+ input.ValueIsStepMismatch(tickMark)) {
+ continue;
+ }
+ tickMarks.AppendElement(tickMark);
+ }
+ tickMarks.Sort();
+ return tickMarks;
+}
+
+Decimal nsRangeFrame::NearestTickMark(const Decimal& aValue) {
+ auto tickMarks = TickMarks();
+ if (tickMarks.IsEmpty() || aValue.isNaN()) {
+ return Decimal::nan();
+ }
+ size_t index;
+ if (BinarySearch(tickMarks, 0, tickMarks.Length(), aValue, &index)) {
+ return tickMarks[index];
+ }
+ if (index == tickMarks.Length()) {
+ return tickMarks.LastElement();
+ }
+ if (index == 0) {
+ return tickMarks[0];
+ }
+ const auto& smallerTickMark = tickMarks[index - 1];
+ const auto& largerTickMark = tickMarks[index];
+ MOZ_ASSERT(smallerTickMark < aValue);
+ MOZ_ASSERT(largerTickMark > aValue);
+ return (aValue - smallerTickMark).abs() < (aValue - largerTickMark).abs()
+ ? smallerTickMark
+ : largerTickMark;
+}
+
+mozilla::dom::HTMLInputElement& nsRangeFrame::InputElement() const {
+ MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast");
+ auto& input = *static_cast<dom::HTMLInputElement*>(GetContent());
+ MOZ_ASSERT(input.ControlType() == FormControlType::InputRange);
+ return input;
+}
+
+void nsRangeFrame::DoUpdateThumbPosition(nsIFrame* aThumbFrame,
+ const nsSize& aRangeSize) {
+ MOZ_ASSERT(aThumbFrame);
+
+ // The idea here is that we want to position the thumb so that the center
+ // of the thumb is on an imaginary line drawn from the middle of one edge
+ // of the range frame's content box to the middle of the opposite edge of
+ // its content box (the opposite edges being the left/right edge if the
+ // range is horizontal, or else the top/bottom edges if the range is
+ // vertical). How far along this line the center of the thumb is placed
+ // depends on the value of the range.
+
+ nsMargin borderAndPadding = GetUsedBorderAndPadding();
+ nsPoint newPosition(borderAndPadding.left, borderAndPadding.top);
+
+ nsSize rangeContentBoxSize(aRangeSize);
+ rangeContentBoxSize.width -= borderAndPadding.LeftRight();
+ rangeContentBoxSize.height -= borderAndPadding.TopBottom();
+
+ nsSize thumbSize = aThumbFrame->GetSize();
+ double fraction = GetValueAsFractionOfRange();
+ MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0);
+
+ if (IsHorizontal()) {
+ if (thumbSize.width < rangeContentBoxSize.width) {
+ nscoord traversableDistance = rangeContentBoxSize.width - thumbSize.width;
+ if (IsRightToLeft()) {
+ newPosition.x += NSToCoordRound((1.0 - fraction) * traversableDistance);
+ } else {
+ newPosition.x += NSToCoordRound(fraction * traversableDistance);
+ }
+ newPosition.y += (rangeContentBoxSize.height - thumbSize.height) / 2;
+ }
+ } else {
+ if (thumbSize.height < rangeContentBoxSize.height) {
+ nscoord traversableDistance =
+ rangeContentBoxSize.height - thumbSize.height;
+ newPosition.x += (rangeContentBoxSize.width - thumbSize.width) / 2;
+ if (IsUpwards()) {
+ newPosition.y += NSToCoordRound((1.0 - fraction) * traversableDistance);
+ } else {
+ newPosition.y += NSToCoordRound(fraction * traversableDistance);
+ }
+ }
+ }
+ aThumbFrame->SetPosition(newPosition);
+}
+
+void nsRangeFrame::DoUpdateRangeProgressFrame(nsIFrame* aRangeProgressFrame,
+ const nsSize& aRangeSize) {
+ MOZ_ASSERT(aRangeProgressFrame);
+
+ // The idea here is that we want to position the ::-moz-range-progress
+ // pseudo-element so that the center line running along its length is on the
+ // corresponding center line of the nsRangeFrame's content box. In the other
+ // dimension, we align the "start" edge of the ::-moz-range-progress
+ // pseudo-element's border-box with the corresponding edge of the
+ // nsRangeFrame's content box, and we size the progress element's border-box
+ // to have a length of GetValueAsFractionOfRange() times the nsRangeFrame's
+ // content-box size.
+
+ nsMargin borderAndPadding = GetUsedBorderAndPadding();
+ nsSize progSize = aRangeProgressFrame->GetSize();
+ nsRect progRect(borderAndPadding.left, borderAndPadding.top, progSize.width,
+ progSize.height);
+
+ nsSize rangeContentBoxSize(aRangeSize);
+ rangeContentBoxSize.width -= borderAndPadding.LeftRight();
+ rangeContentBoxSize.height -= borderAndPadding.TopBottom();
+
+ double fraction = GetValueAsFractionOfRange();
+ MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0);
+
+ if (IsHorizontal()) {
+ nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.width);
+ if (IsRightToLeft()) {
+ progRect.x += rangeContentBoxSize.width - progLength;
+ }
+ progRect.y += (rangeContentBoxSize.height - progSize.height) / 2;
+ progRect.width = progLength;
+ } else {
+ nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.height);
+ progRect.x += (rangeContentBoxSize.width - progSize.width) / 2;
+ if (IsUpwards()) {
+ progRect.y += rangeContentBoxSize.height - progLength;
+ }
+ progRect.height = progLength;
+ }
+ aRangeProgressFrame->SetRect(progRect);
+}
+
+nsresult nsRangeFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {
+ NS_ASSERTION(mTrackDiv, "The track div must exist!");
+ NS_ASSERTION(mThumbDiv, "The thumb div must exist!");
+
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::min ||
+ aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step) {
+ // We want to update the position of the thumb, except in one special
+ // case: If the value attribute is being set, it is possible that we are
+ // in the middle of a type change away from type=range, under the
+ // SetAttr(..., nsGkAtoms::value, ...) call in HTMLInputElement::
+ // HandleTypeChange. In that case the HTMLInputElement's type will
+ // already have changed, and if we call UpdateForValueChange()
+ // we'll fail the asserts under that call that check the type of our
+ // HTMLInputElement. Given that we're changing away from being a range
+ // and this frame will shortly be destroyed, there's no point in calling
+ // UpdateForValueChange() anyway.
+ MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast");
+ bool typeIsRange =
+ static_cast<dom::HTMLInputElement*>(GetContent())->ControlType() ==
+ FormControlType::InputRange;
+ // If script changed the <input>'s type before setting these attributes
+ // then we don't need to do anything since we are going to be reframed.
+ if (typeIsRange) {
+ UpdateForValueChange();
+ }
+ } else if (aAttribute == nsGkAtoms::orient) {
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None,
+ NS_FRAME_IS_DIRTY);
+ } else if (aAttribute == nsGkAtoms::list_) {
+ const bool isRemoval = aModType == MutationEvent_Binding::REMOVAL;
+ if (mListMutationObserver) {
+ mListMutationObserver->Detach();
+ if (isRemoval) {
+ mListMutationObserver = nullptr;
+ } else {
+ mListMutationObserver->Attach();
+ }
+ } else if (!isRemoval) {
+ mListMutationObserver = new ListMutationObserver(*this, true);
+ }
+ }
+ }
+
+ return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+}
+
+nscoord nsRangeFrame::AutoCrossSize(Length aEm) {
+ nscoord minCrossSize(0);
+ if (IsThemed()) {
+ nsPresContext* pc = PresContext();
+ LayoutDeviceIntSize size = pc->Theme()->GetMinimumWidgetSize(
+ pc, this, StyleAppearance::RangeThumb);
+ minCrossSize =
+ pc->DevPixelsToAppUnits(IsHorizontal() ? size.height : size.width);
+ }
+ return std::max(minCrossSize, aEm.ScaledBy(CROSS_AXIS_EM_SIZE).ToAppUnits());
+}
+
+static mozilla::Length OneEm(nsRangeFrame* aFrame) {
+ return aFrame->StyleFont()->mFont.size.ScaledBy(
+ nsLayoutUtils::FontSizeInflationFor(aFrame));
+}
+
+LogicalSize nsRangeFrame::ComputeAutoSize(
+ gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize,
+ nscoord aAvailableISize, const LogicalSize& aMargin,
+ const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
+ ComputeSizeFlags aFlags) {
+ bool isInlineOriented = IsInlineOriented();
+ auto em = OneEm(this);
+
+ const WritingMode wm = GetWritingMode();
+ LogicalSize autoSize(wm);
+ if (isInlineOriented) {
+ autoSize.ISize(wm) = em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits();
+ autoSize.BSize(wm) = AutoCrossSize(em);
+ } else {
+ autoSize.ISize(wm) = AutoCrossSize(em);
+ autoSize.BSize(wm) = em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits();
+ }
+
+ return autoSize.ConvertTo(aWM, wm);
+}
+
+nscoord nsRangeFrame::GetMinISize(gfxContext* aRenderingContext) {
+ const auto* pos = StylePosition();
+ auto wm = GetWritingMode();
+ if (pos->ISize(wm).HasPercent()) {
+ // https://drafts.csswg.org/css-sizing-3/#percentage-sizing
+ // https://drafts.csswg.org/css-sizing-3/#min-content-zero
+ return nsLayoutUtils::ResolveToLength<true>(
+ pos->ISize(wm).AsLengthPercentage(), nscoord(0));
+ }
+ return GetPrefISize(aRenderingContext);
+}
+
+nscoord nsRangeFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ auto em = OneEm(this);
+ if (IsInlineOriented()) {
+ return em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits();
+ }
+ return AutoCrossSize(em);
+}
+
+bool nsRangeFrame::IsHorizontal() const {
+ dom::HTMLInputElement* element =
+ static_cast<dom::HTMLInputElement*>(GetContent());
+ return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient,
+ nsGkAtoms::horizontal, eCaseMatters) ||
+ (!element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient,
+ nsGkAtoms::vertical, eCaseMatters) &&
+ GetWritingMode().IsVertical() ==
+ element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient,
+ nsGkAtoms::block, eCaseMatters));
+}
+
+double nsRangeFrame::GetMin() const {
+ return static_cast<dom::HTMLInputElement*>(GetContent())
+ ->GetMinimum()
+ .toDouble();
+}
+
+double nsRangeFrame::GetMax() const {
+ return static_cast<dom::HTMLInputElement*>(GetContent())
+ ->GetMaximum()
+ .toDouble();
+}
+
+double nsRangeFrame::GetValue() const {
+ return static_cast<dom::HTMLInputElement*>(GetContent())
+ ->GetValueAsDecimal()
+ .toDouble();
+}
+
+bool nsRangeFrame::ShouldUseNativeStyle() const {
+ nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame();
+ nsIFrame* progressFrame = mProgressDiv->GetPrimaryFrame();
+ nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame();
+
+ return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Range &&
+ trackFrame &&
+ !trackFrame->Style()->HasAuthorSpecifiedBorderOrBackground() &&
+ progressFrame &&
+ !progressFrame->Style()->HasAuthorSpecifiedBorderOrBackground() &&
+ thumbFrame &&
+ !thumbFrame->Style()->HasAuthorSpecifiedBorderOrBackground();
+}
diff --git a/layout/forms/nsRangeFrame.h b/layout/forms/nsRangeFrame.h
new file mode 100644
index 0000000000..4d2073aa50
--- /dev/null
+++ b/layout/forms/nsRangeFrame.h
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsRangeFrame_h___
+#define nsRangeFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Decimal.h"
+#include "mozilla/EventForwards.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsIDOMEventListener.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+
+class nsDisplayRangeFocusRing;
+
+namespace mozilla {
+class ListMutationObserver;
+class PresShell;
+namespace dom {
+class Event;
+class HTMLInputElement;
+} // namespace dom
+} // namespace mozilla
+
+class nsRangeFrame final : public nsContainerFrame,
+ public nsIAnonymousContentCreator {
+ friend nsIFrame* NS_NewRangeFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ friend class nsDisplayRangeFocusRing;
+
+ explicit nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ virtual ~nsRangeFrame();
+
+ typedef mozilla::PseudoStyleType PseudoStyleType;
+ typedef mozilla::dom::Element Element;
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsRangeFrame)
+
+ // nsIFrame overrides
+ void Destroy(DestroyContext&) override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"Range"_ns, aResult);
+ }
+#endif
+
+#ifdef ACCESSIBILITY
+ virtual mozilla::a11y::AccType AccessibleType() override;
+#endif
+
+ // nsIAnonymousContentCreator
+ virtual nsresult CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) override;
+ virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ mozilla::LogicalSize ComputeAutoSize(
+ gfxContext* aRenderingContext, mozilla::WritingMode aWM,
+ const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize,
+ const mozilla::LogicalSize& aMargin,
+ const mozilla::LogicalSize& aBorderPadding,
+ const mozilla::StyleSizeOverrides& aSizeOverrides,
+ mozilla::ComputeSizeFlags aFlags) override;
+
+ virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ /**
+ * Returns true if the slider's thumb moves horizontally, or else false if it
+ * moves vertically.
+ */
+ bool IsHorizontal() const;
+
+ /**
+ * Returns true if the slider is oriented along the inline axis.
+ */
+ bool IsInlineOriented() const {
+ return IsHorizontal() != GetWritingMode().IsVertical();
+ }
+
+ /**
+ * Returns true if the slider's thumb moves right-to-left for increasing
+ * values; only relevant when IsHorizontal() is true.
+ */
+ bool IsRightToLeft() const {
+ MOZ_ASSERT(IsHorizontal());
+ return GetWritingMode().IsPhysicalRTL();
+ }
+
+ /**
+ * Returns true if the range progresses upwards (for vertical ranges in
+ * horizontal writing mode, or for bidi-RTL in vertical mode). Only
+ * relevant when IsHorizontal() is false.
+ */
+ bool IsUpwards() const {
+ MOZ_ASSERT(!IsHorizontal());
+ mozilla::WritingMode wm = GetWritingMode();
+ return wm.GetBlockDir() == mozilla::WritingMode::eBlockTB ||
+ wm.GetInlineDir() == mozilla::WritingMode::eInlineBTT;
+ }
+
+ double GetMin() const;
+ double GetMax() const;
+ double GetValue() const;
+
+ /**
+ * Returns the input element's value as a fraction of the difference between
+ * the input's minimum and its maximum (i.e. returns 0.0 when the value is
+ * the same as the minimum, and returns 1.0 when the value is the same as the
+ * maximum).
+ */
+ double GetValueAsFractionOfRange();
+
+ /**
+ * Returns the given value as a fraction of the difference between the input's
+ * minimum and its maximum (i.e. returns 0.0 when the value is the same as the
+ * input's minimum, and returns 1.0 when the value is the same as the input's
+ * maximum).
+ */
+ double GetDoubleAsFractionOfRange(const mozilla::Decimal& value);
+
+ /**
+ * Returns whether the frame and its child should use the native style.
+ */
+ bool ShouldUseNativeStyle() const;
+
+ mozilla::Decimal GetValueAtEventPoint(mozilla::WidgetGUIEvent* aEvent);
+
+ /**
+ * Helper that's used when the value of the range changes to reposition the
+ * thumb, resize the range-progress element, and schedule a repaint. (This
+ * does not reflow, since the position and size of the thumb and
+ * range-progress element do not affect the position or size of any other
+ * frames.)
+ */
+ void UpdateForValueChange();
+
+ nsTArray<mozilla::Decimal> TickMarks();
+
+ /**
+ * Returns the given value's offset from the range's nearest list tick mark
+ * or NaN if there are no tick marks.
+ */
+ mozilla::Decimal NearestTickMark(const mozilla::Decimal& aValue);
+
+ protected:
+ mozilla::dom::HTMLInputElement& InputElement() const;
+
+ private:
+ // Return our preferred size in the cross-axis (the axis perpendicular
+ // to the direction of movement of the thumb).
+ nscoord AutoCrossSize(mozilla::Length aEm);
+
+ // Helper function which reflows the anonymous div frames.
+ void ReflowAnonymousContent(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput);
+
+ void DoUpdateThumbPosition(nsIFrame* aThumbFrame, const nsSize& aRangeSize);
+
+ void DoUpdateRangeProgressFrame(nsIFrame* aProgressFrame,
+ const nsSize& aRangeSize);
+
+ /**
+ * The div used to show the ::-moz-range-track pseudo-element.
+ * @see nsRangeFrame::CreateAnonymousContent
+ */
+ nsCOMPtr<Element> mTrackDiv;
+
+ /**
+ * The div used to show the ::-moz-range-progress pseudo-element, which is
+ * used to (optionally) style the specific chunk of track leading up to the
+ * thumb's current position.
+ * @see nsRangeFrame::CreateAnonymousContent
+ */
+ nsCOMPtr<Element> mProgressDiv;
+
+ /**
+ * The div used to show the ::-moz-range-thumb pseudo-element.
+ * @see nsRangeFrame::CreateAnonymousContent
+ */
+ nsCOMPtr<Element> mThumbDiv;
+
+ /**
+ * This mutation observer is used to invalidate paint when the @list changes,
+ * when a @list exists.
+ */
+ RefPtr<mozilla::ListMutationObserver> mListMutationObserver;
+};
+
+#endif
diff --git a/layout/forms/nsSearchControlFrame.cpp b/layout/forms/nsSearchControlFrame.cpp
new file mode 100644
index 0000000000..e9249cca77
--- /dev/null
+++ b/layout/forms/nsSearchControlFrame.cpp
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSearchControlFrame.h"
+
+#include "HTMLInputElement.h"
+#include "mozilla/PresShell.h"
+#include "nsGkAtoms.h"
+#include "nsNameSpaceManager.h"
+#include "nsStyleConsts.h"
+#include "nsContentUtils.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsCSSPseudoElements.h"
+#include "nsICSSDeclaration.h"
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/AccTypes.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame* NS_NewSearchControlFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsSearchControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsSearchControlFrame)
+
+NS_QUERYFRAME_HEAD(nsSearchControlFrame)
+ NS_QUERYFRAME_ENTRY(nsSearchControlFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsTextControlFrame)
+
+nsSearchControlFrame::nsSearchControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsTextControlFrame(aStyle, aPresContext, kClassID) {}
+
+void nsSearchControlFrame::Destroy(DestroyContext& aContext) {
+ aContext.AddAnonymousContent(mClearButton.forget());
+ nsTextControlFrame::Destroy(aContext);
+}
+
+nsresult nsSearchControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ // We create an anonymous tree for our input element that is structured as
+ // follows:
+ //
+ // input
+ // div - placeholder
+ // div - preview div
+ // div - editor root
+ // button - clear button
+ //
+ // If you change this, be careful to change the order of stuff in
+ // AppendAnonymousContentTo.
+
+ nsTextControlFrame::CreateAnonymousContent(aElements);
+
+ // FIXME: We could use nsTextControlFrame making the show password buttton
+ // code a bit more generic, or rename this frame and use it for password
+ // inputs.
+ //
+ // Create the ::-moz-search-clear-button pseudo-element:
+ mClearButton = MakeAnonElement(PseudoStyleType::mozSearchClearButton, nullptr,
+ nsGkAtoms::button);
+
+ aElements.AppendElement(mClearButton);
+
+ return NS_OK;
+}
+
+void nsSearchControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ nsTextControlFrame::AppendAnonymousContentTo(aElements, aFilter);
+ if (mClearButton) {
+ aElements.AppendElement(mClearButton);
+ }
+}
diff --git a/layout/forms/nsSearchControlFrame.h b/layout/forms/nsSearchControlFrame.h
new file mode 100644
index 0000000000..0499fa84ea
--- /dev/null
+++ b/layout/forms/nsSearchControlFrame.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSearchControlFrame_h__
+#define nsSearchControlFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "nsTextControlFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "mozilla/RefPtr.h"
+
+class nsITextControlFrame;
+class nsPresContext;
+
+namespace mozilla {
+enum class PseudoStyleType : uint8_t;
+class PresShell;
+namespace dom {
+class Element;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * This frame type is used for <input type=search>.
+ */
+class nsSearchControlFrame final : public nsTextControlFrame {
+ friend nsIFrame* NS_NewSearchControlFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ using PseudoStyleType = mozilla::PseudoStyleType;
+ using Element = mozilla::dom::Element;
+
+ explicit nsSearchControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext);
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsSearchControlFrame)
+
+ void Destroy(DestroyContext&) override;
+
+ // nsIAnonymousContentCreator
+ nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
+ void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"SearchControl"_ns, aResult);
+ }
+#endif
+
+ Element* GetAnonClearButton() const { return mClearButton; }
+
+ /**
+ * Update visbility of the clear button depending on the value
+ */
+ void UpdateClearButtonState();
+
+ private:
+ // See nsSearchControlFrame::CreateAnonymousContent of a description of these
+ RefPtr<Element> mClearButton;
+};
+
+#endif // nsSearchControlFrame_h__
diff --git a/layout/forms/nsSelectsAreaFrame.cpp b/layout/forms/nsSelectsAreaFrame.cpp
new file mode 100644
index 0000000000..1218136023
--- /dev/null
+++ b/layout/forms/nsSelectsAreaFrame.cpp
@@ -0,0 +1,189 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "nsSelectsAreaFrame.h"
+
+#include "mozilla/PresShell.h"
+#include "nsIContent.h"
+#include "nsListControlFrame.h"
+#include "nsDisplayList.h"
+#include "WritingModes.h"
+
+using namespace mozilla;
+
+nsContainerFrame* NS_NewSelectsAreaFrame(PresShell* aShell,
+ ComputedStyle* aStyle) {
+ nsSelectsAreaFrame* it =
+ new (aShell) nsSelectsAreaFrame(aStyle, aShell->GetPresContext());
+ return it;
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsSelectsAreaFrame)
+
+NS_QUERYFRAME_HEAD(nsSelectsAreaFrame)
+ NS_QUERYFRAME_ENTRY(nsSelectsAreaFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
+
+static nsListControlFrame* GetEnclosingListFrame(nsIFrame* aSelectsAreaFrame) {
+ nsIFrame* frame = aSelectsAreaFrame->GetParent();
+ while (frame) {
+ if (frame->IsListControlFrame())
+ return static_cast<nsListControlFrame*>(frame);
+ frame = frame->GetParent();
+ }
+ return nullptr;
+}
+
+void nsSelectsAreaFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ nsListControlFrame* list = GetEnclosingListFrame(this);
+ NS_ASSERTION(list,
+ "Must have an nsListControlFrame! Frame constructor is "
+ "broken");
+
+ nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus);
+
+ // Check whether we need to suppress scrollbar updates. We want to do
+ // that if we're in a possible first pass and our block size of a row
+ // has changed.
+ if (list->MightNeedSecondPass()) {
+ nscoord newBSizeOfARow = list->CalcBSizeOfARow();
+ // We'll need a second pass if our block size of a row changed. For
+ // comboboxes, we'll also need it if our block size changed. If
+ // we're going to do a second pass, suppress scrollbar updates for
+ // this pass.
+ if (newBSizeOfARow != mBSizeOfARow) {
+ mBSizeOfARow = newBSizeOfARow;
+ list->SetSuppressScrollbarUpdate(true);
+ }
+ }
+}
+
+namespace mozilla {
+/**
+ * This wrapper class lets us redirect mouse hits from the child frame of
+ * an option element to the element's own frame.
+ * REVIEW: This is what nsSelectsAreaFrame::GetFrameForPoint used to do
+ */
+class nsDisplayOptionEventGrabber : public nsDisplayWrapList {
+ public:
+ nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
+ nsDisplayItem* aItem)
+ : nsDisplayWrapList(aBuilder, aFrame, aItem) {}
+ nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
+ nsDisplayList* aList)
+ : nsDisplayWrapList(aBuilder, aFrame, aList) {}
+ virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect,
+ HitTestState* aState,
+ nsTArray<nsIFrame*>* aOutFrames) override;
+ virtual bool ShouldFlattenAway(nsDisplayListBuilder* aBuilder) override {
+ return false;
+ }
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override {
+ GetChildren()->Paint(aBuilder, aCtx,
+ mFrame->PresContext()->AppUnitsPerDevPixel());
+ }
+ NS_DISPLAY_DECL_NAME("OptionEventGrabber", TYPE_OPTION_EVENT_GRABBER)
+};
+
+void nsDisplayOptionEventGrabber::HitTest(nsDisplayListBuilder* aBuilder,
+ const nsRect& aRect,
+ HitTestState* aState,
+ nsTArray<nsIFrame*>* aOutFrames) {
+ nsTArray<nsIFrame*> outFrames;
+ mList.HitTest(aBuilder, aRect, aState, &outFrames);
+
+ for (uint32_t i = 0; i < outFrames.Length(); i++) {
+ nsIFrame* selectedFrame = outFrames.ElementAt(i);
+ while (selectedFrame &&
+ !(selectedFrame->GetContent() &&
+ selectedFrame->GetContent()->IsHTMLElement(nsGkAtoms::option))) {
+ selectedFrame = selectedFrame->GetParent();
+ }
+ if (selectedFrame) {
+ aOutFrames->AppendElement(selectedFrame);
+ } else {
+ // keep the original result, which could be this frame
+ aOutFrames->AppendElement(outFrames.ElementAt(i));
+ }
+ }
+}
+
+class nsOptionEventGrabberWrapper : public nsDisplayItemWrapper {
+ public:
+ nsOptionEventGrabberWrapper() = default;
+ virtual nsDisplayItem* WrapList(nsDisplayListBuilder* aBuilder,
+ nsIFrame* aFrame,
+ nsDisplayList* aList) override {
+ return MakeDisplayItem<nsDisplayOptionEventGrabber>(aBuilder, aFrame,
+ aList);
+ }
+ virtual nsDisplayItem* WrapItem(nsDisplayListBuilder* aBuilder,
+ nsDisplayItem* aItem) override {
+ return MakeDisplayItem<nsDisplayOptionEventGrabber>(aBuilder,
+ aItem->Frame(), aItem);
+ }
+};
+
+class nsDisplayListFocus : public nsPaintedDisplayItem {
+ public:
+ nsDisplayListFocus(nsDisplayListBuilder* aBuilder, nsSelectsAreaFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayListFocus);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayListFocus)
+
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override {
+ *aSnap = false;
+ // override bounds because the list item focus ring may extend outside
+ // the nsSelectsAreaFrame
+ nsListControlFrame* listFrame = GetEnclosingListFrame(Frame());
+ return listFrame->InkOverflowRectRelativeToSelf() +
+ listFrame->GetOffsetToCrossDoc(Frame()) + ToReferenceFrame();
+ }
+ virtual void Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) override {
+ nsListControlFrame* listFrame = GetEnclosingListFrame(Frame());
+ // listFrame must be non-null or we wouldn't get called.
+ listFrame->PaintFocus(
+ aCtx->GetDrawTarget(),
+ listFrame->GetOffsetToCrossDoc(Frame()) + ToReferenceFrame());
+ }
+ NS_DISPLAY_DECL_NAME("ListFocus", TYPE_LIST_FOCUS)
+};
+
+} // namespace mozilla
+
+void nsSelectsAreaFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ if (!aBuilder->IsForEventDelivery()) {
+ BuildDisplayListInternal(aBuilder, aLists);
+ return;
+ }
+
+ nsDisplayListCollection set(aBuilder);
+ BuildDisplayListInternal(aBuilder, set);
+
+ nsOptionEventGrabberWrapper wrapper;
+ wrapper.WrapLists(aBuilder, this, set, aLists);
+}
+
+void nsSelectsAreaFrame::BuildDisplayListInternal(
+ nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) {
+ nsBlockFrame::BuildDisplayList(aBuilder, aLists);
+
+ nsListControlFrame* listFrame = GetEnclosingListFrame(this);
+ if (listFrame && listFrame->IsFocused()) {
+ // we can't just associate the display item with the list frame,
+ // because then the list's scrollframe won't clip it (the scrollframe
+ // only clips contained descendants).
+ aLists.Outlines()->AppendNewToTop<nsDisplayListFocus>(aBuilder, this);
+ }
+}
diff --git a/layout/forms/nsSelectsAreaFrame.h b/layout/forms/nsSelectsAreaFrame.h
new file mode 100644
index 0000000000..d0cd5a3ba3
--- /dev/null
+++ b/layout/forms/nsSelectsAreaFrame.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsSelectsAreaFrame_h___
+#define nsSelectsAreaFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsBlockFrame.h"
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+class nsSelectsAreaFrame final : public nsBlockFrame {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsSelectsAreaFrame)
+
+ friend nsContainerFrame* NS_NewSelectsAreaFrame(mozilla::PresShell* aShell,
+ ComputedStyle* aStyle);
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ void BuildDisplayListInternal(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists);
+
+ void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ nscoord BSizeOfARow() const { return mBSizeOfARow; }
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"SelectsArea"_ns, aResult);
+ }
+#endif
+
+ protected:
+ explicit nsSelectsAreaFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBlockFrame(aStyle, aPresContext, kClassID),
+ // initialize to wacky value so first call of
+ // nsSelectsAreaFrame::Reflow will always invalidate
+ mBSizeOfARow(nscoord_MIN) {}
+
+ // We cache the block size of a single row so that changes to the
+ // "size" attribute, padding, etc. can all be handled with only one
+ // reflow. We'll have to reflow twice if someone changes our font
+ // size or something like that, so that the block size of our options
+ // will change.
+ nscoord mBSizeOfARow;
+};
+
+#endif /* nsSelectsAreaFrame_h___ */
diff --git a/layout/forms/nsTextControlFrame.cpp b/layout/forms/nsTextControlFrame.cpp
new file mode 100644
index 0000000000..c51b94c56a
--- /dev/null
+++ b/layout/forms/nsTextControlFrame.cpp
@@ -0,0 +1,1325 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/DebugOnly.h"
+
+#include "gfxContext.h"
+#include "nsCOMPtr.h"
+#include "nsFontMetrics.h"
+#include "nsTextControlFrame.h"
+#include "nsIEditor.h"
+#include "nsCaret.h"
+#include "nsCSSPseudoElements.h"
+#include "nsDisplayList.h"
+#include "nsGenericHTMLElement.h"
+#include "nsTextFragment.h"
+#include "nsNameSpaceManager.h"
+
+#include "nsIContent.h"
+#include "nsIScrollableFrame.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsLayoutUtils.h"
+
+#include <algorithm>
+#include "nsRange.h" //for selection setting helper func
+#include "nsINode.h"
+#include "nsPIDOMWindow.h" //needed for notify selection changed to update the menus ect.
+#include "nsQueryObject.h"
+#include "nsILayoutHistoryState.h"
+
+#include "nsFocusManager.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresState.h"
+#include "mozilla/TextEditor.h"
+#include "nsAttrValueInlines.h"
+#include "mozilla/dom/Selection.h"
+#include "nsContentUtils.h"
+#include "nsTextNode.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLTextAreaElement.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/Text.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/Try.h"
+#include "nsFrameSelection.h"
+
+#define DEFAULT_COLUMN_WIDTH 20
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame* NS_NewTextControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsTextControlFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsTextControlFrame)
+
+NS_QUERYFRAME_HEAD(nsTextControlFrame)
+ NS_QUERYFRAME_ENTRY(nsTextControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+ NS_QUERYFRAME_ENTRY(nsITextControlFrame)
+ NS_QUERYFRAME_ENTRY(nsIStatefulFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+#ifdef ACCESSIBILITY
+a11y::AccType nsTextControlFrame::AccessibleType() {
+ return a11y::eHTMLTextFieldType;
+}
+#endif
+
+#ifdef DEBUG
+class EditorInitializerEntryTracker {
+ public:
+ explicit EditorInitializerEntryTracker(nsTextControlFrame& frame)
+ : mFrame(frame), mFirstEntry(false) {
+ if (!mFrame.mInEditorInitialization) {
+ mFrame.mInEditorInitialization = true;
+ mFirstEntry = true;
+ }
+ }
+ ~EditorInitializerEntryTracker() {
+ if (mFirstEntry) {
+ mFrame.mInEditorInitialization = false;
+ }
+ }
+ bool EnteredMoreThanOnce() const { return !mFirstEntry; }
+
+ private:
+ nsTextControlFrame& mFrame;
+ bool mFirstEntry;
+};
+#endif
+
+class nsTextControlFrame::nsAnonDivObserver final
+ : public nsStubMutationObserver {
+ public:
+ explicit nsAnonDivObserver(nsTextControlFrame& aFrame) : mFrame(aFrame) {}
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ private:
+ ~nsAnonDivObserver() = default;
+ nsTextControlFrame& mFrame;
+};
+
+nsTextControlFrame::nsTextControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext,
+ nsIFrame::ClassID aClassID)
+ : nsContainerFrame(aStyle, aPresContext, aClassID) {}
+
+nsTextControlFrame::~nsTextControlFrame() = default;
+
+nsIScrollableFrame* nsTextControlFrame::GetScrollTargetFrame() const {
+ if (!mRootNode) {
+ return nullptr;
+ }
+ return do_QueryFrame(mRootNode->GetPrimaryFrame());
+}
+
+void nsTextControlFrame::Destroy(DestroyContext& aContext) {
+ RemoveProperty(TextControlInitializer());
+
+ // Unbind the text editor state object from the frame. The editor will live
+ // on, but things like controllers will be released.
+ RefPtr textControlElement = ControlElement();
+ if (mMutationObserver) {
+ textControlElement->UnbindFromFrame(this);
+ mRootNode->RemoveMutationObserver(mMutationObserver);
+ mMutationObserver = nullptr;
+ }
+
+ // If there is a drag session, user may be dragging selection in removing
+ // text node in the text control. If so, we should set source node to the
+ // text control because another text node may be recreated soon if the text
+ // control is just reframed.
+ if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) {
+ if (dragSession->IsDraggingTextInTextControl() && mRootNode &&
+ mRootNode->GetFirstChild()) {
+ nsCOMPtr<nsINode> sourceNode;
+ if (NS_SUCCEEDED(
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode))) &&
+ mRootNode->Contains(sourceNode)) {
+ MOZ_ASSERT(sourceNode->IsText());
+ dragSession->UpdateSource(textControlElement, nullptr);
+ }
+ }
+ }
+ // Otherwise, EventStateManager may track gesture to start drag with native
+ // anonymous nodes in the text control element.
+ else if (textControlElement->GetPresContext(Element::eForComposedDoc)) {
+ textControlElement->GetPresContext(Element::eForComposedDoc)
+ ->EventStateManager()
+ ->TextControlRootWillBeRemoved(*textControlElement);
+ }
+
+ // If we're a subclass like nsNumberControlFrame, then it owns the root of the
+ // anonymous subtree where mRootNode is.
+ aContext.AddAnonymousContent(mRootNode.forget());
+ aContext.AddAnonymousContent(mPlaceholderDiv.forget());
+ aContext.AddAnonymousContent(mPreviewDiv.forget());
+ aContext.AddAnonymousContent(mRevealButton.forget());
+
+ nsContainerFrame::Destroy(aContext);
+}
+
+LogicalSize nsTextControlFrame::CalcIntrinsicSize(
+ gfxContext* aRenderingContext, WritingMode aWM,
+ float aFontSizeInflation) const {
+ LogicalSize intrinsicSize(aWM);
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation);
+ const nscoord lineHeight =
+ ReflowInput::CalcLineHeight(*Style(), PresContext(), GetContent(),
+ NS_UNCONSTRAINEDSIZE, aFontSizeInflation);
+ // Use the larger of the font's "average" char width or the width of the
+ // zero glyph (if present) as the basis for resolving the size attribute.
+ const nscoord charWidth =
+ std::max(fontMet->ZeroOrAveCharWidth(), fontMet->AveCharWidth());
+ const nscoord charMaxAdvance = fontMet->MaxAdvance();
+
+ // Initialize based on the width in characters.
+ const int32_t cols = GetCols();
+ intrinsicSize.ISize(aWM) = cols * charWidth;
+
+ // If we do not have what appears to be a fixed-width font, add a "slop"
+ // amount based on the max advance of the font (clamped to twice charWidth,
+ // because some fonts have a few extremely-wide outliers that would result
+ // in excessive width here; e.g. the triple-emdash ligature in SFNS Text),
+ // minus 4px. This helps avoid input fields becoming unusably narrow with
+ // small size values.
+ if (charMaxAdvance - charWidth > AppUnitsPerCSSPixel()) {
+ nscoord internalPadding =
+ std::max(0, std::min(charMaxAdvance, charWidth * 2) -
+ nsPresContext::CSSPixelsToAppUnits(4));
+ internalPadding = RoundToMultiple(internalPadding, AppUnitsPerCSSPixel());
+ intrinsicSize.ISize(aWM) += internalPadding;
+ } else {
+ // This is to account for the anonymous <br> having a 1 twip width
+ // in Full Standards mode, see BRFrame::Reflow and bug 228752.
+ if (PresContext()->CompatibilityMode() == eCompatibility_FullStandards) {
+ intrinsicSize.ISize(aWM) += 1;
+ }
+ }
+
+ // Increment width with cols * letter-spacing.
+ {
+ const StyleLength& letterSpacing = StyleText()->mLetterSpacing;
+ if (!letterSpacing.IsZero()) {
+ intrinsicSize.ISize(aWM) += cols * letterSpacing.ToAppUnits();
+ }
+ }
+
+ // Set the height equal to total number of rows (times the height of each
+ // line, of course)
+ intrinsicSize.BSize(aWM) = lineHeight * GetRows();
+
+ // Add in the size of the scrollbars for textarea
+ if (IsTextArea()) {
+ nsIScrollableFrame* scrollableFrame = GetScrollTargetFrame();
+ NS_ASSERTION(scrollableFrame, "Child must be scrollable");
+ if (scrollableFrame) {
+ LogicalMargin scrollbarSizes(aWM,
+ scrollableFrame->GetDesiredScrollbarSizes());
+ intrinsicSize.ISize(aWM) += scrollbarSizes.IStartEnd(aWM);
+ intrinsicSize.BSize(aWM) += scrollbarSizes.BStartEnd(aWM);
+ }
+ }
+ return intrinsicSize;
+}
+
+nsresult nsTextControlFrame::EnsureEditorInitialized() {
+ // This method initializes our editor, if needed.
+
+ // This code used to be called from CreateAnonymousContent(), but
+ // when the editor set the initial string, it would trigger a
+ // PresShell listener which called FlushPendingNotifications()
+ // during frame construction. This was causing other form controls
+ // to display wrong values. Additionally, calling this every time
+ // a text frame control is instantiated means that we're effectively
+ // instantiating the editor for all text fields, even if they
+ // never get used. So, now this method is being called lazily only
+ // when we actually need an editor.
+
+ if (mEditorHasBeenInitialized) return NS_OK;
+
+ Document* doc = mContent->GetComposedDoc();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ AutoWeakFrame weakFrame(this);
+
+ // Flush out content on our document. Have to do this, because script
+ // blockers don't prevent the sink flushing out content and notifying in the
+ // process, which can destroy frames.
+ doc->FlushPendingNotifications(FlushType::ContentAndNotify);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_ERROR_FAILURE);
+
+ // Make sure that editor init doesn't do things that would kill us off
+ // (especially off the script blockers it'll create for its DOM mutations).
+ {
+ RefPtr<TextControlElement> textControlElement = ControlElement();
+
+ // Hide selection changes during the initialization, as webpages should not
+ // be aware of these initializations
+ AutoHideSelectionChanges hideSelectionChanges(
+ textControlElement->GetConstFrameSelection());
+
+ nsAutoScriptBlocker scriptBlocker;
+
+ // Time to mess with our security context... See comments in GetValue()
+ // for why this is needed.
+ mozilla::dom::AutoNoJSAPI nojsapi;
+
+ // Make sure that we try to focus the content even if the method fails
+ class EnsureSetFocus {
+ public:
+ explicit EnsureSetFocus(nsTextControlFrame* aFrame) : mFrame(aFrame) {}
+ ~EnsureSetFocus() {
+ if (nsContentUtils::IsFocusedContent(mFrame->GetContent()))
+ mFrame->SetFocus(true, false);
+ }
+
+ private:
+ nsTextControlFrame* mFrame;
+ };
+ EnsureSetFocus makeSureSetFocusHappens(this);
+
+#ifdef DEBUG
+ // Make sure we are not being called again until we're finished.
+ // If reentrancy happens, just pretend that we don't have an editor.
+ const EditorInitializerEntryTracker tracker(*this);
+ NS_ASSERTION(!tracker.EnteredMoreThanOnce(),
+ "EnsureEditorInitialized has been called while a previous "
+ "call was in progress");
+#endif
+
+ // Create an editor for the frame, if one doesn't already exist
+ nsresult rv = textControlElement->CreateEditor();
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+
+ // Set mEditorHasBeenInitialized so that subsequent calls will use the
+ // editor.
+ mEditorHasBeenInitialized = true;
+
+ if (weakFrame.IsAlive()) {
+ uint32_t position = 0;
+
+ // Set the selection to the end of the text field (bug 1287655),
+ // but only if the contents has changed (bug 1337392).
+ if (textControlElement->ValueChanged()) {
+ nsAutoString val;
+ textControlElement->GetTextEditorValue(val);
+ position = val.Length();
+ }
+
+ SetSelectionEndPoints(position, position);
+ }
+ }
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ return NS_OK;
+}
+
+already_AddRefed<Element> nsTextControlFrame::MakeAnonElement(
+ PseudoStyleType aPseudoType, Element* aParent, nsAtom* aTag) const {
+ MOZ_ASSERT(aPseudoType != PseudoStyleType::NotPseudo);
+ Document* doc = PresContext()->Document();
+ RefPtr<Element> element = doc->CreateHTMLElement(aTag);
+ element->SetPseudoElementType(aPseudoType);
+ if (aPseudoType == PseudoStyleType::mozTextControlEditingRoot) {
+ // Make our root node editable
+ element->SetFlags(NODE_IS_EDITABLE);
+ }
+
+ if (aPseudoType == PseudoStyleType::mozNumberSpinDown ||
+ aPseudoType == PseudoStyleType::mozNumberSpinUp) {
+ element->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, u"true"_ns,
+ false);
+ }
+
+ if (aParent) {
+ aParent->AppendChildTo(element, false, IgnoreErrors());
+ }
+
+ return element.forget();
+}
+
+already_AddRefed<Element> nsTextControlFrame::MakeAnonDivWithTextNode(
+ PseudoStyleType aPseudoType) const {
+ RefPtr<Element> div = MakeAnonElement(aPseudoType);
+
+ // Create the text node for the anonymous <div> element.
+ nsNodeInfoManager* nim = div->OwnerDoc()->NodeInfoManager();
+ RefPtr<nsTextNode> textNode = new (nim) nsTextNode(nim);
+ // If the anonymous div element is not for the placeholder, we should
+ // mark the text node as "maybe modified frequently" for avoiding ASCII
+ // range checks at every input.
+ if (aPseudoType != PseudoStyleType::placeholder) {
+ textNode->MarkAsMaybeModifiedFrequently();
+ // Additionally, this is a password field, the text node needs to be
+ // marked as "maybe masked" unless it's in placeholder.
+ if (IsPasswordTextControl()) {
+ textNode->MarkAsMaybeMasked();
+ }
+ }
+ div->AppendChildTo(textNode, false, IgnoreErrors());
+ return div.forget();
+}
+
+nsresult nsTextControlFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript());
+ MOZ_ASSERT(mContent, "We should have a content!");
+
+ AddStateBits(NS_FRAME_INDEPENDENT_SELECTION);
+
+ RefPtr<TextControlElement> textControlElement = ControlElement();
+ mRootNode = MakeAnonElement(PseudoStyleType::mozTextControlEditingRoot);
+ if (NS_WARN_IF(!mRootNode)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mMutationObserver = new nsAnonDivObserver(*this);
+ mRootNode->AddMutationObserver(mMutationObserver);
+
+ // Bind the frame to its text control.
+ //
+ // This can realistically fail in paginated mode, where we may replicate
+ // fixed-positioned elements and the replicated frame will not get the chance
+ // to get an editor.
+ nsresult rv = textControlElement->BindToFrame(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mRootNode->RemoveMutationObserver(mMutationObserver);
+ mMutationObserver = nullptr;
+ mRootNode = nullptr;
+ return rv;
+ }
+
+ CreatePlaceholderIfNeeded();
+ if (mPlaceholderDiv) {
+ aElements.AppendElement(mPlaceholderDiv);
+ }
+ CreatePreviewIfNeeded();
+ if (mPreviewDiv) {
+ aElements.AppendElement(mPreviewDiv);
+ }
+
+ // NOTE(emilio): We want the root node always after the placeholder so that
+ // background on the placeholder doesn't obscure the caret.
+ aElements.AppendElement(mRootNode);
+
+ if (StaticPrefs::layout_forms_reveal_password_button_enabled() &&
+ IsPasswordTextControl()) {
+ mRevealButton =
+ MakeAnonElement(PseudoStyleType::mozReveal, nullptr, nsGkAtoms::button);
+ mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden,
+ u"true"_ns, false);
+ mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
+ false);
+ aElements.AppendElement(mRevealButton);
+ }
+
+ rv = UpdateValueDisplay(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ InitializeEagerlyIfNeeded();
+ return NS_OK;
+}
+
+bool nsTextControlFrame::ShouldInitializeEagerly() const {
+ // textareas are eagerly initialized.
+ if (!IsSingleLineTextControl()) {
+ return true;
+ }
+
+ // Also, input elements which have a cached selection should get eager
+ // editor initialization.
+ TextControlElement* textControlElement = ControlElement();
+ if (textControlElement->HasCachedSelection()) {
+ return true;
+ }
+
+ // So do input text controls with spellcheck=true
+ if (auto* htmlElement = nsGenericHTMLElement::FromNode(mContent)) {
+ if (htmlElement->Spellcheck()) {
+ return true;
+ }
+ }
+
+ // If text in the editor is being dragged, we need the editor to create
+ // new source node for the drag session (TextEditor creates the text node
+ // in the anonymous <div> element.
+ if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) {
+ if (dragSession->IsDraggingTextInTextControl()) {
+ nsCOMPtr<nsINode> sourceNode;
+ if (NS_SUCCEEDED(
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode))) &&
+ sourceNode == textControlElement) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void nsTextControlFrame::InitializeEagerlyIfNeeded() {
+ MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript(),
+ "Someone forgot a script blocker?");
+ if (!ShouldInitializeEagerly()) {
+ return;
+ }
+
+ EditorInitializer* initializer = new EditorInitializer(this);
+ SetProperty(TextControlInitializer(), initializer);
+ nsContentUtils::AddScriptRunner(initializer);
+}
+
+void nsTextControlFrame::CreatePlaceholderIfNeeded() {
+ MOZ_ASSERT(!mPlaceholderDiv);
+
+ // Do we need a placeholder node?
+ nsAutoString placeholder;
+ if (!mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, placeholder)) {
+ return;
+ }
+
+ mPlaceholderDiv = MakeAnonDivWithTextNode(PseudoStyleType::placeholder);
+ UpdatePlaceholderText(placeholder, false);
+}
+
+void nsTextControlFrame::PlaceholderChanged(const nsAttrValue* aOld,
+ const nsAttrValue* aNew) {
+ if (!aOld || !aNew) {
+ return; // This should be handled by GetAttributeChangeHint.
+ }
+
+ // If we've changed the attribute but we still haven't reframed, there's
+ // nothing to do either.
+ if (!mPlaceholderDiv) {
+ return;
+ }
+
+ nsAutoString placeholder;
+ aNew->ToString(placeholder);
+ UpdatePlaceholderText(placeholder, true);
+}
+
+void nsTextControlFrame::UpdatePlaceholderText(nsString& aPlaceholder,
+ bool aNotify) {
+ MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv);
+ MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv->GetFirstChild());
+
+ if (IsTextArea()) { // <textarea>s preserve newlines...
+ nsContentUtils::PlatformToDOMLineBreaks(aPlaceholder);
+ } else { // ...<input>s don't
+ nsContentUtils::RemoveNewlines(aPlaceholder);
+ }
+
+ mPlaceholderDiv->GetFirstChild()->AsText()->SetText(aPlaceholder, aNotify);
+}
+
+void nsTextControlFrame::CreatePreviewIfNeeded() {
+ if (!ControlElement()->IsPreviewEnabled()) {
+ return;
+ }
+ mPreviewDiv = MakeAnonDivWithTextNode(PseudoStyleType::mozTextControlPreview);
+}
+
+void nsTextControlFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mPlaceholderDiv && !(aFilter & nsIContent::eSkipPlaceholderContent)) {
+ aElements.AppendElement(mPlaceholderDiv);
+ }
+
+ if (mPreviewDiv) {
+ aElements.AppendElement(mPreviewDiv);
+ }
+
+ if (mRevealButton) {
+ aElements.AppendElement(mRevealButton);
+ }
+
+ aElements.AppendElement(mRootNode);
+}
+
+nscoord nsTextControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result = 0;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+ float inflation = nsLayoutUtils::FontSizeInflationFor(this);
+ WritingMode wm = GetWritingMode();
+ result = CalcIntrinsicSize(aRenderingContext, wm, inflation).ISize(wm);
+ return result;
+}
+
+nscoord nsTextControlFrame::GetMinISize(gfxContext* aRenderingContext) {
+ // Our min inline size is just our preferred width if we have auto inline size
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+ result = GetPrefISize(aRenderingContext);
+ return result;
+}
+
+LogicalSize nsTextControlFrame::ComputeAutoSize(
+ gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize,
+ nscoord aAvailableISize, const LogicalSize& aMargin,
+ const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
+ ComputeSizeFlags aFlags) {
+ float inflation = nsLayoutUtils::FontSizeInflationFor(this);
+ LogicalSize autoSize = CalcIntrinsicSize(aRenderingContext, aWM, inflation);
+
+ // Note: nsContainerFrame::ComputeAutoSize only computes the inline-size (and
+ // only for 'auto'), the block-size it returns is always NS_UNCONSTRAINEDSIZE.
+ const auto& styleISize = aSizeOverrides.mStyleISize
+ ? *aSizeOverrides.mStyleISize
+ : StylePosition()->ISize(aWM);
+ if (styleISize.IsAuto()) {
+ if (aFlags.contains(ComputeSizeFlag::IClampMarginBoxMinSize)) {
+ // CalcIntrinsicSize isn't aware of grid-item margin-box clamping, so we
+ // fall back to nsContainerFrame's ComputeAutoSize to handle that.
+ // XXX maybe a font-inflation issue here? (per the assertion below).
+ autoSize.ISize(aWM) =
+ nsContainerFrame::ComputeAutoSize(
+ aRenderingContext, aWM, aCBSize, aAvailableISize, aMargin,
+ aBorderPadding, aSizeOverrides, aFlags)
+ .ISize(aWM);
+ }
+#ifdef DEBUG
+ else {
+ LogicalSize ancestorAutoSize = nsContainerFrame::ComputeAutoSize(
+ aRenderingContext, aWM, aCBSize, aAvailableISize, aMargin,
+ aBorderPadding, aSizeOverrides, aFlags);
+ MOZ_ASSERT(inflation != 1.0f ||
+ ancestorAutoSize.ISize(aWM) == autoSize.ISize(aWM),
+ "Incorrect size computed by ComputeAutoSize?");
+ }
+#endif
+ }
+ return autoSize;
+}
+
+Maybe<nscoord> nsTextControlFrame::ComputeBaseline(
+ const nsIFrame* aFrame, const ReflowInput& aReflowInput,
+ bool aForSingleLineControl) {
+ // If we're layout-contained, we have no baseline.
+ if (aReflowInput.mStyleDisplay->IsContainLayout()) {
+ return Nothing();
+ }
+ WritingMode wm = aReflowInput.GetWritingMode();
+
+ nscoord lineHeight = aReflowInput.ComputedBSize();
+ if (!aForSingleLineControl || lineHeight == NS_UNCONSTRAINEDSIZE) {
+ lineHeight = aReflowInput.ApplyMinMaxBSize(aReflowInput.GetLineHeight());
+ }
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
+ return Some(nsLayoutUtils::GetCenteredFontBaseline(fontMet, lineHeight,
+ wm.IsLineInverted()) +
+ aReflowInput.ComputedLogicalBorderPadding(wm).BStart(wm));
+}
+
+static bool IsButtonBox(const nsIFrame* aFrame) {
+ auto pseudoType = aFrame->Style()->GetPseudoType();
+ return pseudoType == PseudoStyleType::mozNumberSpinBox ||
+ pseudoType == PseudoStyleType::mozSearchClearButton ||
+ pseudoType == PseudoStyleType::mozReveal;
+}
+
+void nsTextControlFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsTextControlFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ // set values of reflow's out parameters
+ WritingMode wm = aReflowInput.GetWritingMode();
+ aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm));
+
+ {
+ // Calculate the baseline and store it in mFirstBaseline.
+ auto baseline =
+ ComputeBaseline(this, aReflowInput, IsSingleLineTextControl());
+ mFirstBaseline = baseline.valueOr(NS_INTRINSIC_ISIZE_UNKNOWN);
+ if (baseline) {
+ aDesiredSize.SetBlockStartAscent(*baseline);
+ }
+ }
+
+ // overflow handling
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+ nsIFrame* buttonBox = [&]() -> nsIFrame* {
+ nsIFrame* last = mFrames.LastChild();
+ if (!last || !IsButtonBox(last)) {
+ return nullptr;
+ }
+ return last;
+ }();
+
+ // Reflow the button box first, so that we can use its size for the other
+ // frames.
+ nscoord buttonBoxISize = 0;
+ if (buttonBox) {
+ ReflowTextControlChild(buttonBox, aPresContext, aReflowInput, aStatus,
+ aDesiredSize, buttonBoxISize);
+ }
+
+ // perform reflow on all kids
+ nsIFrame* kid = mFrames.FirstChild();
+ while (kid) {
+ if (kid != buttonBox) {
+ MOZ_ASSERT(!IsButtonBox(kid),
+ "Should only have one button box, and should be last");
+ ReflowTextControlChild(kid, aPresContext, aReflowInput, aStatus,
+ aDesiredSize, buttonBoxISize);
+ }
+ kid = kid->GetNextSibling();
+ }
+
+ // take into account css properties that affect overflow handling
+ FinishAndStoreOverflow(&aDesiredSize);
+
+ aStatus.Reset(); // This type of frame can't be split.
+}
+
+void nsTextControlFrame::ReflowTextControlChild(
+ nsIFrame* aKid, nsPresContext* aPresContext,
+ const ReflowInput& aReflowInput, nsReflowStatus& aStatus,
+ ReflowOutput& aParentDesiredSize, nscoord& aButtonBoxISize) {
+ const WritingMode outerWM = aReflowInput.GetWritingMode();
+ // compute available size and frame offsets for child
+ const WritingMode wm = aKid->GetWritingMode();
+ LogicalSize availSize = aReflowInput.ComputedSizeWithPadding(wm);
+ availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
+
+ bool isButtonBox = IsButtonBox(aKid);
+
+ ReflowInput kidReflowInput(aPresContext, aReflowInput, aKid, availSize,
+ Nothing(), ReflowInput::InitFlag::CallerWillInit);
+
+ // Override padding with our computed padding in case we got it from theming
+ // or percentage, if we're not the button box.
+ auto overridePadding =
+ isButtonBox ? Nothing() : Some(aReflowInput.ComputedLogicalPadding(wm));
+ if (!isButtonBox && aButtonBoxISize) {
+ // Button box respects inline-end-padding, so we don't need to.
+ overridePadding->IEnd(outerWM) = 0;
+ }
+
+ // We want to let our button box fill the frame in the block axis, up to the
+ // edge of the control's border. So, we use the control's padding-box as the
+ // containing block size for our button box.
+ auto overrideCBSize =
+ isButtonBox ? Some(aReflowInput.ComputedSizeWithPadding(wm)) : Nothing();
+ kidReflowInput.Init(aPresContext, overrideCBSize, Nothing(), overridePadding);
+
+ LogicalPoint position(wm);
+ if (!isButtonBox) {
+ MOZ_ASSERT(wm == outerWM,
+ "Shouldn't have to care about orthogonal "
+ "writing-modes and such inside the control, "
+ "except for the number spin-box which forces "
+ "horizontal-tb");
+
+ const auto& border = aReflowInput.ComputedLogicalBorder(wm);
+
+ // Offset the frame by the size of the parent's border. Note that we don't
+ // have to account for the parent's padding here, because this child
+ // actually "inherits" that padding and manages it on behalf of the parent.
+ position.B(wm) = border.BStart(wm);
+ position.I(wm) = border.IStart(wm);
+
+ // Set computed width and computed height for the child (the button box is
+ // the only exception, which has an auto size).
+ kidReflowInput.SetComputedISize(
+ std::max(0, aReflowInput.ComputedISize() - aButtonBoxISize));
+ kidReflowInput.SetComputedBSize(aReflowInput.ComputedBSize());
+ }
+
+ // reflow the child
+ ReflowOutput desiredSize(aReflowInput);
+ const nsSize containerSize =
+ aReflowInput.ComputedSizeWithBorderPadding(outerWM).GetPhysicalSize(
+ outerWM);
+ ReflowChild(aKid, aPresContext, desiredSize, kidReflowInput, wm, position,
+ containerSize, ReflowChildFlags::Default, aStatus);
+
+ if (isButtonBox) {
+ const auto& bp = aReflowInput.ComputedLogicalBorderPadding(outerWM);
+ auto size = desiredSize.Size(outerWM);
+ // Center button in the block axis of our content box. We do this
+ // computation in terms of outerWM for simplicity.
+ LogicalRect buttonRect(outerWM);
+ buttonRect.BSize(outerWM) = size.BSize(outerWM);
+ buttonRect.ISize(outerWM) = size.ISize(outerWM);
+ buttonRect.BStart(outerWM) =
+ bp.BStart(outerWM) +
+ (aReflowInput.ComputedBSize() - size.BSize(outerWM)) / 2;
+ // Align to the inline-end of the content box.
+ buttonRect.IStart(outerWM) =
+ bp.IStart(outerWM) + aReflowInput.ComputedISize() - size.ISize(outerWM);
+ buttonRect = buttonRect.ConvertTo(wm, outerWM, containerSize);
+ position = buttonRect.Origin(wm);
+ aButtonBoxISize = size.ISize(outerWM);
+ }
+
+ // place the child
+ FinishReflowChild(aKid, aPresContext, desiredSize, &kidReflowInput, wm,
+ position, containerSize, ReflowChildFlags::Default);
+
+ // consider the overflow
+ aParentDesiredSize.mOverflowAreas.UnionWith(desiredSize.mOverflowAreas);
+}
+
+// IMPLEMENTING NS_IFORMCONTROLFRAME
+void nsTextControlFrame::SetFocus(bool aOn, bool aRepaint) {
+ // If 'dom.placeholeder.show_on_focus' preference is 'false', focusing or
+ // blurring the frame can have an impact on the placeholder visibility.
+ if (!aOn) {
+ return;
+ }
+
+ nsISelectionController* selCon = GetSelectionController();
+ if (!selCon) {
+ return;
+ }
+
+ RefPtr<Selection> ourSel =
+ selCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (!ourSel) {
+ return;
+ }
+
+ mozilla::PresShell* presShell = PresShell();
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (!caret) {
+ return;
+ }
+
+ // Tell the caret to use our selection
+ caret->SetSelection(ourSel);
+
+ // mutual-exclusion: the selection is either controlled by the
+ // document or by the text input/area. Clear any selection in the
+ // document since the focus is now on our independent selection.
+
+ RefPtr<Selection> docSel =
+ presShell->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (!docSel) {
+ return;
+ }
+
+ if (!docSel->IsCollapsed()) {
+ docSel->RemoveAllRanges(IgnoreErrors());
+ }
+
+ // If the focus moved to a text control during text selection by pointer
+ // device, stop extending the selection.
+ if (RefPtr<nsFrameSelection> frameSelection = presShell->FrameSelection()) {
+ frameSelection->SetDragState(false);
+ }
+}
+
+nsresult nsTextControlFrame::SetFormProperty(nsAtom* aName,
+ const nsAString& aValue) {
+ if (!mIsProcessing) { // some kind of lock.
+ mIsProcessing = true;
+ if (nsGkAtoms::select == aName) {
+ // Select all the text.
+ //
+ // XXX: This is lame, we can't call editor's SelectAll method
+ // because that triggers AutoCopies in unix builds.
+ // Instead, we have to call our own homegrown version
+ // of select all which merely builds a range that selects
+ // all of the content and adds that to the selection.
+
+ AutoWeakFrame weakThis = this;
+ SelectAllOrCollapseToEndOfText(true); // NOTE: can destroy the world
+ if (!weakThis.IsAlive()) {
+ return NS_OK;
+ }
+ }
+ mIsProcessing = false;
+ }
+ return NS_OK;
+}
+
+already_AddRefed<TextEditor> nsTextControlFrame::GetTextEditor() {
+ if (NS_WARN_IF(NS_FAILED(EnsureEditorInitialized()))) {
+ return nullptr;
+ }
+ RefPtr el = ControlElement();
+ return do_AddRef(el->GetTextEditor());
+}
+
+nsresult nsTextControlFrame::SetSelectionInternal(
+ nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
+ uint32_t aEndOffset, SelectionDirection aDirection) {
+ // Get the selection, clear it and add the new range to it!
+ nsISelectionController* selCon = GetSelectionController();
+ NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE);
+
+ RefPtr<Selection> selection =
+ selCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ nsDirection direction;
+ if (aDirection == SelectionDirection::None) {
+ // Preserve the direction
+ direction = selection->GetDirection();
+ } else {
+ direction =
+ aDirection == SelectionDirection::Backward ? eDirPrevious : eDirNext;
+ }
+
+ MOZ_TRY(selection->SetStartAndEndInLimiter(*aStartNode, aStartOffset,
+ *aEndNode, aEndOffset, direction,
+ nsISelectionListener::JS_REASON));
+ return NS_OK;
+}
+
+void nsTextControlFrame::ScrollSelectionIntoViewAsync(
+ ScrollAncestors aScrollAncestors) {
+ nsISelectionController* selCon = GetSelectionController();
+ if (!selCon) {
+ return;
+ }
+
+ int16_t flags = aScrollAncestors == ScrollAncestors::Yes
+ ? 0
+ : nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY;
+
+ // Scroll the selection into view (see bug 231389).
+ selCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION, flags);
+}
+
+nsresult nsTextControlFrame::SelectAllOrCollapseToEndOfText(bool aSelect) {
+ nsresult rv = EnsureEditorInitialized();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsINode> rootNode = mRootNode;
+ NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
+
+ RefPtr<Text> text = Text::FromNodeOrNull(rootNode->GetFirstChild());
+ MOZ_ASSERT(text);
+
+ uint32_t length = text->Length();
+
+ rv = SetSelectionInternal(text, aSelect ? 0 : length, text, length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ScrollSelectionIntoViewAsync();
+ return NS_OK;
+}
+
+nsresult nsTextControlFrame::SetSelectionEndPoints(
+ uint32_t aSelStart, uint32_t aSelEnd,
+ nsITextControlFrame::SelectionDirection aDirection) {
+ NS_ASSERTION(aSelStart <= aSelEnd, "Invalid selection offsets!");
+
+ if (aSelStart > aSelEnd) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsINode> startNode, endNode;
+ uint32_t startOffset, endOffset;
+
+ // Calculate the selection start point.
+
+ nsresult rv =
+ OffsetToDOMPoint(aSelStart, getter_AddRefs(startNode), &startOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aSelStart == aSelEnd) {
+ // Collapsed selection, so start and end are the same!
+ endNode = startNode;
+ endOffset = startOffset;
+ } else {
+ // Selection isn't collapsed so we have to calculate
+ // the end point too.
+
+ rv = OffsetToDOMPoint(aSelEnd, getter_AddRefs(endNode), &endOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return SetSelectionInternal(startNode, startOffset, endNode, endOffset,
+ aDirection);
+}
+
+NS_IMETHODIMP
+nsTextControlFrame::SetSelectionRange(
+ uint32_t aSelStart, uint32_t aSelEnd,
+ nsITextControlFrame::SelectionDirection aDirection) {
+ nsresult rv = EnsureEditorInitialized();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aSelStart > aSelEnd) {
+ // Simulate what we'd see SetSelectionStart() was called, followed
+ // by a SetSelectionEnd().
+
+ aSelStart = aSelEnd;
+ }
+
+ return SetSelectionEndPoints(aSelStart, aSelEnd, aDirection);
+}
+
+nsresult nsTextControlFrame::OffsetToDOMPoint(uint32_t aOffset,
+ nsINode** aResult,
+ uint32_t* aPosition) {
+ NS_ENSURE_ARG_POINTER(aResult && aPosition);
+
+ *aResult = nullptr;
+ *aPosition = 0;
+
+ nsresult rv = EnsureEditorInitialized();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<Element> rootNode = mRootNode;
+ NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINodeList> nodeList = rootNode->ChildNodes();
+ uint32_t length = nodeList->Length();
+
+ NS_ASSERTION(length <= 2,
+ "We should have one text node and one mozBR at most");
+
+ nsCOMPtr<nsINode> firstNode = nodeList->Item(0);
+ Text* textNode = firstNode ? firstNode->GetAsText() : nullptr;
+
+ if (length == 0) {
+ rootNode.forget(aResult);
+ *aPosition = 0;
+ } else if (textNode) {
+ uint32_t textLength = textNode->Length();
+ firstNode.forget(aResult);
+ *aPosition = std::min(aOffset, textLength);
+ } else {
+ rootNode.forget(aResult);
+ *aPosition = 0;
+ }
+
+ return NS_OK;
+}
+
+/////END INTERFACE IMPLEMENTATIONS
+
+////NSIFRAME
+nsresult nsTextControlFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ if (aAttribute == nsGkAtoms::value && !mEditorHasBeenInitialized) {
+ UpdateValueDisplay(true);
+ return NS_OK;
+ }
+
+ if (aAttribute == nsGkAtoms::maxlength) {
+ if (RefPtr<TextEditor> textEditor = GetTextEditor()) {
+ textEditor->SetMaxTextLength(ControlElement()->UsedMaxLength());
+ return NS_OK;
+ }
+ }
+ return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+}
+
+void nsTextControlFrame::HandleReadonlyOrDisabledChange() {
+ RefPtr<TextControlElement> el = ControlElement();
+ RefPtr<TextEditor> editor = el->GetTextEditorWithoutCreation();
+ if (!editor) {
+ return;
+ }
+ nsISelectionController* selCon = el->GetSelectionController();
+ if (!selCon) {
+ return;
+ }
+ if (el->IsDisabledOrReadOnly()) {
+ if (nsContentUtils::IsFocusedContent(el)) {
+ selCon->SetCaretEnabled(false);
+ }
+ editor->AddFlags(nsIEditor::eEditorReadonlyMask);
+ } else {
+ if (nsContentUtils::IsFocusedContent(el)) {
+ selCon->SetCaretEnabled(true);
+ }
+ editor->RemoveFlags(nsIEditor::eEditorReadonlyMask);
+ }
+}
+
+void nsTextControlFrame::ElementStateChanged(dom::ElementState aStates) {
+ if (aStates.HasAtLeastOneOfStates(dom::ElementState::READONLY |
+ dom::ElementState::DISABLED)) {
+ HandleReadonlyOrDisabledChange();
+ }
+ return nsContainerFrame::ElementStateChanged(aStates);
+}
+
+/// END NSIFRAME OVERLOADS
+
+// NOTE(emilio): This is needed because the root->primary frame map is not set
+// up by the time this is called.
+static nsIFrame* FindRootNodeFrame(const nsFrameList& aChildList,
+ const nsIContent* aRoot) {
+ for (nsIFrame* f : aChildList) {
+ if (f->GetContent() == aRoot) {
+ return f;
+ }
+ if (nsIFrame* root = FindRootNodeFrame(f->PrincipalChildList(), aRoot)) {
+ return root;
+ }
+ }
+ return nullptr;
+}
+
+void nsTextControlFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
+ if (aListID != FrameChildListID::Principal) {
+ return;
+ }
+
+ // Mark the scroll frame as being a reflow root. This will allow incremental
+ // reflows to be initiated at the scroll frame, rather than descending from
+ // the root frame of the frame hierarchy.
+ if (nsIFrame* frame = FindRootNodeFrame(PrincipalChildList(), mRootNode)) {
+ frame->AddStateBits(NS_FRAME_REFLOW_ROOT);
+
+ ControlElement()->InitializeKeyboardEventListeners();
+
+ bool hasProperty;
+ nsPoint contentScrollPos = TakeProperty(ContentScrollPos(), &hasProperty);
+ if (hasProperty) {
+ // If we have a scroll pos stored to be passed to our anonymous
+ // div, do it here!
+ nsIStatefulFrame* statefulFrame = do_QueryFrame(frame);
+ NS_ASSERTION(statefulFrame,
+ "unexpected type of frame for the anonymous div");
+ UniquePtr<PresState> fakePresState = NewPresState();
+ fakePresState->scrollState() = contentScrollPos;
+ statefulFrame->RestoreState(fakePresState.get());
+ }
+ } else {
+ MOZ_ASSERT(!mRootNode || PrincipalChildList().IsEmpty());
+ }
+}
+
+nsresult nsTextControlFrame::UpdateValueDisplay(bool aNotify,
+ bool aBeforeEditorInit,
+ const nsAString* aValue) {
+ if (!IsSingleLineTextControl()) { // textareas don't use this
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mRootNode, "Must have a div content\n");
+ MOZ_ASSERT(!mEditorHasBeenInitialized,
+ "Do not call this after editor has been initialized");
+
+ nsIContent* childContent = mRootNode->GetFirstChild();
+ Text* textContent;
+ if (!childContent) {
+ // Set up a textnode with our value
+ RefPtr<nsTextNode> textNode = new (mContent->NodeInfo()->NodeInfoManager())
+ nsTextNode(mContent->NodeInfo()->NodeInfoManager());
+ textNode->MarkAsMaybeModifiedFrequently();
+ if (IsPasswordTextControl()) {
+ textNode->MarkAsMaybeMasked();
+ }
+ mRootNode->AppendChildTo(textNode, aNotify, IgnoreErrors());
+ textContent = textNode;
+ } else {
+ textContent = childContent->GetAsText();
+ }
+
+ NS_ENSURE_TRUE(textContent, NS_ERROR_UNEXPECTED);
+
+ // Get the current value of the textfield from the content.
+ nsAutoString value;
+ if (aValue) {
+ value = *aValue;
+ } else {
+ ControlElement()->GetTextEditorValue(value);
+ }
+
+ return textContent->SetText(value, aNotify);
+}
+
+NS_IMETHODIMP
+nsTextControlFrame::GetOwnedSelectionController(
+ nsISelectionController** aSelCon) {
+ NS_ENSURE_ARG_POINTER(aSelCon);
+ NS_IF_ADDREF(*aSelCon = GetSelectionController());
+ return NS_OK;
+}
+
+UniquePtr<PresState> nsTextControlFrame::SaveState() {
+ if (nsIStatefulFrame* scrollStateFrame =
+ do_QueryFrame(GetScrollTargetFrame())) {
+ return scrollStateFrame->SaveState();
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+nsTextControlFrame::RestoreState(PresState* aState) {
+ NS_ENSURE_ARG_POINTER(aState);
+
+ // Query the nsIStatefulFrame from the HTMLScrollFrame
+ if (nsIStatefulFrame* scrollStateFrame =
+ do_QueryFrame(GetScrollTargetFrame())) {
+ return scrollStateFrame->RestoreState(aState);
+ }
+
+ // Most likely, we don't have our anonymous content constructed yet, which
+ // would cause us to end up here. In this case, we'll just store the scroll
+ // pos ourselves, and forward it to the scroll frame later when it's created.
+ SetProperty(ContentScrollPos(), aState->scrollState());
+ return NS_OK;
+}
+
+nsresult nsTextControlFrame::PeekOffset(PeekOffsetStruct* aPos) {
+ return NS_ERROR_FAILURE;
+}
+
+void nsTextControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ DO_GLOBAL_REFLOW_COUNT_DSP("nsTextControlFrame");
+
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+
+ // Redirect all lists to the Content list so that nothing can escape, ie
+ // opacity creating stacking contexts that then get sorted with stacking
+ // contexts external to us.
+ nsDisplayList* content = aLists.Content();
+ nsDisplayListSet set(content, content, content, content, content, content);
+
+ for (auto* kid : mFrames) {
+ BuildDisplayListForChild(aBuilder, kid, set);
+ }
+}
+
+NS_IMETHODIMP
+nsTextControlFrame::EditorInitializer::Run() {
+ if (!mFrame) {
+ return NS_OK;
+ }
+
+ // Need to block script to avoid bug 669767.
+ nsAutoScriptBlocker scriptBlocker;
+
+ RefPtr<mozilla::PresShell> presShell = mFrame->PresShell();
+ bool observes = presShell->ObservesNativeAnonMutationsForPrint();
+ presShell->ObserveNativeAnonMutationsForPrint(true);
+ // This can cause the frame to be destroyed (and call Revoke()).
+ mFrame->EnsureEditorInitialized();
+ presShell->ObserveNativeAnonMutationsForPrint(observes);
+
+ // The frame can *still* be destroyed even though we have a scriptblocker,
+ // bug 682684.
+ if (!mFrame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If there is a drag session which is for dragging text in a text control
+ // and its source node is the text control element, we're being reframed.
+ // In this case we should restore the source node of the drag session to
+ // new text node because it's required for dispatching `dragend` event.
+ if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) {
+ if (dragSession->IsDraggingTextInTextControl()) {
+ nsCOMPtr<nsINode> sourceNode;
+ if (NS_SUCCEEDED(
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode))) &&
+ mFrame->GetContent() == sourceNode) {
+ if (TextEditor* textEditor =
+ mFrame->ControlElement()->GetTextEditorWithoutCreation()) {
+ if (Element* anonymousDivElement = textEditor->GetRoot()) {
+ if (anonymousDivElement && anonymousDivElement->GetFirstChild()) {
+ MOZ_ASSERT(anonymousDivElement->GetFirstChild()->IsText());
+ dragSession->UpdateSource(anonymousDivElement->GetFirstChild(),
+ textEditor->GetSelection());
+ }
+ }
+ }
+ }
+ }
+ }
+ // Otherwise, EventStateManager may be tracking gesture to start a drag.
+ else {
+ TextControlElement* textControlElement = mFrame->ControlElement();
+ if (nsPresContext* presContext =
+ textControlElement->GetPresContext(Element::eForComposedDoc)) {
+ if (TextEditor* textEditor =
+ textControlElement->GetTextEditorWithoutCreation()) {
+ if (Element* anonymousDivElement = textEditor->GetRoot()) {
+ presContext->EventStateManager()->TextControlRootAdded(
+ *anonymousDivElement, *textControlElement);
+ }
+ }
+ }
+ }
+
+ mFrame->FinishedInitializer();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsTextControlFrame::nsAnonDivObserver, nsIMutationObserver)
+
+void nsTextControlFrame::nsAnonDivObserver::CharacterDataChanged(
+ nsIContent* aContent, const CharacterDataChangeInfo&) {
+ mFrame.ClearCachedValue();
+}
+
+void nsTextControlFrame::nsAnonDivObserver::ContentAppended(
+ nsIContent* aFirstNewContent) {
+ mFrame.ClearCachedValue();
+}
+
+void nsTextControlFrame::nsAnonDivObserver::ContentInserted(
+ nsIContent* aChild) {
+ mFrame.ClearCachedValue();
+}
+
+void nsTextControlFrame::nsAnonDivObserver::ContentRemoved(
+ nsIContent* aChild, nsIContent* aPreviousSibling) {
+ mFrame.ClearCachedValue();
+}
+
+Maybe<nscoord> nsTextControlFrame::GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext aExportContext) const {
+ if (!IsSingleLineTextControl()) {
+ if (StyleDisplay()->IsContainLayout()) {
+ return Nothing{};
+ }
+
+ if (aBaselineGroup == BaselineSharingGroup::First) {
+ return Some(std::clamp(mFirstBaseline, 0, BSize(aWM)));
+ }
+ // This isn't great, but the content of the root NAC isn't guaranteed
+ // to be loaded, so the best we can do is the edge of the border-box.
+ if (aWM.IsCentralBaseline()) {
+ return Some(BSize(aWM) / 2);
+ }
+ return Some(0);
+ }
+ NS_ASSERTION(!IsSubtreeDirty(), "frame must not be dirty");
+ return GetSingleLineTextControlBaseline(this, mFirstBaseline, aWM,
+ aBaselineGroup);
+}
diff --git a/layout/forms/nsTextControlFrame.h b/layout/forms/nsTextControlFrame.h
new file mode 100644
index 0000000000..0d52e5849f
--- /dev/null
+++ b/layout/forms/nsTextControlFrame.h
@@ -0,0 +1,358 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTextControlFrame_h___
+#define nsTextControlFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "mozilla/TextControlElement.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsIContent.h"
+#include "nsITextControlFrame.h"
+#include "nsIStatefulFrame.h"
+
+class nsISelectionController;
+class EditorInitializerEntryTracker;
+namespace mozilla {
+class AutoTextControlHandlingState;
+class TextEditor;
+class TextControlState;
+enum class PseudoStyleType : uint8_t;
+namespace dom {
+class Element;
+} // namespace dom
+} // namespace mozilla
+
+class nsTextControlFrame : public nsContainerFrame,
+ public nsIAnonymousContentCreator,
+ public nsITextControlFrame,
+ public nsIStatefulFrame {
+ using Element = mozilla::dom::Element;
+
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsTextControlFrame)
+
+ NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(ContentScrollPos, nsPoint)
+
+ protected:
+ nsTextControlFrame(ComputedStyle*, nsPresContext*, nsIFrame::ClassID);
+
+ public:
+ explicit nsTextControlFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsTextControlFrame(aStyle, aPresContext, kClassID) {}
+
+ virtual ~nsTextControlFrame();
+
+ /**
+ * Destroy() causes preparing to destroy editor and that may cause running
+ * selection listeners of spellchecker selection and document state listeners.
+ * Not sure whether the former does something or not, but nobody should run
+ * content script. The latter is currently only FinderHighlighter to clean up
+ * its fields at destruction. Thus, the latter won't run content script too.
+ * Therefore, this won't run unsafe script.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Destroy(DestroyContext&) override;
+
+ nsIScrollableFrame* GetScrollTargetFrame() const override;
+
+ nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ mozilla::LogicalSize ComputeAutoSize(
+ gfxContext* aRenderingContext, mozilla::WritingMode aWM,
+ const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize,
+ const mozilla::LogicalSize& aMargin,
+ const mozilla::LogicalSize& aBorderPadding,
+ const mozilla::StyleSizeOverrides& aSizeOverrides,
+ mozilla::ComputeSizeFlags aFlags) override;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ Maybe<nscoord> GetNaturalBaselineBOffset(
+ mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup,
+ BaselineExportContext aExportContext) const override;
+
+ BaselineSharingGroup GetDefaultBaselineSharingGroup() const override {
+ return BaselineSharingGroup::Last;
+ }
+
+ static Maybe<nscoord> GetSingleLineTextControlBaseline(
+ const nsIFrame* aFrame, nscoord aFirstBaseline, mozilla::WritingMode aWM,
+ BaselineSharingGroup aBaselineGroup) {
+ if (aFrame->StyleDisplay()->IsContainLayout()) {
+ return Nothing{};
+ }
+ NS_ASSERTION(aFirstBaseline != NS_INTRINSIC_ISIZE_UNKNOWN,
+ "please call Reflow before asking for the baseline");
+ return mozilla::Some(aBaselineGroup == BaselineSharingGroup::First
+ ? aFirstBaseline
+ : aFrame->BSize(aWM) - aFirstBaseline);
+ }
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::AccType AccessibleType() override;
+#endif
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ aResult.AssignLiteral("nsTextControlFrame");
+ return NS_OK;
+ }
+#endif
+
+ // nsIAnonymousContentCreator
+ nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
+ void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ void SetInitialChildList(ChildListID, nsFrameList&&) override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ //==== BEGIN NSIFORMCONTROLFRAME
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void SetFocus(bool aOn, bool aRepaint) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+ SetFormProperty(nsAtom* aName, const nsAString& aValue) override;
+
+ //==== END NSIFORMCONTROLFRAME
+
+ //==== NSITEXTCONTROLFRAME
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<mozilla::TextEditor>
+ GetTextEditor() override;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD
+ SetSelectionRange(uint32_t aSelectionStart, uint32_t aSelectionEnd,
+ SelectionDirection = SelectionDirection::None) override;
+ NS_IMETHOD GetOwnedSelectionController(
+ nsISelectionController** aSelCon) override;
+ nsFrameSelection* GetOwnedFrameSelection() override {
+ return ControlElement()->GetConstFrameSelection();
+ }
+ nsISelectionController* GetSelectionController() {
+ return ControlElement()->GetSelectionController();
+ }
+
+ void PlaceholderChanged(const nsAttrValue* aOld, const nsAttrValue* aNew);
+
+ /**
+ * Ensure mEditor is initialized with the proper flags and the default value.
+ * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created
+ * @throws various and sundry other things
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult EnsureEditorInitialized() override;
+
+ //==== END NSITEXTCONTROLFRAME
+
+ //==== NSISTATEFULFRAME
+
+ mozilla::UniquePtr<mozilla::PresState> SaveState() override;
+ NS_IMETHOD RestoreState(mozilla::PresState* aState) override;
+
+ //=== END NSISTATEFULFRAME
+
+ //==== OVERLOAD of nsIFrame
+
+ /** handler for attribute changes to mContent */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult AttributeChanged(
+ int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType) override;
+ void ElementStateChanged(mozilla::dom::ElementState aStates) override;
+
+ nsresult PeekOffset(mozilla::PeekOffsetStruct* aPos) override;
+
+ NS_DECL_QUERYFRAME
+
+ // Whether we should scroll only the current selection into view in the inner
+ // scroller, or also ancestors as needed.
+ enum class ScrollAncestors { No, Yes };
+ void ScrollSelectionIntoViewAsync(ScrollAncestors = ScrollAncestors::No);
+
+ protected:
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void HandleReadonlyOrDisabledChange();
+
+ /**
+ * Launch the reflow on the child frames - see nsTextControlFrame::Reflow()
+ */
+ void ReflowTextControlChild(nsIFrame* aFrame, nsPresContext* aPresContext,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus,
+ ReflowOutput& aParentDesiredSize,
+ nscoord& aButtonBoxISize);
+
+ public:
+ static Maybe<nscoord> ComputeBaseline(const nsIFrame*, const ReflowInput&,
+ bool aForSingleLineControl);
+
+ Element* GetRootNode() const { return mRootNode; }
+
+ Element* GetPreviewNode() const { return mPreviewDiv; }
+
+ Element* GetPlaceholderNode() const { return mPlaceholderDiv; }
+
+ Element* GetRevealButton() const { return mRevealButton; }
+
+ // called by the focus listener
+ nsresult MaybeBeginSecureKeyboardInput();
+ void MaybeEndSecureKeyboardInput();
+
+ mozilla::TextControlElement* ControlElement() const {
+ MOZ_ASSERT(mozilla::TextControlElement::FromNode(GetContent()));
+ return static_cast<mozilla::TextControlElement*>(GetContent());
+ }
+
+#define DEFINE_TEXTCTRL_CONST_FORWARDER(type, name) \
+ type name() const { return ControlElement()->name(); }
+
+ DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsSingleLineTextControl)
+ DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsTextArea)
+ DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsPasswordTextControl)
+ DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetCols)
+ DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetRows)
+
+#undef DEFINE_TEXTCTRL_CONST_FORWARDER
+
+ protected:
+ class EditorInitializer;
+ friend class EditorInitializer;
+ friend class mozilla::AutoTextControlHandlingState; // needs access to
+ // CacheValue
+ friend class mozilla::TextControlState; // needs access to UpdateValueDisplay
+
+ // Temp reference to scriptrunner
+ NS_DECLARE_FRAME_PROPERTY_WITH_DTOR(TextControlInitializer, EditorInitializer,
+ nsTextControlFrame::RevokeInitializer)
+
+ static void RevokeInitializer(EditorInitializer* aInitializer) {
+ aInitializer->Revoke();
+ };
+
+ class EditorInitializer : public mozilla::Runnable {
+ public:
+ explicit EditorInitializer(nsTextControlFrame* aFrame)
+ : mozilla::Runnable("nsTextControlFrame::EditorInitializer"),
+ mFrame(aFrame) {}
+
+ NS_IMETHOD Run() override;
+
+ // avoids use of AutoWeakFrame
+ void Revoke() { mFrame = nullptr; }
+
+ private:
+ nsTextControlFrame* mFrame;
+ };
+
+ nsresult OffsetToDOMPoint(uint32_t aOffset, nsINode** aResult,
+ uint32_t* aPosition);
+
+ /**
+ * Update the textnode under our anonymous div to show the new
+ * value. This should only be called when we have no editor yet.
+ * @throws NS_ERROR_UNEXPECTED if the div has no text content
+ */
+ nsresult UpdateValueDisplay(bool aNotify, bool aBeforeEditorInit = false,
+ const nsAString* aValue = nullptr);
+
+ /**
+ * Find out whether an attribute exists on the content or not.
+ * @param aAtt the attribute to determine the existence of
+ * @returns false if it does not exist
+ */
+ bool AttributeExists(nsAtom* aAtt) const {
+ return mContent && mContent->AsElement()->HasAttr(aAtt);
+ }
+
+ /**
+ * We call this when we are being destroyed or removed from the PFM.
+ * @param aPresContext the current pres context
+ */
+ void PreDestroy();
+
+ // Compute our intrinsic size. This does not include any borders, paddings,
+ // etc. Just the size of our actual area for the text (and the scrollbars,
+ // for <textarea>).
+ mozilla::LogicalSize CalcIntrinsicSize(gfxContext* aRenderingContext,
+ mozilla::WritingMode aWM,
+ float aFontSizeInflation) const;
+
+ private:
+ // helper methods
+ MOZ_CAN_RUN_SCRIPT nsresult SetSelectionInternal(
+ nsINode* aStartNode, uint32_t aStartOffset, nsINode* aEndNode,
+ uint32_t aEndOffset, SelectionDirection = SelectionDirection::None);
+ MOZ_CAN_RUN_SCRIPT nsresult SelectAllOrCollapseToEndOfText(bool aSelect);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetSelectionEndPoints(uint32_t aSelStart, uint32_t aSelEnd,
+ SelectionDirection = SelectionDirection::None);
+
+ void FinishedInitializer() { RemoveProperty(TextControlInitializer()); }
+
+ const nsAString& CachedValue() const { return mCachedValue; }
+
+ void ClearCachedValue() { mCachedValue.SetIsVoid(true); }
+
+ void CacheValue(const nsAString& aValue) { mCachedValue.Assign(aValue); }
+
+ [[nodiscard]] bool CacheValue(const nsAString& aValue,
+ const mozilla::fallible_t& aFallible) {
+ if (!mCachedValue.Assign(aValue, aFallible)) {
+ ClearCachedValue();
+ return false;
+ }
+ return true;
+ }
+
+ protected:
+ class nsAnonDivObserver;
+
+ nsresult CreateRootNode();
+ void CreatePlaceholderIfNeeded();
+ void UpdatePlaceholderText(nsString&, bool aNotify);
+ void CreatePreviewIfNeeded();
+ already_AddRefed<Element> MakeAnonElement(
+ mozilla::PseudoStyleType, Element* aParent = nullptr,
+ nsAtom* aTag = nsGkAtoms::div) const;
+ already_AddRefed<Element> MakeAnonDivWithTextNode(
+ mozilla::PseudoStyleType) const;
+
+ bool ShouldInitializeEagerly() const;
+ void InitializeEagerlyIfNeeded();
+
+ RefPtr<Element> mRootNode;
+ RefPtr<Element> mPlaceholderDiv;
+ RefPtr<Element> mPreviewDiv;
+ // The Reveal Password button. Only used for type=password, nullptr
+ // otherwise.
+ RefPtr<Element> mRevealButton;
+ RefPtr<nsAnonDivObserver> mMutationObserver;
+ // Cache of the |.value| of <input> or <textarea> element without hard-wrap.
+ // If its IsVoid() returns true, it doesn't cache |.value|.
+ // Otherwise, it's cached when setting specific value or getting value from
+ // TextEditor. Additionally, when contents in the anonymous <div> element
+ // is modified, this is cleared.
+ //
+ // FIXME(bug 1402545): Consider using an nsAutoString here.
+ nsString mCachedValue{VoidString()};
+
+ // Our first baseline, or NS_INTRINSIC_ISIZE_UNKNOWN if we have a pending
+ // Reflow (or if we're contain:layout, which means we have no baseline).
+ nscoord mFirstBaseline = NS_INTRINSIC_ISIZE_UNKNOWN;
+
+ // these packed bools could instead use the high order bits on mState, saving
+ // 4 bytes
+ bool mEditorHasBeenInitialized = false;
+ bool mIsProcessing = false;
+
+#ifdef DEBUG
+ bool mInEditorInitialization = false;
+ friend class EditorInitializerEntryTracker;
+#endif
+};
+
+#endif
diff --git a/layout/forms/test/bug287446_subframe.html b/layout/forms/test/bug287446_subframe.html
new file mode 100644
index 0000000000..51fd701e3b
--- /dev/null
+++ b/layout/forms/test/bug287446_subframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script>
+ function doIs(arg1, arg2, arg3) {
+ window.parent.postMessage("t " + encodeURIComponent(arg1) + " " +
+ encodeURIComponent(arg2) + " " +
+ encodeURIComponent(arg3), "*");
+ }
+
+ function $(arg) { return document.getElementById(arg); }
+
+ window.addEventListener("message",
+ function(evt) {
+ var t = $("target");
+ if (evt.data == "start") {
+ doIs(t.value, "Test", "Shouldn't have lost our initial value");
+ t.focus();
+ sendString("Foo");
+ doIs(t.value, "FooTest", "Typing should work");
+ window.parent.postMessage("c", "*");
+ } else {
+ doIs(evt.data, "continue", "Unexpected message");
+ doIs(t.value, "FooTest", "Shouldn't have lost our typed value");
+ sendString("Bar");
+ doIs(t.value, "FooBarTest", "Typing should still work");
+ window.parent.postMessage("f", "*");
+ }
+ },
+ "false");
+
+ </script>
+ </head>
+ <body>
+ <input id="target" value="Test">
+ </body>
+</html>
diff --git a/layout/forms/test/bug477700_subframe.html b/layout/forms/test/bug477700_subframe.html
new file mode 100644
index 0000000000..c0398fb2ad
--- /dev/null
+++ b/layout/forms/test/bug477700_subframe.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script>
+ function doIs(arg1, arg2, arg3) {
+ window.parent.postMessage("t " + encodeURIComponent(arg1) + " " +
+ encodeURIComponent(arg2) + " " +
+ encodeURIComponent(arg3), "*");
+ }
+
+ function $(arg) { return document.getElementById(arg); }
+
+ window.addEventListener("message",
+ function(evt) {
+ doIs(evt.data, "start", "Unexpected message");
+ $("target").focus();
+ sendString("Test");
+ var t = $("target");
+ doIs(t.value, "Test", "Typing should work");
+ (function() {
+ SpecialPowers.wrap(t).editor.undo();
+ })()
+ doIs(t.value, "", "Undo should work");
+ (function() {
+ SpecialPowers.wrap(t).editor.redo();
+ })()
+ doIs(t.value, "Test", "Redo should work");
+ window.parent.postMessage("f", "*");
+ },
+ "false");
+
+ </script>
+ </head>
+ <body>
+ <input id="target">
+ </body>
+</html>
+
diff --git a/layout/forms/test/bug536567_iframe.html b/layout/forms/test/bug536567_iframe.html
new file mode 100644
index 0000000000..b2b2ca60ac
--- /dev/null
+++ b/layout/forms/test/bug536567_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ </head>
+ <body>
+ <iframe id="content"></iframe>
+ </body>
+</html>
+
diff --git a/layout/forms/test/bug536567_subframe.html b/layout/forms/test/bug536567_subframe.html
new file mode 100644
index 0000000000..c1271becf0
--- /dev/null
+++ b/layout/forms/test/bug536567_subframe.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body>
+<input id="target" type="file" />
+<script type="text/javascript">
+
+window.onload = function() {
+ var fileInput = document.getElementById("target");
+ fileInput.click();
+};
+
+</script>
+</body>
+</html>
diff --git a/layout/forms/test/bug564115_window.html b/layout/forms/test/bug564115_window.html
new file mode 100644
index 0000000000..b55cc0400d
--- /dev/null
+++ b/layout/forms/test/bug564115_window.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Window for Bug 564115</title>
+</head>
+<body>
+<input type="text">
+<div style="height: 10000px;"></div>
+</body>
+</html>
diff --git a/layout/forms/test/chrome.toml b/layout/forms/test/chrome.toml
new file mode 100644
index 0000000000..e37aa39571
--- /dev/null
+++ b/layout/forms/test/chrome.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = [
+ "bug536567_iframe.html",
+ "bug536567_subframe.html",
+]
+
+["test_bug536567_perwindowpb.html"]
diff --git a/layout/forms/test/mochitest.toml b/layout/forms/test/mochitest.toml
new file mode 100644
index 0000000000..0748041524
--- /dev/null
+++ b/layout/forms/test/mochitest.toml
@@ -0,0 +1,121 @@
+[DEFAULT]
+support-files = [
+ "bug287446_subframe.html",
+ "bug477700_subframe.html",
+ "bug564115_window.html",
+]
+
+["test_bug231389.html"]
+
+["test_bug287446.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug345267.html"]
+
+["test_bug346043.html"]
+
+["test_bug348236.html"]
+skip-if = ["true"] # mac(select form control popup behavior is different)
+
+["test_bug353539.html"]
+
+["test_bug365410.html"]
+
+["test_bug378670.html"]
+
+["test_bug402198.html"]
+
+["test_bug411236.html"]
+
+["test_bug446663.html"]
+
+["test_bug476308.html"]
+
+["test_bug477531.html"]
+
+["test_bug477700.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug534785.html"]
+
+["test_bug542914.html"]
+
+["test_bug549170.html"]
+
+["test_bug562447.html"]
+
+["test_bug563642.html"]
+
+["test_bug564115.html"]
+skip-if = ["os == 'android'"] #TIMED_OUT
+
+["test_bug571352.html"]
+skip-if = ["os == 'android'"] #TIMED_OUT
+
+["test_bug572406.html"]
+
+["test_bug572649.html"]
+skip-if = ["os == 'android'"] # Bug 1635771
+
+["test_bug595310.html"]
+
+["test_bug620936.html"]
+
+["test_bug644542.html"]
+
+["test_bug672810.html"]
+
+["test_bug704049.html"]
+
+["test_bug717878_input_scroll.html"]
+
+["test_bug869314.html"]
+
+["test_bug903715.html"]
+skip-if = ["true"]
+
+["test_bug935876.html"]
+
+["test_bug957562.html"]
+
+["test_bug960277.html"]
+
+["test_bug1111995.html"]
+
+["test_bug1301290.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug1305282.html"]
+
+["test_bug1327129.html"]
+
+["test_bug1529036.html"]
+
+["test_listcontrol_search.html"]
+
+["test_readonly.html"]
+
+["test_select_collapsed_page_keys.html"]
+skip-if = ["os == 'mac'"] # select control keyboard behavior is different
+
+["test_select_key_navigation_bug961363.html"]
+
+["test_select_key_navigation_bug1498769.html"]
+
+["test_select_prevent_default.html"]
+
+["test_select_reframe.html"]
+
+["test_select_vertical.html"]
+skip-if = ["true"] # Bug 1170129, # <select> elements don't use an in-page popup on Android
+
+["test_textarea_resize.html"]
+skip-if = ["os == 'android'"]
+
+["test_unstyled_control_height.html"]
diff --git a/layout/forms/test/test_bug1111995.html b/layout/forms/test/test_bug1111995.html
new file mode 100644
index 0000000000..c164dc23f7
--- /dev/null
+++ b/layout/forms/test/test_bug1111995.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1111995
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1111995</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1111995 **/
+
+ function doTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ var clicks = 0;
+ var elms = document.querySelectorAll('.click');
+ for (var i = 0; i < elms.length; ++i) {
+ var e = elms[i];
+ e.addEventListener('click', function(event) {
+ ++clicks;
+ });
+ }
+
+ for (var i = 0; i < elms.length; ++i) {
+ var e = elms[i];
+ synthesizeMouse(e, 3, 3, {});
+ }
+ is(clicks, 0, "click events outside border with radius");
+
+ clicks = 0;
+ synthesizeMouse($("t3"), 17, 17, {});
+ synthesizeMouse($("t4"), 17, 17, {});
+ is(clicks, 2, "click events on border with radius");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="SimpleTest.waitForFocus(doTest, window)">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1111995">Mozilla Bug 1111995</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<input class="click" id="t1" type=button style="width:100px; height:100px; padding:20px; border-radius:50%" value="Round button">
+<button class="click" id="t2" style="width:100px; height:100px; padding:20px; border-radius:50%">Round button</button>
+<input class="click" id="t3" type=button style="width:100px; height:100px; border-width:20px; border-radius:50%" value="Round button">
+<button class="click" id="t4" style="width:100px; height:100px; border-width:20px; border-radius:50%">Round button</button>
+<input class="click" id="t5" type=button style="width:100px; height:100px; border-radius:50%;overflow:hidden" value="Round button">
+<button class="click" id="t6" style="width:100px; height:100px; border-radius:50%;overflow:hidden">Round button</button>
+
+</body>
+</html>
diff --git a/layout/forms/test/test_bug1301290.html b/layout/forms/test/test_bug1301290.html
new file mode 100644
index 0000000000..6b3fc0df14
--- /dev/null
+++ b/layout/forms/test/test_bug1301290.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for Bug 1301290</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ .blue, .green {
+ border: none;
+ box-sizing: border-box;
+ display: block;
+ width: 200px;
+ height: 100px;
+ overflow: scroll;
+ resize: both;
+ }
+
+ .blue {
+ background: blue;
+ }
+
+ .green {
+ background: green;
+ margin-top: -100px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="blue"></div>
+ <textarea class="green" id="textarea"></textarea>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(() => SimpleTest.executeSoon(function() {
+ var textarea = $("textarea");
+ var rect = textarea.getBoundingClientRect();
+
+ synthesizeMouse(textarea, rect.width - 9, rect.height - 9, { type: "mousedown" });
+ synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mousemove" });
+ synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mouseup" });
+
+ var newrect = textarea.getBoundingClientRect();
+ ok(newrect.width > rect.width, "width did not increase");
+ ok(newrect.height > rect.height, "height did not increase");
+ SimpleTest.finish();
+ }));
+ </script>
+ </body>
+</html>
diff --git a/layout/forms/test/test_bug1305282.html b/layout/forms/test/test_bug1305282.html
new file mode 100644
index 0000000000..bd631f3444
--- /dev/null
+++ b/layout/forms/test/test_bug1305282.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=615697
+-->
+<head>
+ <title>Test for Bug 1305282</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1305282">Mozilla Bug 1305282</a>
+<p id="display"></p>
+<div id="content">
+ <select>
+ <option>f o o</option>
+ <option>b a r</option>
+ <option>b o o</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1305282 **/
+
+var select = document.getElementsByTagName('select')[0];
+
+select.addEventListener("change", function(aEvent) {
+ is(select.selectedIndex, 1, "'b a r' option is selected");
+ SimpleTest.finish();
+}, {once: true});
+
+select.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ synthesizeKey("KEY_ArrowDown");
+ SimpleTest.executeSoon(function () {
+ sendString("b");
+ SimpleTest.executeSoon(function () {
+ sendString(" ");
+ SimpleTest.executeSoon(function () {
+ synthesizeKey("KEY_Enter");
+ });
+ });
+ });
+ });
+}, {once: true});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ select.focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug1327129.html b/layout/forms/test/test_bug1327129.html
new file mode 100644
index 0000000000..0e52f140b3
--- /dev/null
+++ b/layout/forms/test/test_bug1327129.html
@@ -0,0 +1,385 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935876
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1327129</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1327129">Mozilla Bug 1327129</a>
+<p id="display"></p>
+<div>
+<select size="3" firstNonDisabledIndex="0">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" firstNonDisabledIndex="1">
+ <option disabled>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" firstNonDisabledIndex="0">
+ <optgroup><option>1</option></optgroup>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" firstNonDisabledIndex="1">
+ <option disabled>1</option>
+ <optgroup><option>2</option></optgroup>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" firstNonDisabledIndex="2">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" firstNonDisabledIndex="-1">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option disabled>3</option>
+ <option disabled>4</option>
+ <option disabled>5</option>
+</select>
+<select size="3" multiple firstNonDisabledIndex="0">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" multiple firstNonDisabledIndex="0">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" multiple firstNonDisabledIndex="0">
+ <optgroup><option>1</option></optgroup>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" multiple firstNonDisabledIndex="1">
+ <option disabled>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" multiple firstNonDisabledIndex="3">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option disabled>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="3" multiple firstNonDisabledIndex="-1">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option disabled>3</option>
+ <option disabled>4</option>
+ <option disabled>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="0">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="0">
+ <optgroup><option>1</option></optgroup>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="1">
+ <optgroup disabled><option>1</option></optgroup>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="1">
+ <option disabled>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="1">
+ <option disabled>1</option>
+ <optgroup><option>2</option></optgroup>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="2">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="3">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option disabled>3</option>
+ <option>4</option>
+ <option>5</option>
+</select>
+<select size="1" firstNonDisabledIndex="-1">
+ <option disabled>1</option>
+ <optgroup><option disabled>2</option></optgroup>
+ <option disabled>3</option>
+ <option disabled>4</option>
+ <option disabled>5</option>
+</select>
+</div>
+<pre id="test">
+</pre>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+
+function runTests()
+{
+ const all = Array.from(document.querySelectorAll('select'));
+ let i = 0;
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ ++i;
+ if (!elem.id)
+ elem.id = "element " + i;
+ });
+
+ //
+ // Test DOWN key on a <select> with no selected options.
+ //
+ const listboxes = Array.from(document.querySelectorAll('select[size="3"]'));
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": DOWN selected first non-disabled option");
+ });
+
+ const comboboxes = Array.from(document.querySelectorAll('select[size="1"]'));
+ // Mac shows the drop-down menu for DOWN, so skip this test there.
+ if (!kIsMac) {
+ comboboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": DOWN selected first non-disabled option");
+ });
+ }
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test SHIFT+DOWN on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown", {shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": SHIFT+DOWN selected first non-disabled option");
+ });
+
+ // Mac shows the drop-down menu for SHIFT+DOWN, so skip this test there.
+ if (!kIsMac) {
+ comboboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown", {shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": SHIFT+DOWN selected first non-disabled option");
+ });
+ }
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test CTRL+DOWN on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown", {ctrlKey:true});
+ if (!elem.multiple)
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+DOWN selected first non-disabled option");
+ else
+ is(elem.selectedIndex, -1, elem.id + ": CTRL+DOWN did NOT select first any option");
+ });
+
+ // Mac shows the drop-down menu for CTRL+DOWN, so skip this test there.
+ if (!kIsMac) {
+ comboboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown", {ctrlKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+DOWN selected first non-disabled option");
+ });
+ }
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test SPACE on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ sendString(" ");
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": SPACE selected first non-disabled option");
+ });
+
+ // All platforms shows the drop-down menu for SPACE so skip testing that
+ // on the comboboxes.
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test CTRL+SPACE on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey(" ", {ctrlKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+SPACE selected first non-disabled option");
+ });
+
+ // non-Mac shows the drop-down menu for CTRL+SPACE, so skip this test there.
+ if (kIsMac) {
+ comboboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey(" ", {ctrlKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+SPACE selected first non-disabled option");
+ });
+ }
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test SHIFT+SPACE on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey(" ", {shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": SHIFT+SPACE selected first non-disabled option");
+ });
+
+ // All platforms shows the drop-down menu for SHIFT+SPACE so skip testing that
+ // on the comboboxes.
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test CTRL+SHIFT+DOWN on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown", {ctrlKey:true, shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+SHIFT+DOWN selected first non-disabled option");
+ });
+
+ // Mac shows the drop-down menu for CTRL+SHIFT+DOWN, so skip this test there.
+ if (!kIsMac) {
+ comboboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey("KEY_ArrowDown", {ctrlKey:true, shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+SHIFT+DOWN selected first non-disabled option");
+ });
+ }
+
+ all.forEach((elem) => {
+ elem.selectedIndex = -1;
+ elem.blur();
+ });
+
+ //
+ // Test CTRL+SHIFT+SPACE on a <select> with no selected options.
+ //
+ listboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey(" ", {ctrlKey:true, shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+SHIFT+SPACE selected first non-disabled option");
+ });
+
+ // non-Mac shows the drop-down menu for CTRL+SHIFT+SPACE, so skip this test there.
+ if (kIsMac) {
+ comboboxes.forEach((elem) => {
+ elem.focus();
+ synthesizeKey(" ", {ctrlKey:true, shiftKey:true});
+ is(""+elem.selectedIndex,
+ elem.getAttribute('firstNonDisabledIndex'),
+ elem.id + ": CTRL+SHIFT+SPACE selected first non-disabled option");
+ });
+ }
+
+ SimpleTest.finish();
+}
+
+
+SimpleTest.waitForFocus(runTests);
+</script>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug1529036.html b/layout/forms/test/test_bug1529036.html
new file mode 100644
index 0000000000..0d1c4fa207
--- /dev/null
+++ b/layout/forms/test/test_bug1529036.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1529036
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1529036</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+html,body {
+ color:black; background-color:white; font:16px/1 monospace; padding:0; margin:0;
+}
+ </style>
+ <script type="application/javascript">
+
+ /** Test for Bug 1529036 **/
+
+ function doTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ var clicks = 0;
+ var elms = document.querySelectorAll('.click');
+ for (var i = 0; i < elms.length; ++i) {
+ var e = elms[i];
+ e.addEventListener('click', function(event) {
+ ++clicks;
+ });
+ }
+
+ var elms = document.querySelectorAll('.click.hit');
+ for (var i = 0; i < elms.length; ++i) {
+ var e = elms[i];
+ let r = e.getBoundingClientRect();
+ synthesizeMouse(e, 50, 50, {});
+ }
+ is(clicks, elms.length, "click events on overflow");
+
+ clicks = 0;
+ elms = document.querySelectorAll('.click.nohit');
+ for (var i = 0; i < elms.length; ++i) {
+ var e = elms[i];
+ let r = e.getBoundingClientRect();
+ synthesizeMouse(e, 50, 50, {});
+ }
+ is(clicks, 0, "click events on clipped overflow");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body onload="SimpleTest.waitForFocus(doTest, window)">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1529036">Mozilla Bug 1529036</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<div style="border: 1px solid">
+<button class="click hit" id="t1" style="width:10px; height:10px; padding:20px; border-radius:50%"><div>button<br>button<br>button<br>button<br>button<br></div></button>
+<button class="click hit" id="t2" style="width:10px; height:10px; padding:20px; border:1px solid"><div>button<br>button<br>button<br>button<br>button<br></div></button>
+<button class="click hit" id="t3" style="width:10px; height:10px; border-width:20px; border-radius:50%"><div>button<br>button<br>button<br>button<br>button<br></div></button>
+<button class="click hit" id="t4" style="width:10px; height:10px; border:20px solid"><div>button<br>button<br>button<br>button<br>button<br></div></button>
+<button class="click nohit" id="t5" style="width:10px; height:10px; padding:20px; overflow:hidden; border-radius:50%"><div>button<br>button<br>button<br>button<br>button<br></div></button>
+<button class="click nohit" id="t6" style="width:10px; height:10px; padding:20px; overflow:hidden; border:1px solid"><div>button<br>button<br>button<br>button<br>button<br></div></button>
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/test/test_bug231389.html b/layout/forms/test/test_bug231389.html
new file mode 100644
index 0000000000..9239839247
--- /dev/null
+++ b/layout/forms/test/test_bug231389.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=231389
+-->
+<head>
+ <title>Test for Bug 231389</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=231389">Mozilla Bug 231389</a>
+<p id="display">
+ <textarea id="area" rows="5">
+ Here
+ is
+ some
+ very
+ long
+ text
+ that
+ we're
+ using
+ for
+ testing
+ purposes
+ </textarea>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 231389 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var area = document.getElementById("area");
+ var val = area.value;
+ var pos = val.indexOf("purposes");
+
+ is(area.scrollTop, 0, "The textarea should not be scrolled initially");
+ area.selectionStart = pos;
+ area.selectionEnd = pos;
+ requestAnimationFrame(function() {
+ isnot(area.scrollTop, 0, "The textarea's insertion point should be scrolled into view");
+
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug287446.html b/layout/forms/test/test_bug287446.html
new file mode 100644
index 0000000000..bb9e3bec39
--- /dev/null
+++ b/layout/forms/test/test_bug287446.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=287446
+-->
+<head>
+ <title>Test for Bug 287446</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=287446">Mozilla Bug 287446</a>
+<p id="display">
+ <iframe id="i"
+ src="http://example.com/tests/layout/forms/test/bug287446_subframe.html"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 287446 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ isnot(window.location.host, "example.com", "test is not testing cross-site");
+ var accessed = false;
+ try {
+ $("i").contentDocument.documentElement;
+ accessed = true;
+ } catch(e) {}
+ is(accessed, false, "Shouldn't be able to access cross-site");
+
+ $("i").style.display = "none";
+ document.body.offsetWidth;
+ is(document.defaultView.getComputedStyle($("i")).display, "none",
+ "toggling display failed");
+ $("i").style.display = "";
+ document.body.offsetWidth;
+ is(document.defaultView.getComputedStyle($("i")).display, "inline",
+ "toggling display back failed");
+
+ $("i").contentWindow.postMessage("start", "*");
+});
+
+function continueTest() {
+ $("i").style.display = "none";
+ document.body.offsetWidth;
+ is(document.defaultView.getComputedStyle($("i")).display, "none",
+ "toggling display second time failed");
+ $("i").style.display = "";
+ document.body.offsetWidth;
+ is(document.defaultView.getComputedStyle($("i")).display, "inline",
+ "toggling display back second time failed");
+
+$("i").contentWindow.postMessage("continue", "*");
+}
+
+window.addEventListener("message",
+ function(evt) {
+ var arr = evt.data.split(/ /).map(decodeURIComponent);
+ if (arr[0] == 't') {
+ is(arr[1], arr[2], arr[3]);
+ } else if (arr[0] == 'c') {
+ continueTest();
+ } else if (arr[0] == 'f') {
+ SimpleTest.finish();
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug345267.html b/layout/forms/test/test_bug345267.html
new file mode 100644
index 0000000000..ba9a3bd555
--- /dev/null
+++ b/layout/forms/test/test_bug345267.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345267
+-->
+<head>
+ <title>Test for Bug 345267</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345267">Mozilla Bug 345267</a>
+<p id="display">
+ <input id="d1" maxlength="3" value="abcde">
+ <input id="d2" maxlength="3">
+ <input id="d3" maxlength="3">
+ <input id="d4" value="abcdefghijk">
+ <input id="target" value="abcdefghijklm" maxlength="3">
+</p>
+<div id="content" style="display: none">
+ <input id="u1" maxlength="3" value="abcdef">
+ <input id="u2" maxlength="3">
+ <input id="u3" maxlength="3">
+ <input id="u4" value="abcdefghijkl">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 345267 **/
+SimpleTest.waitForExplicitFinish();
+runTest();
+
+function runTest() {
+ is($("d1").value, "abcde",
+ "Displayed initial value should not be truncated by maxlength");
+ is($("u1").value, "abcdef",
+ "Undisplayed initial value should not be truncated by maxlength");
+
+ $("d2").value = "abcdefg";
+ is($("d2").value, "abcdefg",
+ "Displayed set value should not be truncated by maxlength");
+
+ $("u2").value = "abcdefgh";
+ is($("u2").value, "abcdefgh",
+ "Undisplayed set value should not be truncated by maxlength");
+
+ $("d3").defaultValue = "abcdefghi";
+ is($("d3").value, "abcdefghi",
+ "Displayed set defaultValue should not be truncated by maxlength");
+
+ $("u3").defaultValue = "abcdefghij";
+ is($("u3").value, "abcdefghij",
+ "Undisplayed set defaultValue should not be truncated by maxlength");
+
+ $("d4").maxLength = "3";
+ is($("d4").value, "abcdefghijk",
+ "Displayed: setting maxLength should not truncate existing value");
+
+ $("u4").maxLength = "3";
+ is($("u4").value, "abcdefghijkl",
+ "Undisplayed: setting maxLength should not truncate existing value");
+
+ // Now start the editing tests
+ is($("target").value, "abcdefghijklm", "Test starting state incorrect");
+ $("target").focus();
+ $("target").selectionStart = $("target").selectionEnd = 13;
+ sendKey("back_space");
+ is($("target").value, "abcdefghijkl", "Should only delete one char");
+ sendKey("back_space");
+ is($("target").value, "abcdefghijk", "Should only delete one char again");
+ (function () {
+ SpecialPowers.wrap($("target")).controllers.getControllerForCommand('cmd_undo')
+ .doCommand('cmd_undo');
+ })();
+ is($("target").value, "abcdefghijklm",
+ "Should be able to undo deletion in the face of maxlength");
+ sendString("nopq");
+ is($("target").value, "abcdefghijklm",
+ "Typing should have no effect when already past maxlength");
+
+ $("target").value = "";
+ sendString("abcde");
+ is($("target").value, "abc", "Typing should be limited by maxlength");
+
+ $("target").value = "";
+ sendString("ad");
+ sendKey("left");
+ sendString("bc");
+ is($("target").value, "abd", "Typing should be limited by maxlength again");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/layout/forms/test/test_bug346043.html b/layout/forms/test/test_bug346043.html
new file mode 100644
index 0000000000..e5db9bb8cf
--- /dev/null
+++ b/layout/forms/test/test_bug346043.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=346043
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 346043</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 346043 **/
+ function test(select, index, useEscape) {
+ select.selectedIndex = index;
+ is(select.selectedIndex, index, "Selected index is broken");
+
+ // Open the select dropdown.
+ //
+ // Using Alt+Down (instead of a click) seems necessary to make subsequent
+ // mouse events work with the dropdown itself, instead of the window below
+ // it.
+ select.focus();
+ synthesizeKey("KEY_ArrowDown", {altKey: true});
+
+ var options = select.getElementsByTagName("option");
+ synthesizeMouseAtCenter(options[1],
+ {type: "mousemove", clickCount: 0});
+
+ // Close the select dropdown.
+ if (useEscape)
+ synthesizeKey("KEY_Escape"); // Tests a different code path.
+ select.blur();
+ is(select.selectedIndex, index, "Selected index shouldn't change");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ test(document.getElementById("test-unselected"), -1, true);
+ test(document.getElementById("test-unselected"), -1, false);
+ test(document.getElementById("test-disabled"), 0, true);
+ test(document.getElementById("test-disabled"), 0, false);
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=346043">Mozilla Bug 346043</a>
+ <p id="display"></p>
+ <div id="content">
+ <select id="test-unselected">
+ <option>A</option>
+ <option>B</option>
+ <option>C</option>
+ </select>
+
+ <select id="test-disabled">
+ <option disabled>A</option>
+ <option>B</option>
+ <option>C</option>
+ </select>
+ </div>
+ <pre id="test"></pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug348236.html b/layout/forms/test/test_bug348236.html
new file mode 100644
index 0000000000..f00f89efa7
--- /dev/null
+++ b/layout/forms/test/test_bug348236.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=348236
+-->
+<head>
+
+ <title>Test for Bug 348236</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <style type="text/css">
+ #eSelect {
+ position: fixed; top:0; left: 350px; font-size: 24px; width: 100px
+ }
+ #eSelect option {
+ margin: 0; padding: 0; height: 24px
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=348236">Mozilla Bug 348236</a>
+<p id="display"></p>
+<div id="content">
+
+ <select id="eSelect" size="1" onchange="++this.onchangeCount">
+ <option selected>1</option>
+ <option>2</option>
+ <option id="option3">3</option>
+ </select>
+</div>
+<pre id="test">
+<script type="text/javascript">
+
+ /** Test for Bug 348236 **/
+
+SimpleTest.waitForExplicitFinish()
+addLoadEvent(function test() {
+
+ var
+ WinUtils = SpecialPowers.getDOMWindowUtils(window),
+ sec = netscape.security,
+ eSelect = $("eSelect"),
+ timeout = 0 // Choose a larger value like 500 ms if you want to see what's happening.
+
+ function keypressOnSelect(key) {
+ eSelect.focus();
+ synthesizeKey(key.key, {altKey: key.altKey});
+ }
+
+ function testKey(key, keyString, functionToContinue) {
+ var selectGotClick
+ function clickListener() { selectGotClick = true }
+ eSelect.selectedIndex = 0
+ eSelect.onchangeCount = 0
+
+ // Drop the SELECT down.
+ keypressOnSelect(key)
+ // This timeout and the following are necessary to let the sent events take effect.
+ setTimeout(cont1, timeout)
+ function cont1() {
+ // Move the mouse over option 3.
+ let option3 = document.getElementById("option3");
+ let rect = option3.getBoundingClientRect();
+ WinUtils.sendMouseEvent("mousemove", rect.left + 4, rect.top + 4, 0, 0, 0, true)
+ setTimeout(cont2, timeout)
+ }
+ function cont2() {
+ // Close the select.
+ keypressOnSelect(key)
+ setTimeout(cont3, timeout)
+ }
+ function cont3() {
+ is(eSelect.value, "3", "Select's value should be 3 after hovering over option 3 and pressing " + keyString + ".")
+ is(eSelect.onchangeCount, 1, "Onchange should have fired once.")
+
+ // Simulate click on area to the left of the select.
+ eSelect.addEventListener("click", clickListener, true)
+ selectGotClick = false
+ WinUtils.sendMouseEvent("mousedown", 320, 0, 0, 0, 0, true)
+ WinUtils.sendMouseEvent("mouseup", 320, 0, 0, 0, 0, true)
+ setTimeout(cont4, timeout)
+ }
+ function cont4() {
+ eSelect.removeEventListener("click", clickListener, true)
+ ok(!selectGotClick, "SELECT must not capture mouse events after closing it with " + keyString + ".")
+ functionToContinue()
+ }
+ }
+
+
+ // Quick sanity checks.
+ is(eSelect.value, "1", "SELECT value should be 1 after load.")
+ is(eSelect.selectedIndex, 0, "SELECT selectedIndex should be 0 after load.")
+
+ // Check if sending key events works.
+ keypressOnSelect({key: "KEY_ArrowDown"});
+ is(eSelect.value, "2", "SELECT value should be 2 after pressing Down.")
+
+ // Test ALT-Down.
+ testKey({key: "KEY_ArrowDown", altKey: true}, "ALT-Down", nextKey1)
+ function nextKey1() {
+ // Test ALT-Up.
+ testKey({key: "KEY_ArrowUp", altKey: true}, "ALT-Up", nextKey2)
+ }
+ function nextKey2() {
+ // Test the F4 key on Windows.
+ if (/Win/i.test(navigator.platform))
+ testKey({key: "KEY_F4"}, "F4", finished)
+ else
+ finished()
+ }
+ function finished() {
+ // Reset value to get the expected value if we reload the page.
+ eSelect.selectedIndex = 0
+ SimpleTest.finish()
+ }
+})
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug353539.html b/layout/forms/test/test_bug353539.html
new file mode 100644
index 0000000000..593250d207
--- /dev/null
+++ b/layout/forms/test/test_bug353539.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=353539
+-->
+<head>
+ <title>Test for Bug 353539</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=353539">Mozilla Bug 353539</a>
+<p id="display">
+ <textarea id="area" rows="5">
+ Here
+ is
+ some
+ very
+ long
+ text
+ that
+ we're
+ using
+ for
+ testing
+ purposes
+ </textarea>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 353539 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var area = document.getElementById("area");
+
+ is(area.scrollTop, 0, "The textarea should not be scrolled initially");
+ area.focus();
+ setTimeout(function() {
+ is(area.scrollTop, 0, "The textarea's insertion point should not be scrolled into view");
+
+ SimpleTest.finish();
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug365410.html b/layout/forms/test/test_bug365410.html
new file mode 100644
index 0000000000..94ba8abfa0
--- /dev/null
+++ b/layout/forms/test/test_bug365410.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=365410
+-->
+<title>Test for Bug 365410</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<style>
+ select { box-sizing: content-box }
+</style>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=365410">Mozilla Bug 365410</a>
+<p id="display">
+<select id="test0" multiple="multiple">
+ <option id="option">Item 1</option>
+ <option>Item 2</option>
+ <option>Item 3</option>
+ <option>Item 4</option>
+ <option>Item 5</option>
+ <option>Item 6</option>
+ <option>Item 7</option>
+ <option>Item 8</option>
+ <option>Item 9</option>
+ <option>Item 10</option>
+ <option>Item 11</option>
+ <option>Item 12</option>
+ <option>Item 13</option>
+ <option>Item 14</option>
+ <option>Item 15</option>
+</select>
+<select id="test1" multiple="multiple" size="1">
+ <option>Item 1</option>
+ <option>Item 2</option>
+ <option>Item 3</option>
+ <option>Item 4</option>
+ <option>Item 5</option>
+ <option>Item 6</option>
+ <option>Item 7</option>
+ <option>Item 8</option>
+ <option>Item 9</option>
+ <option>Item 10</option>
+ <option>Item 11</option>
+ <option>Item 12</option>
+ <option>Item 13</option>
+ <option>Item 14</option>
+ <option>Item 15</option>
+</select>
+<select id="test2" multiple="multiple" size="1" style="height:0.9em">
+ <option>Item 1</option>
+ <option>Item 2</option>
+ <option>Item 3</option>
+ <option>Item 4</option>
+ <option>Item 5</option>
+ <option>Item 6</option>
+ <option>Item 7</option>
+ <option>Item 8</option>
+ <option>Item 9</option>
+ <option>Item 10</option>
+ <option>Item 11</option>
+ <option>Item 12</option>
+ <option>Item 13</option>
+ <option>Item 14</option>
+ <option>Item 15</option>
+</select>
+<select id="test3" multiple="multiple" size="1"></select>
+<select id="test4" multiple="multiple" size="1" style="height:0.9em"></select>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 365410 **/
+
+function pageUpDownTest(id,index) {
+ var elm = document.getElementById(id);
+ elm.focus();
+ elm.selectedIndex = 0;
+ sendKey("page_down");
+ sendKey("page_down");
+ sendKey("page_up");
+ sendKey("page_down");
+ is(elm.selectedIndex, index, "pageUpDownTest: selectedIndex for " + id + " is " + index);
+}
+
+function upDownTest(id,index) {
+ var elm = document.getElementById(id);
+ elm.focus();
+ elm.selectedIndex = 0;
+ sendKey("down");
+ sendKey("down");
+ sendKey("up");
+ sendKey("down");
+ is(elm.selectedIndex, index, "upDownTest: selectedIndex for " + id + " is " + index);
+}
+
+function setHeight(id, h) {
+ var elm = document.getElementById(id);
+ elm.style.height = h + 'px';
+}
+
+function runTest() {
+ var h = document.getElementById("option").clientHeight;
+ var list5itemsHeight = h * 5.5;
+ setHeight("test0", list5itemsHeight);
+ setHeight("test1", list5itemsHeight);
+ setHeight("test3", list5itemsHeight);
+
+ pageUpDownTest("test0",8);
+ pageUpDownTest("test1",8);
+ pageUpDownTest("test2",2);
+ pageUpDownTest("test3",-1);
+ pageUpDownTest("test4",-1);
+ upDownTest("test0",2);
+ upDownTest("test1",2);
+ upDownTest("test2",2);
+ upDownTest("test3",-1);
+ upDownTest("test4",-1);
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug378670.html b/layout/forms/test/test_bug378670.html
new file mode 100644
index 0000000000..f039cf35d6
--- /dev/null
+++ b/layout/forms/test/test_bug378670.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=378670
+-->
+<head>
+ <title>Test for Bug 378670</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=378670">Mozilla Bug 378670</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+Clicking on the select should not crash Mozilla
+<select id="select">
+<option>1</option>
+<option>2</option>
+</select>
+
+<pre id="test">
+<script>
+document.body.addEventListener('popupshowing', function(e) {e.target.remove() }, true);
+</script>
+<script type="application/javascript">
+
+/** Test for Bug 378670 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+function clickit() {
+ var select = document.getElementById('select');
+
+ sendMouseEvent({type:'mousedown'}, select);
+ sendMouseEvent({type:'mouseup'}, select);
+ sendMouseEvent({type:'click'}, select);
+
+ setTimeout(finish, 200);
+}
+
+window.addEventListener('load', clickit);
+
+function finish()
+{
+ ok(true, "This is a mochikit version of a crash test. To complete is to pass.");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug402198.html b/layout/forms/test/test_bug402198.html
new file mode 100644
index 0000000000..f319a11976
--- /dev/null
+++ b/layout/forms/test/test_bug402198.html
@@ -0,0 +1,77 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=402198
+-->
+<head>
+ <title>Test for Bug 402198</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402198">Mozilla Bug 402198</a>
+<p id="display">
+ <select></select>
+ <select><optgroup></optgroup></select>
+ <legend style="overflow: scroll;">
+ <select></select>
+ </legend>
+ <span></span>
+ <span style="display: -moz-box;">
+ <select></select>
+ </span>
+ <legend style=" ">
+ <label style="overflow: scroll; display: -moz-box;">
+ <select></select>
+ </label>
+ </legend>
+ <legend>
+ <label style=" display: table;">
+ <select id="a">
+ <option>High Grade</option>
+ <option>Medium Grade</option>
+ </select>
+ </label>
+ </legend>
+
+ <input>
+ <select multiple="multiple"></select>
+ <select style="overflow: scroll; display: -moz-box;">
+ <optgroup></optgroup>
+ <optgroup style="display: table-cell;"></optgroup>
+ </select>
+</p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function doe3() {
+ document.documentElement.style.display = 'none';
+ document.body.offsetHeight;
+ document.documentElement.style.display = '';
+ document.body.offsetHeight;
+
+ document.getElementById('a').focus();
+ document.body.style.display = 'none';
+
+ synthesizeKey('KEY_Tab', {shiftKey: true});
+
+ is(0, 0, "this is a crash/assertion test, so we're ok if we survived this far");
+ setTimeout(function() {document.body.style.display = ''; SimpleTest.finish();}, 0);
+}
+
+function do_test() {
+ setTimeout(doe3,300);
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+addLoadEvent(do_test);
+</script>
+</pre>
+
+<style>
+* {quotes: "quote" "quote" !important;}
+</style>
+
+</body>
+</html>
diff --git a/layout/forms/test/test_bug411236.html b/layout/forms/test/test_bug411236.html
new file mode 100644
index 0000000000..b2e2bee380
--- /dev/null
+++ b/layout/forms/test/test_bug411236.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=411236
+-->
+<head>
+ <title>Test for Bug 411236</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=411236">Mozilla Bug 411236</a>
+<p id="display"></p>
+<div id="content">
+ <input type="file" onfocus="window.oTarget = event.originalTarget;"
+ onclick="window.fileInputGotClick = true; return false;"
+ id="fileinput">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 411236 **/
+
+window.oTarget = null;
+window.fileInputGotClick = false;
+
+function test() {
+ // Try to find the '<input>' using tabbing.
+ var i = 0;
+ while (!window.oTarget && i < 100) {
+ ++i;
+ synthesizeKey("KEY_Tab");
+ }
+
+ if (i >= 100) {
+ ok(false, "Couldn't find an input element!");
+ SimpleTest.finish();
+ return;
+ }
+
+ ok(window.oTarget instanceof HTMLInputElement, "Should have focused the input element!");
+ var e = document.createEvent("mouseevents");
+ e.initMouseEvent("click", true, true, window, 0, 1, 1, 1, 1,
+ false, false, false, false, 0, null);
+ SpecialPowers.wrap(window.oTarget).dispatchEvent(e);
+ ok(window.fileInputGotClick,
+ "File input should have got a click event, but not open the file dialog.");
+ SimpleTest.finish();
+}
+
+function beginTest() {
+ // accessibility.tabfocus must be set to value 7 before running test also
+ // on a mac.
+ SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}, do_test);
+}
+
+function do_test() {
+ window.focus();
+ document.getElementById('fileinput').focus();
+ setTimeout(test, 100);
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+addLoadEvent(beginTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug446663.html b/layout/forms/test/test_bug446663.html
new file mode 100644
index 0000000000..bdab0b20ef
--- /dev/null
+++ b/layout/forms/test/test_bug446663.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=446663
+-->
+<head>
+ <title>Test for Bug 446663</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446663">Mozilla Bug 446663</a>
+<p id="display">
+<style>#bug446663_a:focus{overflow:hidden}</style>
+<input id="bug446663_a"><input id="bug446663_b"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 446663 **/
+
+function test_edit_cmds(id) {
+
+ var elm = document.getElementById(id);
+ elm.focus();
+ elm.select();
+ SpecialPowers.wrap(elm).controllers.getControllerForCommand('cmd_cut')
+ .doCommand('cmd_cut');
+ is(elm.value, '', id + " cut");
+
+ SpecialPowers.wrap(elm).controllers.getControllerForCommand('cmd_undo')
+ .doCommand('cmd_undo');
+ is(elm.value, '123', id + " undo");
+}
+
+var inputHappened = false;
+function inputListener() {
+ inputHappened = true;
+ $(id).removeEventListener("input", inputListener);
+}
+
+var id = 'bug446663_a'
+var elm = document.getElementById(id);
+elm.focus();
+var x = document.body.offsetHeight;
+$(id).addEventListener("input", inputListener);
+sendChar('1');
+is(inputHappened, true, "How come no input?");
+sendChar('3');
+sendKey('LEFT')
+sendChar('2');
+elm.blur();
+x = document.body.offsetHeight;
+is(elm.value, '123', id + " edit");
+test_edit_cmds(id)
+
+id = 'bug446663_b'
+elm = document.getElementById(id);
+elm.focus();
+sendChar('1');
+elm.style.display = 'none'
+var x = document.body.offsetHeight;
+elm.style.display = 'inline'
+x = document.body.offsetHeight;
+sendChar('3');
+sendKey('LEFT')
+sendChar('2');
+elm.blur();
+x = document.body.offsetHeight;
+is(elm.value, '123', id + " edit");
+test_edit_cmds(id)
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/layout/forms/test/test_bug476308.html b/layout/forms/test/test_bug476308.html
new file mode 100644
index 0000000000..41858d9eb8
--- /dev/null
+++ b/layout/forms/test/test_bug476308.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=476308
+-->
+<head>
+ <title>Test for Bug 345267</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<button style="-moz-appearance: none; width: 100px; height: 60px; background-color: red; border: 2px solid green; box-shadow: 30px 0px 3.5px black; position: absolute; top: 300px; left: 20px;"
+ id="button1">1</button>
+
+<br />
+<div style="width: 100px; height: 100px; background-color: green; border: 3px dotted blue; box-shadow: -30px -20px 0px black; position: absolute; top: 500px; left: 70px;"
+ id="div1">2</div>
+
+<script type="text/javascript">
+ var elem = document.elementFromPoint(130, 310);
+ isnot(elem, document.getElementById("button1"), "button1's box-shadow is receiving events when it shouldn't");
+
+ elem = document.elementFromPoint(50, 500);
+ isnot(elem, document.getElementById("div1"), "div1's box-shadow is receiving events when it shouldn't");
+</script>
+
+</body>
+</html>
+
diff --git a/layout/forms/test/test_bug477531.html b/layout/forms/test/test_bug477531.html
new file mode 100644
index 0000000000..e0c7979af4
--- /dev/null
+++ b/layout/forms/test/test_bug477531.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=477531
+-->
+<head>
+ <title>Test for Bug 477531</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <style type="text/css">
+ #s {
+ margin-left: 10px;
+ }
+
+ #s:indeterminate {
+ margin-left: 30px;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477531">Mozilla Bug 477531</a>
+<p id="display"></p>
+<div id="content">
+
+<input type="checkbox" id="s" />
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 477531 **/
+is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"),
+ "10px",
+ "Non-indeterminate checkbox should have a margin of 10px");
+
+$("s").indeterminate = true;
+
+is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"),
+ "30px",
+ "Indeterminate checkbox should have a margin of 30px");
+
+$("s").setAttribute("type", "radio");
+
+is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"),
+ "30px",
+ "Setting an indeterminate element to type radio should give it indeterminate styles");
+
+$("s").setAttribute("type", "checkbox");
+
+is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"),
+ "30px",
+ "Setting an indeterminate element to type checkbox should give it indeterminate styles");
+
+$("s").indeterminate = false;
+
+is(document.defaultView.getComputedStyle($("s")).getPropertyValue("margin-left"),
+ "10px",
+ "Newly non-indeterminate checkbox should have a margin of 10px");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/layout/forms/test/test_bug477700.html b/layout/forms/test/test_bug477700.html
new file mode 100644
index 0000000000..1ab8d88321
--- /dev/null
+++ b/layout/forms/test/test_bug477700.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=477700
+-->
+<head>
+ <title>Test for Bug 477700</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477700">Mozilla Bug 477700</a>
+<p id="display">
+ <iframe id="i"
+ src="http://example.com/tests/layout/forms/test/bug477700_subframe.html"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 477700 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ isnot(window.location.host, "example.com", "test is not testing cross-site");
+ var accessed = false;
+ try {
+ $("i").contentDocument.documentElement;
+ accessed = true;
+ } catch(e) {}
+ is(accessed, false, "Shouldn't be able to access cross-site");
+
+ $("i").style.display = "none";
+ document.body.offsetWidth;
+ is(document.defaultView.getComputedStyle($("i")).display, "none",
+ "toggling display failed");
+ $("i").style.display = "";
+ document.body.offsetWidth;
+ is(document.defaultView.getComputedStyle($("i")).display, "inline",
+ "toggling display back failed");
+
+ $("i").contentWindow.postMessage("start", "*");
+});
+
+window.addEventListener("message",
+ function(evt) {
+ var arr = evt.data.split(/ /).map(decodeURIComponent);
+ if (arr[0] == 't') {
+ is(arr[1], arr[2], arr[3]);
+ } else if (arr[0] == 'f') {
+ SimpleTest.finish();
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug534785.html b/layout/forms/test/test_bug534785.html
new file mode 100644
index 0000000000..7e43838927
--- /dev/null
+++ b/layout/forms/test/test_bug534785.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=534785
+-->
+<head>
+ <title>Test for Bug 534785</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534785">Mozilla Bug 534785</a>
+<p id="display"></p>
+<input type="text" value="test">
+<div id="reframe">
+<textarea></textarea>
+<textarea>test</textarea>
+<input type="text">
+<input type="text" value="test">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 534785 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ i.addEventListener("focus", function() {
+ is(i.value, "test", "Sanity check");
+
+ is(document.activeElement, i, "Should be focused before frame reconstruction");
+ sendString("1");
+ is(i.value, "1test", "Can accept keyboard events before frame reconstruction");
+
+ // force frame reconstruction
+ i.style.display = "none";
+ document.offsetHeight;
+ i.style.display = "";
+ document.offsetHeight;
+
+ is(document.activeElement, i, "Should be focused after frame reconstruction");
+ sendString("2");
+ is(i.value, "12test", "Can accept keyboard events after frame reconstruction");
+
+ // Make sure reframing happens gracefully
+ var reframeDiv = document.getElementById("reframe");
+ var textAreaWithoutValue = reframeDiv.querySelectorAll("textarea")[0];
+ var textAreaWithValue = reframeDiv.querySelectorAll("textarea")[1];
+ var inputWithoutValue = reframeDiv.querySelectorAll("input")[0];
+ var inputWithValue = reframeDiv.querySelectorAll("input")[1];
+ reframeDiv.style.display = "none";
+ document.body.offsetWidth;
+ reframeDiv.style.display = "";
+ document.body.offsetWidth;
+ [textAreaWithoutValue, inputWithoutValue].forEach(function (elem) {
+ is(elem.value, "", "Value should persist correctly");
+ });
+ [textAreaWithValue, inputWithValue].forEach(function (elem) {
+ is(elem.value, "test", "Value should persist correctly");
+ });
+ [inputWithoutValue, inputWithValue].forEach(elem => elem.type = "submit");
+ document.body.offsetWidth;
+ is(inputWithoutValue.value, "", "Value should persist correctly");
+ is(inputWithValue.value, "test", "Value should persist correctly");
+ [inputWithoutValue, inputWithValue].forEach(elem => elem.type = "text");
+ document.body.offsetWidth;
+ is(inputWithoutValue.value, "", "Value should persist correctly");
+ is(inputWithValue.value, "test", "Value should persist correctly");
+ [inputWithoutValue, inputWithValue].forEach(elem => elem.focus()); // initialze the editor
+ reframeDiv.style.display = "none";
+ document.body.offsetWidth;
+ reframeDiv.style.display = "";
+ document.body.offsetWidth;
+ is(inputWithoutValue.value, "", "Value should persist correctly with editor");
+ is(inputWithValue.value, "test", "Value should persist correctly with editor");
+
+ SimpleTest.finish();
+ });
+ i.focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug536567_perwindowpb.html b/layout/forms/test/test_bug536567_perwindowpb.html
new file mode 100644
index 0000000000..224b2c74a4
--- /dev/null
+++ b/layout/forms/test/test_bug536567_perwindowpb.html
@@ -0,0 +1,215 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=536567
+-->
+<head>
+ <title>Test for Bug 536567</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536567">Mozilla Bug 536567</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 536567 **/
+
+const Cm = Components.manager;
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+var tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+var homeDir = Services.dirsvc.get("Desk", Ci.nsIFile);
+
+function newDir() {
+ var dir = tmpDir.clone();
+ dir.append("testdir" + Math.floor(Math.random() * 10000));
+ dir.QueryInterface(Ci.nsIFile);
+ dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+ return dir;
+}
+
+var dirs = [];
+for(let i = 0; i < 6; i++) {
+ dirs.push(newDir());
+}
+dirs.push(homeDir);
+var domains = ['http://mochi.test:8888', 'http://example.org:80', 'http://example.com:80'];
+/*
+ * These tests take 3 args each:
+ * - which domain to load
+ * - the filePicker displayDirectory we expect to be set
+ * - the file to pick (in most cases this will show up in the next test,
+ * as indicated by the comments)
+ */
+var tests = [
+ "clear history",
+ [0, 6, 0], // 0 -> 3
+ [1, 6, 1], // 1 -> 4
+ [2, 6, 2], // 2 -> 5
+ [0, 0, 3], // 3 -> 6
+ [1, 1, 1], // 4 -> 8
+ [2, 2, 2], // 5 -> 9
+ [0, 3, 1], // 6 -> 7
+ [0, 1, 0], // 7 -> x
+ [1, 1, 1], // 8 -> x
+ [2, 2, 2], // 9 -> x
+ "clear history",
+ [0, 6, 0], // 11 -> 15
+ [1, 6, 1], // 12 -> 16
+ [2, 6, 2], // 13 -> 17
+ "pb on",
+ [0, 0, 3], // 15 -> 18
+ [1, 1, 4], // 16 -> 19
+ [2, 2, 5], // 17 -> 20
+ [0, 3, 3], // 18 -> x
+ [1, 4, 4], // 19 -> x
+ [2, 5, 5], // 20 -> x
+ "pb off",
+ [0, 0, 5], // 22 -> 26
+ [1, 1, 4], // 23 -> 27
+ [2, 2, 3], // 24 -> 28
+ "pb on",
+ [0, 3, 5], // 26 -> x
+ [1, 4, 4], // 27 -> x
+ [2, 5, 3], // 28 -> x
+ "clear history",
+ // Not checking after clear history because browser.download.lastDir content
+ // pref is not being clear properly in private windows.
+ //[0, 6, 0], // 30 -> x
+ //[1, 6, 1], // 31 -> x
+ //[2, 6, 2], // 32 -> x
+ "pb off"
+];
+
+var testIndex = 0;
+var content;
+var normalWindow;
+var privateWindow;
+var normalWindowIframe;
+var privateWindowIframe;
+
+function runTest() {
+ var test = tests[testIndex];
+ if (test == undefined) {
+ endTest();
+ } else if (test == "pb on") {
+ content = privateWindowIframe;
+ testIndex++;
+ runTest();
+ } else if (test == "pb off") {
+ content = normalWindowIframe;
+ testIndex++;
+ runTest();
+ } else if (test == "clear history") {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ testIndex++;
+ runTest();
+ } else {
+ var file = dirs[test[2]].clone();
+ file.append("file.file");
+ MockFilePicker.setFiles([file]);
+ content.setAttribute('src', domains[test[0]] + '/chrome/layout/forms/test/bug536567_subframe.html');
+ }
+}
+
+function endTest() {
+ for(let i = 0; i < dirs.length - 1; i++) {
+ dirs[i].remove(true);
+ }
+
+ normalWindow.close();
+ privateWindow.close();
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+}
+
+var mainWindow = window.browsingContext.topChromeWindow;
+var contentPage = "http://mochi.test:8888/chrome/layout/forms/test/bug536567_iframe.html";
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished");
+}
+
+function testOnWindow(aIsPrivate, aCallback) {
+ var win = mainWindow.OpenBrowserWindow({private: aIsPrivate});
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(Services.io.newURI(contentPage), {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+ });
+ return;
+ }
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+ win.gBrowser.selectedBrowser.focus();
+ SimpleTest.info("DOMContentLoaded's window: " + win.location + " vs. " + window.location);
+ win.setTimeout(function() { aCallback(win); }, 0);
+ }, true);
+ SimpleTest.info("load's window: " + win.location + " vs. " + window.location);
+ win.setTimeout(function() {
+ win.gBrowser.loadURI(Services.io.newURI(contentPage), {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+ });
+ }, 0);
+ });
+}
+
+MockFilePicker.showCallback = function(filepicker) {
+ var test = tests[testIndex];
+ var returned = -1;
+ for (let i = 0; i < dirs.length; i++) {
+ var dir = MockFilePicker.displayDirectory
+ ? MockFilePicker.displayDirectory
+ : Services.dirsvc.get(MockFilePicker.displaySpecialDirectory, Ci.nsIFile);
+ if (dirs[i].path == dir.path) {
+ returned = i;
+ break;
+ }
+ }
+ if (test[1] == -1) {
+ ok(false, "We should never get an unknown directory back");
+ } else {
+ is(returned, test[1], 'test ' + testIndex);
+ }
+
+ filepicker.window.setTimeout(function() {
+ testIndex++;
+ runTest();
+ }, 0);
+};
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ testOnWindow(false, function(aWin) {
+ var selectedBrowser = aWin.gBrowser.selectedBrowser;
+
+ normalWindow = aWin;
+ normalWindowIframe =
+ selectedBrowser.contentDocument.getElementById("content");
+
+ testOnWindow(true, function(aPrivateWin) {
+ selectedBrowser = aPrivateWin.gBrowser.selectedBrowser;
+
+ privateWindow = aPrivateWin;
+ privateWindowIframe =
+ selectedBrowser.contentDocument.getElementById("content");
+
+ content = normalWindowIframe;
+ selectedBrowser.contentWindow.setTimeout(runTest, 0);
+ });
+ });
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug542914.html b/layout/forms/test/test_bug542914.html
new file mode 100644
index 0000000000..ff7a68acea
--- /dev/null
+++ b/layout/forms/test/test_bug542914.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=542914
+-->
+<head>
+ <title>Test for Bug 542914</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=542914">Mozilla Bug 542914</a>
+<p id="display">
+ <input type="text" id="a" value="test">
+ <input type="text" id="b">
+ <input type="text" id="c">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 542914 **/
+SimpleTest.waitForExplicitFinish();
+function runTests(callback, type) {
+ var a = $("a");
+
+ // Test that the initial value of the control is available to script
+ // without initilization of the editor
+ is(a.value, "test", "The value is available before initialization");
+ // Initialize the editor
+ a.focus();
+ // Test that the value does not change after initialization
+ is(a.value, "test", "The value does not change after initializtion");
+
+ var b = $("b");
+
+ // Test that the initial value is empty before initialization.
+ is(b.value, "", "The value is empty before initialization");
+ // Make sure that the value can be changed before initialization
+ b.value ="some value";
+ is(b.value, "some value", "The value can be changed before initialization");
+ // Initialize the editor
+ b.focus();
+ // Make sure that the value does not change after initialization
+ is(b.value, "some value", "The value does not change after initialization");
+ // Make sure that the value does not change if the element is hidden
+ b.style.display = "none";
+ document.body.offsetHeight;
+ is(b.value, "some value", "The value does not change while hidden");
+ b.style.display = "";
+ document.body.offsetHeight;
+ b.focus();
+ is(b.value, "some value", "The value does not change after being shown");
+
+ var c = $("c");
+
+ // Make sure that the control accepts input events without explicit initialization
+ is(c.value, "", "Control is empty initially");
+ c.focus();
+ sendChar("a");
+ is(c.value, "a", "Control accepts input without explicit initialization");
+ // Make sure that the control retains its caret position
+ c.focus();
+ c.blur();
+ c.focus();
+ sendChar("b");
+ is(c.value, "ab", "Control retains caret position after being re-focused");
+
+ var d = document.createElement("input");
+ d.setAttribute("type", type);
+ $("display").appendChild(d);
+ document.body.offsetHeight;
+
+ // Make sure dynamically injected inputs work as expected
+ is(d.value, "", "Dynamic control's initial value should be empty");
+ d.value = "new";
+ d.focus();
+ is(d.value, "new", "Dynamic control's value can be set before initialization");
+ sendChar("x");
+ is(d.value, "newx", "Dynamic control accepts keyboard input without explicit initialization");
+ $("display").removeChild(d);
+ is(d.value, "newx", "Dynamic control retains value after being removed from the document");
+
+ callback();
+}
+
+var gPreviousType = "text";
+function setTypes(aType) {
+ var content = document.getElementById("display");
+ content.innerHTML = content.innerHTML.replace(gPreviousType, aType);
+ gPreviousType = aType;
+}
+
+addLoadEvent(function() {
+ ok(true, "Running tests on <input type=text>");
+ runTests(function() {
+ ok(true, "Running tests on <input type=password>");
+ setTypes("password");
+ runTests(function() {
+ ok(true, "Running tests on <input type=tel>");
+ setTypes("tel");
+ runTests(function() {
+ SimpleTest.finish();
+ }, "tel");
+ }, "password");
+ }, "text");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug549170.html b/layout/forms/test/test_bug549170.html
new file mode 100644
index 0000000000..a8a3f6d508
--- /dev/null
+++ b/layout/forms/test/test_bug549170.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=549170
+-->
+<head>
+ <title>Test for Bug 549170</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="window.setTimeout(runTests, 0);">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549170">Mozilla Bug 549170</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id='i'
+ onmouseup="mouseHandler(event);"
+ onmousedown="mouseHandler(event);">
+<textarea id='t'
+ onmouseup="mouseHandler(event);"
+ onmousedown="mouseHandler(event);"></textarea><br>
+<input id='ip' placeholder='foo'
+ onmouseup="mouseHandler(event);"
+ onmousedown="mouseHandler(event);">
+<textarea id='tp' placeholder='foo'
+ onmouseup="mouseHandler(event);"
+ onmousedown="mouseHandler(event);"></textarea>
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 549170 **/
+
+var gNumberOfMouseEventsCaught = 0;
+
+SimpleTest.waitForExplicitFinish();
+
+function mouseHandler(aEvent)
+{
+ gNumberOfMouseEventsCaught++;
+ is(SpecialPowers.wrap(aEvent).originalTarget.nodeName, "DIV", "An inner div should be the target of the event");
+ ok(SpecialPowers.wrap(aEvent).originalTarget.implementedPseudoElement == "::-moz-text-control-editing-root", "the target div should be the editor div");
+}
+
+function checkMouseEvents(element)
+{
+ gNumberOfMouseEventsCaught = 0;
+
+ let x = element.offsetWidth / 2;
+ let y = element.offsetHeight / 2;
+
+ synthesizeMouse(element, x, y, {type: "mousedown", button: 0});
+ synthesizeMouse(element, x, y, {type: "mouseup", button: 0});
+ synthesizeMouse(element, x, y, {type: "mousedown", button: 1});
+ // NOTE: this event is going to copy the buffer on linux, this should not be a problem
+ synthesizeMouse(element, x, y, {type: "mouseup", button: 1});
+ synthesizeMouse(element, x, y, {type: "mousedown", button: 2});
+ synthesizeMouse(element, x, y, {type: "mouseup", button: 2});
+
+ is(gNumberOfMouseEventsCaught, 6, "Some mouse events have not been caught");
+}
+
+function runTests()
+{
+ checkMouseEvents(document.getElementById('i'));
+ checkMouseEvents(document.getElementById('t'));
+ checkMouseEvents(document.getElementById('ip'));
+ checkMouseEvents(document.getElementById('tp'));
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug562447.html b/layout/forms/test/test_bug562447.html
new file mode 100644
index 0000000000..86042bb485
--- /dev/null
+++ b/layout/forms/test/test_bug562447.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=562447
+-->
+<head>
+ <title>Test for Bug 562447</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<body>
+<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug?id=562447">Mozilla Bug 562447</a>
+
+<input id="WhyDoYouFocusMe"
+ style="position: absolute; left: -50px; top: 10000px;">
+
+<pre id="test">
+<script>
+addLoadEvent(function() {
+ // Scroll down a bit
+ window.scrollTo(0, 5000);
+
+ setTimeout(function() {
+ // Make sure that we're scrolled by 5000px
+ is(Math.round(window.pageYOffset), 5000, "Make sure we're scrolled correctly");
+
+ // Scroll back up, and mess with the input box along the way
+ var input = document.getElementById("WhyDoYouFocusMe");
+ input.focus();
+ input.blur();
+ window.scrollTo(0, 0);
+
+ setTimeout(function() {
+ is(window.pageYOffset, 0, "Make sure we're scrolled back up correctly");
+
+ // Scroll back up
+ window.scrollTo(0, 5000);
+
+ setTimeout(function() {
+ is(Math.round(window.pageYOffset), 5000, "Sanity check");
+
+ window.scrollTo(0, 0);
+ input.focus();
+ input.blur();
+
+ setTimeout(function() {
+ isnot(Math.round(window.pageYOffset), 0, "This time we shouldn't be scrolled up");
+
+ SimpleTest.finish();
+ }, 0);
+ }, 0);
+ }, 0);
+ }, 0);
+});
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/layout/forms/test/test_bug563642.html b/layout/forms/test/test_bug563642.html
new file mode 100644
index 0000000000..72bb4c6858
--- /dev/null
+++ b/layout/forms/test/test_bug563642.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=563642
+-->
+<head>
+ <title>Test for Bug 563642</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563642">Mozilla Bug 563642</a>
+<p id="display">
+<select id="test1" multiple="multiple" size="1">
+ <option>Item 1</option>
+ <option>Item 2</option>
+ <option>Item 3</option>
+ <option>Item 4</option>
+ <option>Item 5</option>
+</select>
+<select id="test2" multiple="multiple" size="1">
+ <option>Item 1</option>
+ <option disabled>Item 2</option>
+ <option>Item 3</option>
+ <option disabled>Item 4</option>
+ <option>Item 5</option>
+</select>
+<select id="test3" multiple="multiple"></select>
+<select id="test4" multiple="multiple" size="1"></select>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 563642 **/
+
+function pageUpDownTest(id,index) {
+ var elm = document.getElementById(id);
+ elm.focus();
+ elm.selectedIndex = 0;
+ sendKey("page_down");
+ sendKey("page_down");
+ sendKey("page_down");
+ sendKey("page_up");
+ sendKey("page_down");
+ is(elm.selectedIndex, index, "pageUpDownTest: selectedIndex for " + id + " is " + index);
+}
+
+function upDownTest(id,index) {
+ var elm = document.getElementById(id);
+ elm.focus();
+ elm.selectedIndex = 0;
+ sendKey("down");
+ sendKey("down");
+ sendKey("down");
+ sendKey("up");
+ sendKey("down");
+ is(elm.selectedIndex, index, "upDownTest: selectedIndex for " + id + " is " + index);
+}
+
+function runTest() {
+ pageUpDownTest("test1",3);
+ pageUpDownTest("test2",4);
+ pageUpDownTest("test3",-1);
+ pageUpDownTest("test4",-1);
+ upDownTest("test1",3);
+ upDownTest("test2",4);
+ upDownTest("test3",-1);
+ upDownTest("test4",-1);
+
+ SimpleTest.finish();
+}
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug564115.html b/layout/forms/test/test_bug564115.html
new file mode 100644
index 0000000000..16fb423341
--- /dev/null
+++ b/layout/forms/test/test_bug564115.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=564115
+-->
+<head>
+ <title>Test for Bug 564115</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<body>
+<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug?id=564115">Mozilla Bug 564115</a>
+
+<pre id="test">
+<script>
+
+const TEST_URL = "/tests/layout/forms/test/bug564115_window.html";
+
+addLoadEvent(function() {
+ var win = open(TEST_URL, "", "width=600,height=600");
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var input = doc.querySelector("input");
+
+ // Focus the input box, and wait for the focus to actually happen
+ input.focus();
+ win.requestAnimationFrame(function() {
+ win.requestAnimationFrame(function() {
+ // Scroll down a bit
+ win.scrollTo(0, 5000);
+
+ setTimeout(function() {
+ is(Math.round(win.pageYOffset), 5000, "Page should be scrolled correctly");
+
+ // Refocus the window
+ SimpleTest.waitForFocus(function() {
+ SimpleTest.waitForFocus(function() {
+ is(Math.round(win.pageYOffset), 5000,
+ "The page's scroll offset should not have been changed");
+
+ win.close();
+ SimpleTest.finish();
+ }, win);
+ });
+ }, 0);
+ });
+ });
+ }, win);
+});
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/layout/forms/test/test_bug571352.html b/layout/forms/test/test_bug571352.html
new file mode 100644
index 0000000000..73ad7454f3
--- /dev/null
+++ b/layout/forms/test/test_bug571352.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=571352
+-->
+<head>
+ <title>Test for Bug 571352</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=571352">Mozilla Bug 571352</a>
+<p id="display"><select multiple style="width:300px/*to avoid any overlay scrollbar messing with our mouse clicks*/"><option>0<option>1<option>2<option>3<option>4<option>5</select></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 571352 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function test() {
+ function focusList() {
+ $('display').firstChild.focus();
+ }
+ function option(index) {
+ return $('display').firstChild.childNodes[index];
+ }
+ function remove(index) {
+ var sel = $('display').firstChild;
+ sel.removeChild(sel.childNodes[index]);
+ document.body.clientHeight;
+ }
+ function up() { synthesizeKey("KEY_ArrowUp"); }
+ function shiftUp() { synthesizeKey("KEY_ArrowUp", {shiftKey:true}); }
+ function down() { synthesizeKey("KEY_ArrowDown"); }
+ function shiftDown() { synthesizeKey("KEY_ArrowDown", {shiftKey:true}); }
+ function mouseEvent(index,event) {
+ synthesizeMouse(option(index), 5, 5, event);
+ }
+
+ const click = {};
+ const shiftClick = {shiftKey:true};
+ focusList();
+ mouseEvent(0,click)
+ is(document.activeElement,$('display').firstChild,"<select> is focused");
+ ok(option(0).selected,"first option is selected");
+ mouseEvent(2,shiftClick)
+ remove(0);
+ ok(option(0).selected && option(1).selected,"first two options are selected");
+ mouseEvent(2,shiftClick)
+ ok(option(0).selected && option(1).selected && option(2).selected,"first three options are selected");
+ shiftUp();
+ ok(option(0).selected && option(1).selected,"first two options are selected");
+ remove(1);
+ ok(option(0).selected,"first option is selected");
+ shiftDown();
+ ok(option(0).selected && option(1).selected,"first two options are selected");
+ down();
+ ok(option(2).selected,"third option is selected");
+ shiftDown();
+ ok(option(2).selected && option(3).selected,"third & fourth option are selected");
+ remove(2);
+ shiftUp();
+ ok(option(1).selected && option(2).selected,"2nd & third option are selected");
+ remove(0);
+ mouseEvent(0,shiftClick)
+ ok(option(0).selected && option(1).selected,"all remaining 2 options are selected");
+ shiftDown();
+ remove(1);
+ ok(!option(0).selected,"first option is unselected");
+ remove(0); // select is now empty
+ ok($('display').firstChild.firstChild==null,"all options were removed");
+
+ SimpleTest.finish();
+});
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug572406.html b/layout/forms/test/test_bug572406.html
new file mode 100644
index 0000000000..8b4d0b81ce
--- /dev/null
+++ b/layout/forms/test/test_bug572406.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=572406
+-->
+<head>
+ <title>Test for Bug 572406</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload='runTests();'>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572406">Mozilla Bug 572406</a>
+<p id="display"></p>
+<div id='content'>
+ <textarea id='i'>foo</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 572406 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests()
+{
+ var textarea = document.getElementById('i');
+
+ textarea.firstChild.nodeValue = "bar";
+ is(textarea.value, textarea.firstChild.nodeValue,
+ "textarea value should be firstChild.nodeValue");
+ is(textarea.defaultValue, textarea.firstChild.nodeValue,
+ "textarea defaultValue should be firstChild.nodeValue");
+
+ textarea.style.display = 'none';
+ SimpleTest.executeSoon(function() {
+ textarea.firstChild.nodeValue = "tulip";
+ is(textarea.value, textarea.firstChild.nodeValue,
+ "textarea value should be firstChild.nodeValue");
+ is(textarea.defaultValue, textarea.firstChild.nodeValue,
+ "textarea defaultValue should be firstChild.nodeValue");
+ SimpleTest.finish();
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug572649.html b/layout/forms/test/test_bug572649.html
new file mode 100644
index 0000000000..10fa6e63f7
--- /dev/null
+++ b/layout/forms/test/test_bug572649.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=572649
+-->
+<head>
+ <title>Test for Bug 572649</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572649">Mozilla Bug 572649</a>
+<p id="display">
+ <textarea id="area" rows="5">
+ Here
+ is
+ some
+ very
+ long
+ text
+ that
+ we're
+ using
+ for
+ testing
+ purposes
+ </textarea>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 572649 **/
+SimpleTest.waitForExplicitFinish();
+
+// We intermittently trigger two "Wrong parent ComputedStyle" assertions
+// on B2G emulator builds (bug XXXXXXX). The two frames that get incorrect
+// ComputedStyle parents are scroll bar parts in the <textarea>.
+SimpleTest.expectAssertions(0, 2);
+
+addLoadEvent(function() {
+ var area = document.getElementById("area");
+
+ is(area.scrollTop, 0, "The textarea should not be scrolled initially");
+ area.addEventListener("focus", function() {
+ setTimeout(function() {
+ is(area.scrollTop, 0, "The textarea's insertion point should not be scrolled into view");
+
+ SimpleTest.finish();
+ }, 0);
+ }, {once: true});
+ setTimeout(function() {
+ var rect = area.getBoundingClientRect();
+ synthesizeMouse(area, rect.width - 5, 5, {});
+ }, 0);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug595310.html b/layout/forms/test/test_bug595310.html
new file mode 100644
index 0000000000..47db981167
--- /dev/null
+++ b/layout/forms/test/test_bug595310.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=595310
+-->
+<head>
+ <title>Test for Bug 595310</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595310">Mozilla Bug 595310</a>
+<p id="display"></p>
+<div id="content">
+ <input id='i' value="bar">
+ <textarea id='t'>bar</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 595310 **/
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // We want to compare to value="bar" and no placeholder shown.
+ // That is what is currently showed.
+ var s1 = snapshotWindow(window, false);
+
+ var content = document.getElementById('content');
+ var i = document.getElementById('i');
+ var t = SpecialPowers.wrap(document.getElementById('t'));
+ i.value = ""; i.placeholder = "foo";
+ t.value = ""; t.placeholder = "foo";
+
+ // Flushing.
+ // Note: one call would have been enough actually but I didn't want to favour
+ // one element... ;)
+ i.getBoundingClientRect();
+ t.getBoundingClientRect();
+
+ function synthesizeDropText(aElement, aText)
+ {
+ SpecialPowers.wrap(aElement).editor.insertText(aText);
+ }
+
+ // We insert "bar" and we should only see "bar" now.
+ synthesizeDropText(i, "bar");
+ synthesizeDropText(t, "bar");
+
+ var s2 = snapshotWindow(window, false);
+
+ ok(compareSnapshots(s1, s2, true)[0],
+ "When setting the value, the placeholder should disappear.");
+
+ SimpleTest.finish();
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug620936.html b/layout/forms/test/test_bug620936.html
new file mode 100644
index 0000000000..aa1beed01a
--- /dev/null
+++ b/layout/forms/test/test_bug620936.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=620936
+-->
+<head>
+ <title>Test for Bug 620936</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620936">Mozilla Bug 620936</a>
+<p id="display"></p>
+<div id="content">
+ <input value="foo">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 620936 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.querySelector("input");
+ i.focus();
+ i.setSelectionRange(100, 100);
+ is(i.selectionStart, 3, "The selection should be set to the end of the text");
+ is(i.selectionEnd, 3, "The selection should be set to the end of the text");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug644542.html b/layout/forms/test/test_bug644542.html
new file mode 100644
index 0000000000..6c33cbe958
--- /dev/null
+++ b/layout/forms/test/test_bug644542.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=644542
+-->
+<head>
+ <title>Test for Bug 644542</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ select:after {
+ content: ' appended string';
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=644542">Mozilla Bug 644542</a>
+<p id="display">
+ <form method="post" action="">
+ <select id="select">
+ <option value="1">1</option>
+ <option value="2">2</option>
+ </select>
+ </form>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 644542 **/
+
+var select = document.getElementById("select");
+
+var clicks = 0;
+
+function click() {
+ synthesizeMouseAtCenter(select, { });
+ ++clicks;
+
+ // At least two clicks were required for bug 644542, sometimes more;
+ // delay is long enough that this doesn't look like a double
+ // click, and also allows time for popup to show and for painting.
+ setTimeout(clicks < 4 ? click : done, 500);
+}
+
+function done() {
+ ok(true, "No crash on opening dropdown");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+// waitForFocus is most likely not the right thing to wait for, but
+// without this the first click is ineffective (even with a reflow forced
+// before synthesizeMouse).
+SimpleTest.waitForFocus(click);
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug672810.html b/layout/forms/test/test_bug672810.html
new file mode 100644
index 0000000000..991fe57389
--- /dev/null
+++ b/layout/forms/test/test_bug672810.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=672810
+-->
+<head>
+ <title>Test for Bug 672810</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=672810">Mozilla Bug 672810</a>
+<p id="display"></p>
+<div id="content">
+ <select id="s1" multiple size="10"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select>
+ <select id="s2" size="10"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select>
+ <select id="s3" size="1"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 672810 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ var sel = document.getElementsByTagName('select');
+
+ sel[0].addEventListener('focus', function() {
+ s = sel[0];
+ s.removeEventListener('focus', arguments.callee);
+ synthesizeKey('KEY_ArrowDown');
+ is(s.selectedIndex,0, s.id + ": initial DOWN selects first option");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true});
+ is(s.selectedIndex,0, s.id + ": first option is still selected");
+ ok(!s[1].selected,s.id + ": CTRL+DOWN did not select 2nd option");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true});
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,0, s.id + ": first option is still selected");
+ ok(!s[1].selected,s.id + ": 2nd option is still unselected");
+ ok(s[2].selected,s.id + ": 3rd option is selected");
+ ok(s[3].selected,s.id + ": 4th option is selected");
+ ok(!s[4].selected,s.id + ": 5th option is unselected");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,0, s.id + ": first option is still selected");
+ ok(!s[1].selected,s.id + ": 2nd option is still unselected");
+ ok(s[2].selected,s.id + ": 3rd option is still selected");
+ ok(s[3].selected,s.id + ": 4th option is still selected");
+ ok(s[4].selected,s.id + ": 5th option is selected");
+ ok(!s[5].selected,s.id + ": 6th option is unselected");
+ synthesizeKey('KEY_ArrowUp', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,0, s.id + ": first option is still selected");
+ ok(!s[1].selected,s.id + ": 2nd option is still unselected");
+ ok(s[2].selected,s.id + ": 3rd option is still selected");
+ ok(s[3].selected,s.id + ": 4th option is still selected");
+ ok(s[4].selected,s.id + ": 5th option is still selected");
+ ok(!s[5].selected,s.id + ": 6th option is still unselected");
+ synthesizeKey(' ', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,0, s.id + ": first option is still selected");
+ ok(!s[1].selected,s.id + ": 2nd option is still unselected");
+ ok(s[2].selected,s.id + ": 3rd option is still selected");
+ ok(!s[3].selected,s.id + ": 4th option is unselected");
+ ok(s[4].selected,s.id + ": 5th option is still selected");
+ ok(!s[5].selected,s.id + ": 6th option is still unselected");
+ synthesizeKey(' ', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,0, s.id + ": first option is still selected");
+ ok(!s[1].selected,s.id + ": 2nd option is still unselected");
+ ok(s[2].selected,s.id + ": 3rd option is still selected");
+ ok(s[3].selected,s.id + ": 4th option is selected");
+ ok(s[4].selected,s.id + ": 5th option is still selected");
+ ok(!s[5].selected,s.id + ": 6th option is still unselected");
+ setTimeout(function(){sel[1].focus()},0);
+ });
+ sel[1].addEventListener('focus', function() {
+ s = sel[1];
+ s.removeEventListener('focus', arguments.callee);
+ synthesizeKey('KEY_ArrowDown');
+ is(s.selectedIndex,0, s.id + ": initial DOWN selects first option");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true});
+ is(s.selectedIndex,1, s.id + ": 2nd option is selected");
+ ok(!s[0].selected,s.id + ": CTRL+DOWN deselected first option");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true});
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,3, s.id + ": 4th option is selected");
+ ok(!s[1].selected,s.id + ": CTRL+SHIFT+DOWN deselected 2nd option");
+ synthesizeKey(' ', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,3, s.id + ": 4th option is still selected");
+ synthesizeKey(' ', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,3, s.id + ": 4th option is still selected");
+ setTimeout(function(){sel[2].focus()},0);
+ });
+ sel[2].addEventListener('focus', function() {
+ if (!navigator.platform.includes("Mac")) {
+ s = sel[2];
+ s.removeEventListener('focus', arguments.callee);
+ synthesizeKey('KEY_ArrowDown');
+ is(s.selectedIndex,1, s.id + ": initial DOWN selects 2nd option");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true});
+ is(s.selectedIndex,2, s.id + ": 3rd option is selected");
+ ok(!s[1].selected,s.id + ": CTRL+DOWN deselected 2nd option");
+ synthesizeKey('KEY_ArrowDown', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,3, s.id + ": 4th option is selected");
+ ok(!s[2].selected,s.id + ": CTRL+SHIFT+DOWN deselected 3rd option");
+ synthesizeKey(' ', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,3, s.id + ": 4th option is still selected");
+ synthesizeKey(' ', {ctrlKey: true, shiftKey: true});
+ is(s.selectedIndex,3, s.id + ": 4th option is still selected");
+ } else {
+ todo(false, "Make this test work on OSX");
+ }
+ setTimeout(function(){SimpleTest.finish()},0);
+ });
+ sel[0].focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug704049.html b/layout/forms/test/test_bug704049.html
new file mode 100644
index 0000000000..1da2badea2
--- /dev/null
+++ b/layout/forms/test/test_bug704049.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=704049
+-->
+<head>
+ <title>Test for Bug 704049</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=704049">Mozilla Bug 704049</a>
+<p id="display"></p>
+<input type="radio" id="radio11" name="group1">
+<input type="radio" id="radio12" name="group1">
+<input type="radio" id="radio21" name="group2" checked>
+<input type="radio" id="radio22" name="group2">
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 704049 **/
+
+window.addEventListener("click", function (e) { e.preventDefault(); });
+
+function doTest()
+{
+ var target = document.getElementById("radio11");
+ synthesizeMouseAtCenter(target, {});
+ is(target.checked, false, "radio11 is checked");
+ target = document.getElementById("radio12");
+ synthesizeMouseAtCenter(target, {});
+ is(target.checked, false, "radio12 is checked");
+ target = document.getElementById("radio21");
+ synthesizeMouseAtCenter(target, {});
+ is(target.checked, true, "radio21 is not checked");
+ target = document.getElementById("radio22");
+ synthesizeMouseAtCenter(target, {});
+ is(target.checked, false, "radio22 is checked");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(doTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug717878_input_scroll.html b/layout/forms/test/test_bug717878_input_scroll.html
new file mode 100644
index 0000000000..6bd27ddacc
--- /dev/null
+++ b/layout/forms/test/test_bug717878_input_scroll.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717878
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 717878</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717878">Mozilla Bug 717878</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<!-- size=10 and monospace font ensure there's no overflow in either direction -->
+<input id="no-overflow" type="text"
+ size="10"
+ style="
+ font-family: monospace;
+ font-size: 1em;"
+ value="Short">
+<!-- ditto, with appearance:none -->
+<input id="no-overflow2" type="text"
+ size="10"
+ style="
+ -webkit-appearance:none;
+ font-family: monospace;
+ font-size: 1em;"
+ value="Short">
+<!-- size=10, monospace font, and height=0.5em ensure overflow in both directions -->
+<input id="overflow" type="text"
+ size="10"
+ style="
+ font-family: monospace;
+ font-size: 3em;
+ height: 0.5em;"
+ value="This is a long string">
+<!-- ditto, with appearance:none -->
+<input id="overflow2" type="text"
+ size="10"
+ style="
+ -webkit-appearance:none;
+ font-family: monospace;
+ font-size: 3em;
+ height: 0.5em;"
+ value="This is a long string">
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 717878 **/
+
+/**
+ * Test an element's scroll properties for correctness
+ *
+ * @param element Element to test
+ * @param prop Specify the property to test,
+ * i.e. "scrollLeft" or "scrollTop"
+ * @param propMax Specify the scrollMax property to test,
+ * i.e. "scrollLeftMax" or "scrollTopMax"
+ * @param is_overflow Specify whether the element is
+ * scrollable in the above direction
+ */
+function test_scroll(element, scroll, scrollMax, is_overflow) {
+
+ is(element[scroll], 0, element.id + " initial " + scroll + " != 0");
+ if (is_overflow) {
+ isnot(element[scrollMax], 0, element.id + " " + scrollMax + " == 0");
+ } else {
+ is(element[scrollMax], 0, element.id + " " + scrollMax + " != 0");
+ }
+
+ element[scroll] = 10;
+ if (is_overflow) {
+ isnot(element[scroll], 0, element.id + " unable to scroll " + scroll);
+ } else {
+ is(element[scroll], 0, element.id + " able to scroll " + scroll);
+ }
+
+ element[scroll] = element[scrollMax];
+ is(element[scroll], element[scrollMax], element.id + " did not scroll to " + scrollMax);
+
+ element[scroll] = element[scrollMax] + 10;
+ is(element[scroll], element[scrollMax], element.id + " scrolled past " + scrollMax);
+}
+
+var no_overflow = document.getElementById("no-overflow");
+test_scroll(no_overflow, "scrollLeft", "scrollLeftMax", /* is_overflow */ false);
+test_scroll(no_overflow, "scrollTop", "scrollTopMax", /* is_overflow */ false);
+
+var no_overflow2 = document.getElementById("no-overflow2");
+test_scroll(no_overflow2, "scrollLeft", "scrollLeftMax", /* is_overflow */ false);
+test_scroll(no_overflow2, "scrollTop", "scrollTopMax", /* is_overflow */ false);
+
+var overflow = document.getElementById("overflow");
+test_scroll(overflow, "scrollLeft", "scrollLeftMax", /* is_overflow */ true);
+test_scroll(overflow, "scrollTop", "scrollTopMax", /* is_overflow */ true);
+
+var overflow2 = document.getElementById("overflow2");
+test_scroll(overflow2, "scrollLeft", "scrollLeftMax", /* is_overflow */ true);
+test_scroll(overflow2, "scrollTop", "scrollTopMax", /* is_overflow */ true);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug869314.html b/layout/forms/test/test_bug869314.html
new file mode 100644
index 0000000000..7c786fccfc
--- /dev/null
+++ b/layout/forms/test/test_bug869314.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=869314
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 869314</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <style type="text/css">
+ .selectbox {
+ background-color: #00FF00;
+ }
+ </style>
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=869314">Mozilla Bug 869314</a>
+<p id="display"></p>
+<div id="content">
+
+ <select id="selectbox1" name="non-native selectbox" class="selectbox">
+ <option value="item">test item</option>
+ </select>
+
+ <select id="selectbox2" name="native selectbox">
+ <option value="item">test item</option>
+ </select>
+
+ <script type="application/javascript">
+ let Cc = SpecialPowers.Cc;
+ let Ci = SpecialPowers.Ci;
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ let osName = sysInfo.getProperty("name");
+ let isNNT = SpecialPowers.getBoolPref("widget.non-native-theme.enabled");
+ if (osName == "Darwin" && !isNNT) { // Native styled macOS form controls.
+ // This test is for macOS with native styled form controls only. See bug for more info.
+ ok(document.getElementById("selectbox1").clientWidth >
+ document.getElementById("selectbox2").clientWidth,
+ "Non-native styled combobox does not have enough space for a " +
+ "dropmarker!");
+ } else {
+ // We need to call at least one test function to make the test harness
+ // happy.
+ ok(true, "Test wasn't ignored but should have been.");
+ }
+ </script>
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug903715.html b/layout/forms/test/test_bug903715.html
new file mode 100644
index 0000000000..b887b2cd01
--- /dev/null
+++ b/layout/forms/test/test_bug903715.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=903715
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 903715</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=903715">Mozilla Bug 903715</a>
+<p id="display"></p>
+<div id="content">
+ <form id="form" action="/">
+ <select id="select" name="select">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+ <option>8</option>
+ <option>9</option>
+ </select>
+ <input id="input-text" name="text" value="some text">
+ <input id="input-submit" type="submit">
+ </form>
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+SimpleTest.waitForFocus(runTests, window);
+
+function runTests()
+{
+ var form = document.getElementById("form");
+ form.addEventListener("keypress", function (aEvent) {
+ ok(false, "keypress event shouldn't be fired when the preceding keydown event caused closing the dropdown of the select element");
+ }, true);
+ form.addEventListener("submit", function (aEvent) {
+ ok(false, "submit shouldn't be performed by the Enter key press on the select element");
+ aEvent.preventDefault();
+ }, true);
+ var select = document.getElementById("select");
+ select.addEventListener("change", function (aEvent) {
+ var input = document.getElementById("input-text");
+ input.focus();
+ input.select();
+ });
+
+ select.focus();
+
+ select.addEventListener("popupshowing", function (aEvent) {
+ setTimeout(function () {
+ synthesizeKey("KEY_ArrowDown");
+ select.addEventListener("popuphiding", function (aEventInner) {
+ setTimeout(function () {
+ // Enter key should cause closing the dropdown of the select element
+ // and keypress event shouldn't be fired on the input element because
+ // which shouldn't cause sumbmitting the form contents.
+ ok(true, "Test passes if there is no error");
+ SimpleTest.finish();
+ }, 100);
+ });
+ // Close dropdown.
+ synthesizeKey("KEY_Enter");
+ }, 100);
+ });
+
+ // Open dropdown.
+ synthesizeKey("KEY_ArrowDown", { altKey: true });
+}
+</script>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug935876.html b/layout/forms/test/test_bug935876.html
new file mode 100644
index 0000000000..4488fdf962
--- /dev/null
+++ b/layout/forms/test/test_bug935876.html
@@ -0,0 +1,502 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935876
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 935876</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935876">Mozilla Bug 935876</a>
+<p id="display"></p>
+<div>
+<select id="listbox" size="3">
+ <option selected>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+</select>
+<select id="multipleListbox" size="3" multiple>
+ <option selected>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+</select>
+<select id="combobox">
+ <option selected>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+</select>
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+const kIsWin = navigator.platform.indexOf("Win") == 0;
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+const kIsAndroid = navigator.appVersion.indexOf("Android") != 0;
+
+function runTests()
+{
+ var doPreventDefault = false;
+ function onKeydown(aEvent)
+ {
+ if (doPreventDefault) {
+ aEvent.preventDefault();
+ }
+ }
+
+ var keyPressEventFired = false;
+ function onKeypress(aEvent)
+ {
+ keyPressEventFired = true;
+ }
+
+ var keyDownEventConsumedByJS = false;
+ var keyDownEventConsumed = false;
+ function onkeydownInSystemEventGroup(aEvent)
+ {
+ keyDownEventConsumedByJS = aEvent.defaultPrevented;
+ // If defaultPrevented is true via SpecialPowers, that means default was
+ // prevented, possibly in a non-content-visible way (e.g. by a system
+ // group handler).
+ keyDownEventConsumed = SpecialPowers.wrap(aEvent).defaultPrevented;
+ }
+
+ function reset()
+ {
+ keyPressEventFired = false;
+ keyDownEventConsumedByJS = false;
+ keyDownEventConsumed = false;
+ }
+
+ function check(aExpectingKeydownConsumed, aIsPrintableKey, aDescription)
+ {
+ if (doPreventDefault) {
+ ok(!keyPressEventFired, "keypress event shouldn't be fired for " + aDescription +
+ " if preventDefault() of keydown event was called");
+ ok(keyDownEventConsumedByJS, "keydown event of " + aDescription +
+ " should be consumed in content level if preventDefault() of keydown event is called");
+ ok(keyDownEventConsumed, "keydown event of " + aDescription +
+ " should be consumed in system level if preventDefault() of keydown event is called");
+ } else if (aExpectingKeydownConsumed) {
+ ok(!keyPressEventFired, "keypress event shouldn't be fired for " + aDescription);
+ ok(!keyDownEventConsumedByJS, "keydown event of " + aDescription + " shouldn't be consumed in content level");
+ ok(keyDownEventConsumed, "keydown event of " + aDescription + " should be consumed in system level");
+ } else {
+ if (aIsPrintableKey) {
+ ok(keyPressEventFired, "keypress event should be fired for printable key, " + aDescription);
+ } else {
+ ok(!keyPressEventFired, "keypress event shouldn't be fired for non-printable key, " + aDescription);
+ }
+ ok(!keyDownEventConsumedByJS, "keydown event of " + aDescription + " shouldn't be consumed in content level");
+ ok(!keyDownEventConsumed, "keydown event of " + aDescription + " should be consumed in system level");
+ }
+ }
+
+ var listbox = document.getElementById("listbox");
+ listbox.addEventListener("keydown", onKeydown);
+ listbox.addEventListener("keypress", onKeypress);
+ SpecialPowers.addSystemEventListener(listbox, "keydown", onkeydownInSystemEventGroup, false);
+
+ listbox.focus();
+
+ [ false, true ].forEach(function (consume) {
+ doPreventDefault = consume;
+ for (var i = 0; i < listbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowDown");
+ check(true, false, "DownArrow key on listbox #" + i);
+ }
+
+ for (var i = 0; i < listbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowUp");
+ check(true, false, "'ArrowUp' key on listbox #" + i);
+ }
+
+ for (var i = 0; i < listbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowRight");
+ check(true, false, "'ArrowRight' key on listbox #" + i);
+ }
+
+ for (var i = 0; i < listbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowLeft");
+ check(true, false, "'ArrowLeft' key on listbox #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageDown");
+ check(true, false, "'PageDown' key on listbox #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageUp");
+ check(true, false, "'PageUp' key on listbox #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_End");
+ check(true, false, "'End' key on listbox #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_Home");
+ check(true, false, "'Home' key on listbox #" + i);
+ }
+
+ reset()
+ synthesizeKey("KEY_Enter");
+ check(false, true, "'Enter' key on listbox");
+
+ reset()
+ synthesizeKey("KEY_Escape");
+ check(false, false, "'Escape' key on listbox");
+
+ reset()
+ synthesizeKey("KEY_F4");
+ check(false, false, "F4 key on listbox");
+
+ reset()
+ sendString("a");
+ check(false, true, "'A' key on listbox");
+ });
+
+ listbox.removeEventListener("keydown", onKeydown);
+ listbox.removeEventListener("keypress", onKeypress);
+ SpecialPowers.removeSystemEventListener(listbox, "keydown", onkeydownInSystemEventGroup, false);
+
+
+
+ var multipleListbox = document.getElementById("multipleListbox");
+ multipleListbox.addEventListener("keydown", onKeydown);
+ multipleListbox.addEventListener("keypress", onKeypress);
+ SpecialPowers.addSystemEventListener(multipleListbox, "keydown", onkeydownInSystemEventGroup, false);
+
+ multipleListbox.focus();
+
+ [ false, true ].forEach(function (consume) {
+ doPreventDefault = consume;
+ for (var i = 0; i < multipleListbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowDown");
+ check(true, false, "'ArrowDown' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < multipleListbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowUp");
+ check(true, false, "'ArrowUp' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < multipleListbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowRight");
+ check(true, false, "'ArrowRight' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < multipleListbox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowLeft");
+ check(true, false, "'ArrowLeft' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageDown");
+ check(true, false, "'PageDown' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageUp");
+ check(true, false, "'PageUp' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_End");
+ check(true, false, "'End' key on multiple listbox #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_Home");
+ check(true, false, "'Home' key on multiple listbox #" + i);
+ }
+
+ reset()
+ synthesizeKey("KEY_Enter");
+ check(true, true, "'Enter' key on multiple listbox");
+
+ reset()
+ synthesizeKey("KEY_Escape");
+ check(false, false, "'Escape' key on multiple listbox");
+
+ reset()
+ synthesizeKey("KEY_F4");
+ check(false, false, "'F4' key on multiple listbox");
+
+ reset()
+ sendString("a");
+ check(false, true, "'A' key on multiple listbox");
+ });
+
+ multipleListbox.removeEventListener("keydown", onKeydown);
+ multipleListbox.removeEventListener("keypress", onKeypress);
+ SpecialPowers.removeSystemEventListener(multipleListbox, "keydown", onkeydownInSystemEventGroup, false);
+
+
+
+ var combobox = document.getElementById("combobox");
+ combobox.addEventListener("keydown", onKeydown);
+ combobox.addEventListener("keypress", onKeypress);
+ SpecialPowers.addSystemEventListener(combobox, "keydown", onkeydownInSystemEventGroup, false);
+
+ combobox.focus();
+
+ [ false, true ].forEach(function (consume) {
+ doPreventDefault = consume;
+ if (!kIsMac) {
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowDown");
+ check(true, false, "'ArrowDown' key on combobox #" + i);
+ }
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowUp");
+ check(true, false, "'ArrowUp' key on combobox #" + i);
+ }
+ } else {
+ todo(false, "Make this test work on OSX");
+ }
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowRight");
+ check(true, false, "'ArrowRight' key on combobox #" + i);
+ }
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowLeft");
+ check(true, false, "'ArrowLeft' key on combobox #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageDown");
+ check(true, false, "'PageDown' key on combobox #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageUp");
+ check(true, false, "'PageUp' key on combobox #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_End");
+ check(true, false, "'End' key on combobox #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_Home");
+ check(true, false, "'Home' key on combobox #" + i);
+ }
+
+ reset()
+ synthesizeKey("KEY_Enter");
+ check(false, true, "'Enter' key on combobox");
+
+ reset()
+ synthesizeKey("KEY_Escape");
+ check(false, false, "'Escape' key on combobox");
+
+ if (!kIsWin) {
+ reset()
+ synthesizeKey("KEY_F4");
+ check(false, false, "'F4' key on combobox");
+ }
+
+ reset()
+ sendString("a");
+ check(false, true, "'A' key on combobox");
+ });
+
+ function finish()
+ {
+ combobox.removeEventListener("keydown", onKeydown);
+ combobox.removeEventListener("keypress", onKeypress);
+ SpecialPowers.removeSystemEventListener(combobox, "keydown", onkeydownInSystemEventGroup, false);
+ SimpleTest.finish();
+ }
+
+ // Mac uses native popup for dropdown. Let's skip the tests for popup
+ // since it's not handled in nsListControlFrame.
+ // Similarly, Android doesn't use popup for dropdown.
+ if (kIsMac || kIsAndroid) {
+ finish();
+ return;
+ }
+
+ function testDropDown(aCallback)
+ {
+ testOpenDropDown(function () {
+ reset()
+ synthesizeKey("KEY_ArrowDown", {altKey: true});
+ }, function () {
+ check(true, false, "Alt + DownArrow key on combobox at opening dropdown");
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowDown");
+ check(true, false, "'ArrowDown' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowUp");
+ check(true, false, "'ArrowUp' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowRight");
+ check(true, false, "'ArrowRight' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < combobox.options.length + 1; i++) {
+ reset()
+ synthesizeKey("KEY_ArrowLeft");
+ check(true, false, "'ArrowLeft' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageDown");
+ check(true, false, "'PageDown' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < 4; i++) {
+ reset()
+ synthesizeKey("KEY_PageUp");
+ check(true, false, "'PageUp' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_End");
+ check(true, false, "'End' key on combobox during dropdown open #" + i);
+ }
+
+ for (var i = 0; i < 2; i++) {
+ reset()
+ synthesizeKey("KEY_Home");
+ check(true, false, "'Home' key on combobox during dropdown open #" + i);
+ }
+
+ testCloseDropDown(function () {
+ reset()
+ synthesizeKey("KEY_Enter");
+ }, function () {
+ testOpenDropDown(function () {
+ check(true, true, "'Enter' key on combobox at closing dropdown");
+
+ synthesizeKey("KEY_ArrowUp", {altKey: true});
+ }, function () {
+ check(true, false, "'Alt' + 'ArrowUp' key on combobox at opening dropdown");
+
+ testCloseDropDown(function () {
+ reset()
+ synthesizeKey("KEY_Escape");
+ }, function () {
+ check(true, false, "'Escape' key on combobox at closing dropdown");
+
+ // F4 key opens/closes dropdown only on Windows. So, other platforms
+ // don't need to do anymore.
+ if (!kIsWin) {
+ aCallback();
+ return;
+ }
+
+ testOpenDropDown(function () {
+ reset()
+ synthesizeKey("KEY_F4");
+ }, function () {
+ check(true, false, "'F4' key on combobox at opening dropdown on Windows");
+
+ testCloseDropDown(function () {
+ reset()
+ synthesizeKey("KEY_F4");
+ }, function () {
+ check(true, false, "'F4' key on combobox at closing dropdown on Windows");
+
+ aCallback();
+ return;
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+
+ doPreventDefault = false;
+ testDropDown(function () {
+ // Even if keydown event is consumed by JS, opening/closing dropdown
+ // should work for a11y and security (e.g., cannot close dropdown causes
+ // staying top-most window on the screen). If it's blocked by JS, this
+ // test would cause permanent timeout.
+ doPreventDefault = true;
+ testDropDown(finish);
+ });
+}
+
+function testOpenDropDown(aTest, aOnOpenDropDown)
+{
+ document.addEventListener("popupshowing", function (aEvent) {
+ document.removeEventListener(aEvent.type, arguments.callee);
+ setTimeout(aOnOpenDropDown, 0);
+ });
+ aTest();
+}
+
+function testCloseDropDown(aTest, aOnCloseDropDown)
+{
+ document.addEventListener("popuphiding", function (aEvent) {
+ document.removeEventListener(aEvent.type, arguments.callee);
+ setTimeout(aOnCloseDropDown, 0)
+ });
+ aTest();
+}
+
+SimpleTest.waitForFocus(runTests);
+</script>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug957562.html b/layout/forms/test/test_bug957562.html
new file mode 100644
index 0000000000..52821d8757
--- /dev/null
+++ b/layout/forms/test/test_bug957562.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=957562
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 903715</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957562">Mozilla Bug 957562</a>
+<p id="display"></p>
+<input id="n" onfocus="kill()" type="number" style="border:20px solid black">
+<pre id="test">
+</pre>
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+var killed = false;
+function kill() {
+ if (killed) {
+ return;
+ }
+ killed = true;
+ n.style.display = 'none';
+ r = n.getBoundingClientRect();
+ setTimeout(function() {
+ ok(true, "Didn't crash");
+ SimpleTest.finish();
+ }, 0);
+}
+
+function runTests()
+{
+ synthesizeMouse(n, 2, 2, {});
+}
+</script>
+</body>
+</html>
diff --git a/layout/forms/test/test_bug960277.html b/layout/forms/test/test_bug960277.html
new file mode 100644
index 0000000000..28981b121a
--- /dev/null
+++ b/layout/forms/test/test_bug960277.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=960277
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 903715</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=960277">Mozilla Bug 960277</a>
+<p id="display"></p>
+<fieldset style="position:relative; height:100px; margin:50px; background:blue;">
+<legend style="background:purple">
+ <div id="d" style="position:absolute; background:yellow; top:0; left:0; width:50px; height:50px;"></div>
+</legend>
+</fieldset>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+var rect = d.getBoundingClientRect();
+is(document.elementFromPoint(rect.left + 10, rect.top + 10), d,
+ "Hit testing yellow div");
+</script>
+</body>
+</html>
diff --git a/layout/forms/test/test_listcontrol_search.html b/layout/forms/test/test_listcontrol_search.html
new file mode 100644
index 0000000000..d696edae66
--- /dev/null
+++ b/layout/forms/test/test_listcontrol_search.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=849438
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for &lt;select&gt; list control search</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** This test will focus a select element and press a key that matches the
+ first non-space character of an entry. **/
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var select = document.getElementsByTagName('select')[0];
+ select.focus();
+ sendString('a');
+
+ is(select.options[0].selected, false, "the first option isn't selected");
+ is(select.options[1].selected, true, "the second option is selected");
+
+ select.blur();
+
+ SimpleTest.finish();
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=849438">Mozilla Bug 849438</a>
+<p id="display"></p>
+<div id="content">
+ <select>
+ <option>Please select an entry</option>
+ <option>&nbsp;a</option>
+ <option>&nbsp;b</option>
+ </select>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_readonly.html b/layout/forms/test/test_readonly.html
new file mode 100644
index 0000000000..3bfb768f9c
--- /dev/null
+++ b/layout/forms/test/test_readonly.html
@@ -0,0 +1,58 @@
+<!doctype html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="should-apply">
+ <textarea></textarea>
+ <input type="text">
+ <input type="password">
+ <input type="search">
+ <input type="tel">
+ <input type="email">
+ <input type="url">
+ <input type="number">
+ <input type="date">
+ <input type="time">
+ <input type="month">
+ <input type="week">
+ <input type="datetime-local">
+</div>
+<div id="should-not-apply">
+ <input type="hidden">
+ <input type="button">
+ <input type="image">
+ <input type="reset">
+ <input type="submit">
+ <input type="radio">
+ <input type="file">
+ <input type="checkbox">
+ <input type="range">
+ <input type="color">
+</div>
+<script>
+for (const element of Array.from(document.querySelectorAll('#should-apply *'))) {
+ let elementDesc = element.tagName.toLowerCase();
+ if (elementDesc === "input")
+ elementDesc += ` type="${element.type}"`;
+ test(function() {
+ assert_false(element.matches(':read-only'), "Shouldn't be initially read-only");
+ assert_true(element.matches(':read-write'), "Thus should be read-write");
+ element.setAttribute("readonly", "readonly");
+ assert_true(element.matches(':read-only'), "Should become read-only");
+ assert_false(element.matches(':read-write'), "Thus should stop being read-write");
+ }, elementDesc);
+}
+
+for (const element of Array.from(document.querySelectorAll('#should-not-apply *'))) {
+ let elementDesc = element.tagName.toLowerCase();
+ if (elementDesc === "input")
+ elementDesc += ` type="${element.type}"`;
+ test(function() {
+ assert_true(element.matches(':read-only'), "Should match read-only");
+ assert_false(element.matches(':read-write'), "Should not be read-write");
+ element.setAttribute("readonly", "readonly");
+ assert_true(element.matches(':read-only'), "Should keep matching read-only");
+ assert_false(element.matches(':read-write'), "Should still not be read-write");
+ }, elementDesc);
+}
+</script>
diff --git a/layout/forms/test/test_select_collapsed_page_keys.html b/layout/forms/test/test_select_collapsed_page_keys.html
new file mode 100644
index 0000000000..08c1615152
--- /dev/null
+++ b/layout/forms/test/test_select_collapsed_page_keys.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test for page up/down in collapsed select (bug 1488828)</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ function test() {
+ let select = document.getElementById("select");
+ select.focus();
+ is(select.selectedIndex, 0, "Option 0 initially selected");
+ synthesizeKey("KEY_PageDown", {});
+ ok(select.selectedIndex >= 2, "PageDown skips more than 1 option");
+ ok(select.selectedIndex < 49, "PageDown does not move to the last option");
+ synthesizeKey("KEY_PageUp", {});
+ is(select.selectedIndex, 0, "PageUp skips more than 1 option");
+ SimpleTest.finish();
+ }
+</script>
+</head>
+<body onload="test()">
+<div>
+ <select id="select" size="1">
+ <option selected>0</option>
+ </select>
+ <script>
+ // Add more options so we have 50 in total.
+ let select = document.getElementById("select");
+ for (let i = 1; i <= 49; ++i) {
+ let option = document.createElement("option");
+ option.textContent = i;
+ select.appendChild(option);
+ }
+ </script>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_select_key_navigation_bug1498769.html b/layout/forms/test/test_select_key_navigation_bug1498769.html
new file mode 100644
index 0000000000..3574761e66
--- /dev/null
+++ b/layout/forms/test/test_select_key_navigation_bug1498769.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1498769
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 1498769</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+ /** Test for Bug 1498769 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function test() {
+ const kIsMac = navigator.platform.indexOf("Mac") == 0;
+ SimpleTest.waitForFocus(function() {
+ [...document.querySelectorAll('select')].forEach(function(e) {
+ e.focus();
+ const description = ` (size ${e.size})`;
+ is(e.selectedIndex, 1, "the 'selected' attribute is respected" + description);
+ if (kIsMac && e.size == "1") {
+ // On OSX, UP/DOWN opens the dropdown menu rather than changing
+ // the value so we skip the rest of this test there in this case.
+ return;
+ }
+ synthesizeKey("VK_DOWN", {});
+ is(e.selectedIndex, 2, "VK_DOWN selected the first option below" + description);
+ synthesizeKey("VK_UP", {});
+ is(e.selectedIndex, 0, "VK_UP skips the display:none/contents option" + description);
+ synthesizeKey("VK_DOWN", {});
+ is(e.selectedIndex, 2, "VK_DOWN skips the display:none/contents option" + description);
+ });
+ SimpleTest.finish();
+ });
+ }
+</script>
+</head>
+<body onload="test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1498769">Mozilla Bug 1498769</a>
+<div>
+ <select size="4">
+ <option>0</option>
+ <option selected style="display:none">1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="4">
+ <option>0</option>
+ <option selected style="display:contents">1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="4">
+ <option>0</option>
+ <optgroup label="group" style="display:none">
+ <option selected>1</option>
+ </optgroup>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="4">
+ <option>0</option>
+ <optgroup label="group" style="display:contents">
+ <option selected>1</option>
+ </optgroup>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="4">
+ <option>0</option>
+ <optgroup label="group" style="display:contents">
+ <option selected style="display:none">1</option>
+ </optgroup>
+ <option>2</option>
+ <option>3</option>
+ </select>
+
+<!-- Same as above but with size="1" -->
+
+ <select size="1">
+ <option>0</option>
+ <option selected style="display:none">1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="1">
+ <option>0</option>
+ <option selected style="display:contents">1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="1">
+ <option>0</option>
+ <optgroup label="group" style="display:none">
+ <option selected>1</option>
+ </optgroup>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="1">
+ <option>0</option>
+ <optgroup label="group" style="display:contents">
+ <option selected>1</option>
+ </optgroup>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ <select size="1">
+ <option>0</option>
+ <optgroup label="group" style="display:contents">
+ <option selected style="display:none">1</option>
+ </optgroup>
+ <option>2</option>
+ <option>3</option>
+ </select>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_select_key_navigation_bug961363.html b/layout/forms/test/test_select_key_navigation_bug961363.html
new file mode 100644
index 0000000000..7815149778
--- /dev/null
+++ b/layout/forms/test/test_select_key_navigation_bug961363.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=961363
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 961363</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+ /** Test for Bug 961363 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function test() {
+ SimpleTest.waitForFocus(function() {
+ const single_list = [
+ {key: "DOWN", change: true, state: [false, false, true, false]},
+ {key: "UP", change: true, state: [false, true, false, false]},
+ {key: "RIGHT", change: true, state: [false, false, true, false]},
+ {key: "LEFT", change: true, state: [false, true, false, false]},
+ {key: "END", change: true, state: [false, false, false, true ]},
+ {key: "HOME", change: true, state: [true, false, false, false]},
+ {key: "PAGE_DOWN", change: false, state: [true, false, false, false]},
+ {key: "PAGE_UP", change: false, state: [true, false, false, false]}
+ ];
+
+ const single_dropdown = [
+ {key: "DOWN", change: true, state: [false, false, true, false]},
+ {key: "UP", change: true, state: [false, true, false, false]},
+ {key: "RIGHT", change: true, state: [false, false, true, false]},
+ {key: "LEFT", change: true, state: [false, true, false, false]},
+ {key: "END", change: true, state: [false, false, false, true ]},
+ {key: "HOME", change: true, state: [true, false, false, false]},
+ {key: "PAGE_DOWN", change: false, state: [true, false, false, false]},
+ {key: "PAGE_UP", change: false, state: [true, false, false, false]}
+ ];
+
+ const multiple = [
+ {key: "DOWN", change: false, state: [false, true, true, false]},
+ {key: "UP", change: false, state: [false, false, true, false]},
+ {key: "RIGHT", change: false, state: [false, false, false, false]},
+ {key: "LEFT", change: false, state: [false, true, false, false]},
+ {key: "PAGE_DOWN", change: false, state: [false, true, false, true ]},
+ {key: "PAGE_UP", change: false, state: [false, false, false, true ]},
+ {key: "END", change: false, state: [false, false, false, false]},
+ {key: "HOME", change: false, state: [true, false, false, false]}
+ ];
+
+ function select_test(id, tests) {
+ let element = document.getElementById(id);
+ element.focus();
+ tests.forEach(data => {
+ let previousValue = element.value;
+ let key = data.k;
+ synthesizeKey("VK_" + data.key, {shiftKey: false, metaKey: false,
+ ctrlKey: true });
+ (data.change ? isnot : is)(
+ element.value, previousValue,
+ `value should ${data.change ? "": "not "} have changed while testing CTRL+${data.key} (id: ${id})`
+ );
+
+ // Hit ctrl+space, but only for <select multiple> elements; doing so
+ // for single <select> elements will just trigger the dropdown to
+ // open. This is especially important because e10s-backed dropdowns
+ // behave differently: their .value isn't updated until the dropdown
+ // is closed (and the change confirmed), e.g. by pressing Enter.
+ let action;
+ if (element.multiple) {
+ synthesizeKey(" ", {shiftKey: false, metaKey: false,
+ ctrlKey: true});
+ action = `CTRL+SPACE (after testing CTRL+${data.key})`;
+ } else {
+ action = `testing CTRL+${data.key}`;
+ }
+
+ let selected = [...element.options].map(o => o.selected);
+ is(selected.toString(), data.state.toString(),
+ `selected options match after ${action} (id: ${id})`);
+ });
+ }
+
+ select_test("single-list", single_list);
+ if (!navigator.platform.includes("Mac")) {
+ select_test("single-dropdown", single_dropdown);
+ } else {
+ todo(false, "Make these tests work on OSX");
+ }
+
+ select_test("multiple", multiple);
+ SimpleTest.finish();
+ });
+ }
+</script>
+</head>
+<body onload="test();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=961363">Mozilla Bug 961363</a>
+<div>
+ <ul>
+ <li>
+ <select id="single-list" size="3">
+ <option>0</option>
+ <option selected>1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ </li>
+ <li>
+ <select id="single-dropdown" size="1">
+ <option>0</option>
+ <option selected>1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ </li>
+ <li>
+ <select id="multiple" multiple size="3">
+ <option>0</option>
+ <option selected>1</option>
+ <option>2</option>
+ <option>3</option>
+ </select>
+ </li>
+ </ul>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_select_prevent_default.html b/layout/forms/test/test_select_prevent_default.html
new file mode 100644
index 0000000000..4ad9db0580
--- /dev/null
+++ b/layout/forms/test/test_select_prevent_default.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=291082
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 291082</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+ /** Test for Bug 291082 **/
+
+
+ SimpleTest.waitForExplicitFinish();
+
+ function preventDefault(event) {
+ event.preventDefault();
+ }
+
+ function runTest() {
+ document.getElementById("keydown").addEventListener("keydown", preventDefault);
+ document.getElementById("keypress").addEventListener("keypress", preventDefault);
+
+ SimpleTest.waitForFocus(function() {
+ if (navigator.platform.indexOf("Mac") == 0) {
+ todo(false, "Make this test work on OSX");
+ SimpleTest.finish();
+ return;
+ }
+ var testData = [ "one", "two", "three", "four", "keydown", "keypress" ];
+
+ // The order of the keys in otherKeys is important for the test to function properly.
+ var otherKeys = [ "DOWN", "UP", "RIGHT", "LEFT", "PAGE_DOWN", "PAGE_UP",
+ "END", "HOME" ];
+
+ testData.forEach(function(id) {
+ var element = document.getElementById(id);
+ element.focus();
+ var previousValue = element.value;
+ sendChar('2');
+ is(element.value, previousValue, "value should not have changed (id: " + id + ")");
+ previousValue = element.value;
+ otherKeys.forEach(function(key) {
+ sendKey(key);
+ // All these preventDefault on key down in various ways.
+ let shouldchange = id != "keydown" && id != "one" && id != "three";
+ (shouldchange ? isnot : is)(element.value, previousValue, "value should " + (shouldchange ? "" : "not ") + "have changed while testing key " + key + " (id: " + id + ")");
+ previousValue = element.value;
+ });
+ });
+ SimpleTest.finish();
+ });
+ }
+</script>
+</head>
+<body onload="runTest();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291082">Mozilla Bug 291082</a>
+<div>
+ <ul>
+ <li>
+ <select id="one" onkeydown="event.preventDefault();">
+ <option>0</option>
+ <option>1</option>
+ <option>2</option>
+ </select>
+ select onkeydown="event.preventDefault();"
+ </li>
+ <li>
+ <select id="two" onkeypress="event.preventDefault();">
+ <option>0</option>
+ <option>1</option>
+ <option>2</option>
+ </select>
+ select onkeypress="event.preventDefault();"
+ </li>
+ <li onkeydown="event.preventDefault();">
+ <select id="three">
+ <option>0</option>
+ <option>1</option>
+ <option>2</option>
+ </select>
+ li onkeydown="event.preventDefault();"
+ </li>
+ <li onkeypress="event.preventDefault();">
+ <select id="four">
+ <option>0</option>
+ <option>1</option>
+ <option>2</option>
+ </select>
+ li onkeypress="event.preventDefault();"
+ </li>
+ <li>
+ <select id="keydown">
+ <option>0</option>
+ <option>1</option>
+ <option>2</option>
+ </select>
+ select.addEventListener("keydown", function(event) { event.preventDefault(); });
+ </li>
+ <li>
+ <select id="keypress">
+ <option>0</option>
+ <option>1</option>
+ <option>2</option>
+ <option>9</option>
+ </select>
+ select.addEventListener("keypress", function(event) { event.preventDefault(); });
+ </li>
+ </ul>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_select_reframe.html b/layout/forms/test/test_select_reframe.html
new file mode 100644
index 0000000000..666d8074b8
--- /dev/null
+++ b/layout/forms/test/test_select_reframe.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Test for page up/down in collapsed select (bug 1488828)</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<style>
+.reframe {
+ display: flex;
+}
+</style>
+<div id="container">
+ <select>
+ <option>ABC</option>
+ <option>DEF</option>
+ </select>
+</div>
+<script>
+(async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const utils = SpecialPowers.DOMWindowUtils;
+ const select = document.querySelector("select");
+ await SimpleTest.promiseFocus(window);
+
+ ok(!select.openInParentProcess, "Should not be open")
+
+ select.focus();
+ synthesizeKey("VK_SPACE");
+
+ ok(SpecialPowers.wrap(select).openInParentProcess, "Should open");
+
+ const container = document.getElementById("container");
+ container.getBoundingClientRect(); // flush layout
+
+ const frameCountBeforeReframe = utils.framesConstructed;
+
+ container.classList.add("reframe");
+
+ container.getBoundingClientRect(); // flush layout
+
+ ok(utils.framesConstructed > frameCountBeforeReframe, "Should have reframed");
+ ok(SpecialPowers.wrap(select).openInParentProcess, "Should remain open");
+
+ select.remove();
+
+ container.getBoundingClientRect(); // flush layout
+ ok(!SpecialPowers.wrap(select).openInParentProcess, "Should close after removal");
+
+ SimpleTest.finish();
+}());
+</script>
diff --git a/layout/forms/test/test_select_vertical.html b/layout/forms/test/test_select_vertical.html
new file mode 100644
index 0000000000..51c6208bd3
--- /dev/null
+++ b/layout/forms/test/test_select_vertical.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for select popup in vertical writing mode</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ SimpleTest.waitForFocus(function() {
+ var vlr = document.getElementById("vlr");
+ var selWidth = vlr.offsetWidth;
+ var optWidth = vlr.children[0].offsetWidth;
+
+ // We should be able to choose options from the vertical-lr <select>
+ // at positions increasingly to the right of the element itself.
+ is(vlr.value, "1", "(vertical-lr) initial value should be 1");
+
+ synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 });
+ synthesizeMouse(vlr, selWidth + 1.5 * optWidth, 5, { type : "mouseup", button: 0 });
+
+ is(vlr.value, "2", "(vertical-lr) new value should be 2");
+
+ synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 });
+ synthesizeMouse(vlr, selWidth + 2.5 * optWidth, 5, { type : "mouseup", button: 0 });
+
+ is(vlr.value, "3", "(vertical-lr) new value should be 3");
+
+ synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 });
+ synthesizeMouse(vlr, selWidth + 0.5 * optWidth, 5, { type : "mouseup", button: 0 });
+
+ is(vlr.value, "1", "(vertical-lr) value should be back to 1");
+
+ var vrl = document.getElementById("vrl");
+
+ // We should be able to choose options from the vertical-rl <select>
+ // at positions increasingly to the left of the element itself.
+ is(vrl.value, "1", "(vertical-rl) initial value should be 1");
+
+ synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 });
+ synthesizeMouse(vrl, -1.5 * optWidth, 5, { type : "mouseup", button: 0 });
+
+ is(vrl.value, "2", "(vertical-rl) new value should be 2");
+
+ synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 });
+ synthesizeMouse(vrl, -2.5 * optWidth, 5, { type : "mouseup", button: 0 });
+
+ is(vrl.value, "3", "(vertical-rl) new value should be 3");
+
+ synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 });
+ synthesizeMouse(vrl, -0.5 * optWidth, 5, { type : "mouseup", button: 0 });
+
+ is(vrl.value, "1", "(vertical-rl) value should be back to 1");
+
+ SimpleTest.finish();
+ });
+}
+</script>
+
+<body onload="test();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1113206">Mozilla Bug 1113206</a>
+<div>
+ <select id="vlr" style="writing-mode: vertical-lr; margin: 80px;">
+ <option value="1">one
+ <option value="2">two
+ <option value="3">three
+ <option value="4">four
+ </select>
+ <select id="vrl" style="writing-mode: vertical-rl; margin: 80px;">
+ <option value="1">one
+ <option value="2">two
+ <option value="3">three
+ <option value="4">four
+ </select>
+</div>
diff --git a/layout/forms/test/test_textarea_resize.html b/layout/forms/test/test_textarea_resize.html
new file mode 100644
index 0000000000..93889a0d17
--- /dev/null
+++ b/layout/forms/test/test_textarea_resize.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 477700</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="content" style="display: none">
+</div>
+
+<textarea id="textarea" style="-moz-appearance: none; border: 2px solid black; padding: 3px; box-sizing: border-box; min-width: 15px; min-height: 15px;">Text</textarea>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for textbox resizing **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(() => SimpleTest.executeSoon(doTheTest));
+
+// -1 means use the default value which is 'both', then test explicitly
+// setting each possible value.
+var currentResize = -1;
+var currentBoxSizing = 0;
+var currentPointer = 0;
+var resizeTypes = [ "horizontal", "vertical", "none", "inherit", "both" ];
+var boxSizingTypes = [ "", "border-box" ];
+var pointerTypes = [ synthesizeMouse, synthesizeTouch]
+
+function doTheTest() {
+ runTest(pointerTypes[currentPointer]);
+}
+
+function runTest(aPointerFunc) {
+ var boxSizingText = " with box sizing " + (currentBoxSizing ? boxSizingTypes[currentBoxSizing] : "content-box");
+
+ var textarea = $("textarea");
+ var rect = textarea.getBoundingClientRect();
+ var touch = aPointerFunc.name.match(/Touch/);
+ // -1 means use the default value of resize, i.e. "both"
+ var type = (currentResize == -1) ? "both" : resizeTypes[currentResize];
+ // assume that the resizer is in the lower right corner
+
+ aPointerFunc(textarea, rect.width - 10, rect.height - 10, { type: touch ? "touchstart" : "mousedown" });
+ aPointerFunc(textarea, rect.width + 40, rect.height + 40, { type: touch ? "touchmove" : "mousemove" });
+
+ var newrect = textarea.getBoundingClientRect();
+ var hchange = (type == "both" || type == "horizontal");
+ var vchange = (type == "both" || type == "vertical");
+
+ is(Math.round(newrect.width), Math.round(rect.width + (hchange ? 50 : 0)),
+ type + " width has increased" + boxSizingText + " using " + aPointerFunc.name);
+ is(Math.round(newrect.height), Math.round(rect.height + (vchange ? 50 : 0)),
+ type + " height has increased" + boxSizingText + " using " + aPointerFunc.name);
+
+ aPointerFunc(textarea, rect.width - 20, rect.height - 20, { type: touch ? "touchmove" : "mousemove" });
+
+ newrect = textarea.getBoundingClientRect();
+
+ is(Math.round(newrect.width), Math.round(rect.width - (hchange ? 10 : 0)),
+ type + " width has decreased" + boxSizingText + " using " + aPointerFunc.name);
+ is(Math.round(newrect.height), Math.round(rect.height - (vchange ? 10 : 0)),
+ type + " height has decreased" + boxSizingText + " using " + aPointerFunc.name);
+
+ aPointerFunc(textarea, rect.width - 220, rect.height - 220, { type: touch ? "touchmove" : "mousemove" });
+
+ newrect = textarea.getBoundingClientRect();
+ ok(hchange ? newrect.width >= 15 : Math.round(newrect.width) == Math.round(rect.width),
+ type + " width decreased below minimum" + boxSizingText + " using " + newrect.width);
+ ok(vchange ? newrect.height >= 15 : Math.round(newrect.height) == Math.round(rect.height),
+ type + " height decreased below minimum" + boxSizingText + " using " + aPointerFunc.name);
+
+ aPointerFunc(textarea, rect.width - 8, rect.height - 8, { type: touch ? "touchend" : "mouseup" });
+
+ textarea.style.width = "auto";
+ textarea.style.height = "auto";
+
+ if (currentBoxSizing++ <= boxSizingTypes.length) {
+ textarea.style.MozBoxSizing = boxSizingTypes[currentBoxSizing];
+ SimpleTest.executeSoon(doTheTest);
+ } else {
+ currentBoxSizing = 0;
+ if (++currentResize < resizeTypes.length) {
+ textarea.style.resize = resizeTypes[currentResize];
+ SimpleTest.executeSoon(doTheTest);
+ } else {
+ currentResize = -1;
+ textarea.style.resize = "";
+ if (++currentPointer < pointerTypes.length) {
+ SimpleTest.executeSoon(doTheTest);
+ } else {
+ SimpleTest.finish();
+ }
+ }
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/forms/test/test_unstyled_control_height.html b/layout/forms/test/test_unstyled_control_height.html
new file mode 100644
index 0000000000..ad5cd125d2
--- /dev/null
+++ b/layout/forms/test/test_unstyled_control_height.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style>
+ #container *, #container2 * {
+ white-space: nowrap;
+ appearance: none;
+ }
+ input {
+ /* Reduce the width so that container can fit all its children in 600px viewport width. */
+ width: 100px;
+ }
+ /* date by default uses a monospace font, which might have different metrics */
+ input, button, select {
+ font: Menu;
+ }
+ .small-font * {
+ font-size: 8px !important; /* important to override rule above */
+ }
+ .no-padding * {
+ padding: 0;
+ }
+</style>
+
+<!-- Each container should fit all its children in the same line to verify every
+ child has the same |top|. -->
+<div id="container">
+ <input>
+ <!-- Putting the <input> containing Burmese characters here is just to verify
+ our current behavior. They are slightly clipped. So if we fix it by
+ making the <input> taller, it's OK to remove it from this test. -->
+ <input value="漢字 jpg မြန်မာစာ">
+ <input type=date>
+ <button>Foo</button>
+ <select><option>Foo</option></select>
+</div>
+
+<br>
+<div id="container2">
+ <button>漢字 Foo မြန်မာစာ</button>
+ <select><option>漢字 Foo မြန်မာစာ</option></select>
+</div>
+
+<script>
+function testHeightMatches(id, desc) {
+ let commonHeight = null;
+ let commonTop = null;
+ for (let element of document.querySelectorAll(`#${id} > *`)) {
+ let rect = element.getBoundingClientRect();
+ if (commonHeight === null) {
+ commonHeight = rect.height;
+ commonTop = rect.top;
+ }
+ is(rect.height, commonHeight, `Height of the controls should match for ${element.outerHTML}${desc}`);
+ is(rect.top, commonTop, `Top of the controls should match for ${element.outerHTML}${desc}`);
+ }
+}
+
+for (id of ["container", "container2"]) {
+ const container = document.getElementById(id);
+
+ testHeightMatches(id, "");
+
+ container.className = "no-padding";
+
+ testHeightMatches(id, " without padding");
+
+ container.className = "small-font";
+
+ testHeightMatches(id, " with an small font");
+}
+</script>