diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index 7557d15a15..f03f21c3f7 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -557,7 +557,7 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks {
transition: none;
}
-#DateTimePickerPanel {
+#DateTimePickerPanel[active="true"] {
-moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup");
}
diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul
index 9a4fa0fd52..e8567eb5c2 100644
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -160,8 +160,10 @@
hidden="true"
orient="vertical"
noautofocus="true"
+ noautohide="true"
consumeoutsideclicks="false"
- level="parent">
+ level="parent"
+ tabspecific="true">
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
index e8a4af454c..62f194493a 100644
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -543,9 +543,8 @@ GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData,
bool
HTMLInputElement::ValueAsDateEnabled(JSContext* cx, JSObject* obj)
{
- return Preferences::GetBool("dom.experimental_forms", false) ||
- Preferences::GetBool("dom.forms.datepicker", false) ||
- Preferences::GetBool("dom.forms.datetime", false);
+ return IsExperimentalFormsEnabled() || IsDatePickerEnabled() ||
+ IsInputDateTimeEnabled();
}
NS_IMETHODIMP
@@ -628,7 +627,7 @@ HTMLInputElement::nsFilePickerShownCallback::Done(int16_t aResult)
RefPtr dispatchChangeEventCallback =
new DispatchChangeEventCallback(mInput);
- if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ if (IsWebkitDirPickerEnabled() &&
mInput->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
ErrorResult error;
GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error);
@@ -827,7 +826,7 @@ HTMLInputElement::IsPopupBlocked() const
nsresult
HTMLInputElement::InitDatePicker()
{
- if (!Preferences::GetBool("dom.forms.datepicker", false)) {
+ if (!IsDatePickerEnabled()) {
return NS_OK;
}
@@ -2516,10 +2515,8 @@ bool
HTMLInputElement::IsExperimentalMobileType(uint8_t aType)
{
return (aType == NS_FORM_INPUT_DATE &&
- !Preferences::GetBool("dom.forms.datetime", false) &&
- !Preferences::GetBool("dom.forms.datepicker", false)) ||
- (aType == NS_FORM_INPUT_TIME &&
- !Preferences::GetBool("dom.forms.datetime", false));
+ !IsInputDateTimeEnabled() && !IsDatePickerEnabled()) ||
+ (aType == NS_FORM_INPUT_TIME && !IsInputDateTimeEnabled());
}
bool
@@ -3025,8 +3022,8 @@ HTMLInputElement::GetDisplayFileName(nsAString& aValue) const
nsXPIDLString value;
if (mFilesOrDirectories.IsEmpty()) {
- if ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) ||
- (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ if ((IsDirPickerEnabled() && Allowdirs()) ||
+ (IsWebkitDirPickerEnabled() &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"NoDirSelected", value);
@@ -3055,7 +3052,7 @@ HTMLInputElement::SetFilesOrDirectories(const nsTArray& a
{
ClearGetFilesHelpers();
- if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) {
+ if (IsWebkitFileSystemEnabled()) {
HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this);
mEntries.Clear();
}
@@ -3074,7 +3071,7 @@ HTMLInputElement::SetFiles(nsIDOMFileList* aFiles,
mFilesOrDirectories.Clear();
ClearGetFilesHelpers();
- if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) {
+ if (IsWebkitFileSystemEnabled()) {
HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this);
mEntries.Clear();
}
@@ -3097,14 +3094,14 @@ HTMLInputElement::MozSetDndFilesAndDirectories(const nsTArray dispatchChangeEventCallback =
new DispatchChangeEventCallback(this);
- if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ if (IsWebkitDirPickerEnabled() &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
ErrorResult rv;
GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */,
@@ -3182,8 +3179,8 @@ HTMLInputElement::GetFiles()
return nullptr;
}
- if (Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs() &&
- (!Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) ||
+ if (IsDirPickerEnabled() && Allowdirs() &&
+ (!IsWebkitDirPickerEnabled() ||
!HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
return nullptr;
}
@@ -4366,8 +4363,8 @@ HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor)
do_QueryInterface(aVisitor.mEvent->mOriginalTarget);
if (target &&
target->FindFirstNonChromeOnlyAccessContent() == this &&
- ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) ||
- (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ ((IsDirPickerEnabled() && Allowdirs()) ||
+ (IsWebkitDirPickerEnabled() &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)))) {
type = FILE_PICKER_DIRECTORY;
}
@@ -5735,20 +5732,133 @@ HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult)
return true;
}
-static bool
-IsDateTimeEnabled(int32_t aNewType)
+/* static */ bool
+HTMLInputElement::IsDateTimeTypeSupported(uint8_t aDateTimeInputType)
{
- return (aNewType == NS_FORM_INPUT_DATE &&
- (Preferences::GetBool("dom.forms.datetime", false) ||
- Preferences::GetBool("dom.experimental_forms", false) ||
- Preferences::GetBool("dom.forms.datepicker", false))) ||
- (aNewType == NS_FORM_INPUT_TIME &&
- (Preferences::GetBool("dom.forms.datetime", false) ||
- Preferences::GetBool("dom.experimental_forms", false))) ||
- ((aNewType == NS_FORM_INPUT_MONTH ||
- aNewType == NS_FORM_INPUT_WEEK ||
- aNewType == NS_FORM_INPUT_DATETIME_LOCAL) &&
- Preferences::GetBool("dom.forms.datetime", false));
+ return (aDateTimeInputType == NS_FORM_INPUT_DATE &&
+ (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled() ||
+ IsDatePickerEnabled())) ||
+ (aDateTimeInputType == NS_FORM_INPUT_TIME &&
+ (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled())) ||
+ ((aDateTimeInputType == NS_FORM_INPUT_MONTH ||
+ aDateTimeInputType == NS_FORM_INPUT_WEEK ||
+ aDateTimeInputType == NS_FORM_INPUT_DATETIME_LOCAL) &&
+ IsInputDateTimeEnabled());
+}
+
+/* static */ bool
+HTMLInputElement::IsWebkitDirPickerEnabled()
+{
+ static bool sWebkitDirPickerEnabled = false;
+ static bool sWebkitDirPickerPrefCached = false;
+ if (!sWebkitDirPickerPrefCached) {
+ sWebkitDirPickerPrefCached = true;
+ Preferences::AddBoolVarCache(&sWebkitDirPickerEnabled,
+ "dom.webkitBlink.dirPicker.enabled",
+ false);
+ }
+
+ return sWebkitDirPickerEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsWebkitFileSystemEnabled()
+{
+ static bool sWebkitFileSystemEnabled = false;
+ static bool sWebkitFileSystemPrefCached = false;
+ if (!sWebkitFileSystemPrefCached) {
+ sWebkitFileSystemPrefCached = true;
+ Preferences::AddBoolVarCache(&sWebkitFileSystemEnabled,
+ "dom.webkitBlink.filesystem.enabled",
+ false);
+ }
+
+ return sWebkitFileSystemEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsDirPickerEnabled()
+{
+ static bool sDirPickerEnabled = false;
+ static bool sDirPickerPrefCached = false;
+ if (!sDirPickerPrefCached) {
+ sDirPickerPrefCached = true;
+ Preferences::AddBoolVarCache(&sDirPickerEnabled, "dom.input.dirpicker",
+ false);
+ }
+
+ return sDirPickerEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsDatePickerEnabled()
+{
+ static bool sDatePickerEnabled = false;
+ static bool sDatePickerPrefCached = false;
+ if (!sDatePickerPrefCached) {
+ sDatePickerPrefCached = true;
+ Preferences::AddBoolVarCache(&sDatePickerEnabled, "dom.forms.datepicker",
+ false);
+ }
+
+ return sDatePickerEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsExperimentalFormsEnabled()
+{
+ static bool sExperimentalFormsEnabled = false;
+ static bool sExperimentalFormsPrefCached = false;
+ if (!sExperimentalFormsPrefCached) {
+ sExperimentalFormsPrefCached = true;
+ Preferences::AddBoolVarCache(&sExperimentalFormsEnabled,
+ "dom.experimental_forms",
+ false);
+ }
+
+ return sExperimentalFormsEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsInputDateTimeEnabled()
+{
+ static bool sDateTimeEnabled = false;
+ static bool sDateTimePrefCached = false;
+ if (!sDateTimePrefCached) {
+ sDateTimePrefCached = true;
+ Preferences::AddBoolVarCache(&sDateTimeEnabled, "dom.forms.datetime",
+ false);
+ }
+
+ return sDateTimeEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsInputNumberEnabled()
+{
+ static bool sInputNumberEnabled = false;
+ static bool sInputNumberPrefCached = false;
+ if (!sInputNumberPrefCached) {
+ sInputNumberPrefCached = true;
+ Preferences::AddBoolVarCache(&sInputNumberEnabled, "dom.forms.number",
+ false);
+ }
+
+ return sInputNumberEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsInputColorEnabled()
+{
+ static bool sInputColorEnabled = false;
+ static bool sInputColorPrefCached = false;
+ if (!sInputColorPrefCached) {
+ sInputColorPrefCached = true;
+ Preferences::AddBoolVarCache(&sInputColorEnabled, "dom.forms.color",
+ false);
+ }
+
+ return sInputColorEnabled;
}
bool
@@ -5766,12 +5876,11 @@ HTMLInputElement::ParseAttribute(int32_t aNamespaceID,
if (success) {
newType = aResult.GetEnumValue();
if ((IsExperimentalMobileType(newType) &&
- !Preferences::GetBool("dom.experimental_forms", false)) ||
- (newType == NS_FORM_INPUT_NUMBER &&
- !Preferences::GetBool("dom.forms.number", false)) ||
- (newType == NS_FORM_INPUT_COLOR &&
- !Preferences::GetBool("dom.forms.color", false)) ||
- (IsDateTimeInputType(newType) && !IsDateTimeEnabled(newType))) {
+ !IsExperimentalFormsEnabled()) ||
+ (newType == NS_FORM_INPUT_NUMBER && !IsInputNumberEnabled()) ||
+ (newType == NS_FORM_INPUT_COLOR && !IsInputColorEnabled()) ||
+ (IsDateTimeInputType(newType) &&
+ !IsDateTimeTypeSupported(newType))) {
newType = kInputDefaultType->value;
aResult.SetTo(newType, &aValue);
}
diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h
index 9ca876aee8..9baaffb63e 100644
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -1632,9 +1632,73 @@ private:
return IsSingleLineTextControl(false, aType) ||
aType == NS_FORM_INPUT_RANGE ||
aType == NS_FORM_INPUT_NUMBER ||
- aType == NS_FORM_INPUT_TIME;
+ aType == NS_FORM_INPUT_TIME ||
+ aType == NS_FORM_INPUT_DATE;
}
+ /**
+ * Checks if aDateTimeInputType should be supported based on "dom.forms.datetime",
+ * "dom.forms.datepicker" and "dom.experimental_forms".
+ */
+ static bool
+ IsDateTimeTypeSupported(uint8_t aDateTimeInputType);
+
+ /**
+ * Checks preference "dom.webkitBlink.dirPicker.enabled" to determine if
+ * webkitdirectory should be supported.
+ */
+ static bool
+ IsWebkitDirPickerEnabled();
+
+ /**
+ * Checks preference "dom.webkitBlink.filesystem.enabled" to determine if
+ * webkitEntries should be supported.
+ */
+ static bool
+ IsWebkitFileSystemEnabled();
+
+ /**
+ * Checks preference "dom.input.dirpicker" to determine if file and directory
+ * entries API should be supported.
+ */
+ static bool
+ IsDirPickerEnabled();
+
+ /**
+ * Checks preference "dom.forms.datepicker" to determine if date picker should
+ * be supported.
+ */
+ static bool
+ IsDatePickerEnabled();
+
+ /**
+ * Checks preference "dom.experimental_forms" to determine if experimental
+ * implementation of input element should be enabled.
+ */
+ static bool
+ IsExperimentalFormsEnabled();
+
+ /**
+ * Checks preference "dom.forms.datetime" to determine if input date/time
+ * related types should be supported.
+ */
+ static bool
+ IsInputDateTimeEnabled();
+
+ /**
+ * Checks preference "dom.forms.number" to determine if input type=number
+ * should be supported.
+ */
+ static bool
+ IsInputNumberEnabled();
+
+ /**
+ * Checks preference "dom.forms.color" to determine if date/time related
+ * types should be supported.
+ */
+ static bool
+ IsInputColorEnabled();
+
struct nsFilePickerFilter {
nsFilePickerFilter()
: mFilterMask(0) {}
diff --git a/dom/html/test/forms/mochitest.ini b/dom/html/test/forms/mochitest.ini
index 6fceefd981..199e4baf80 100644
--- a/dom/html/test/forms/mochitest.ini
+++ b/dom/html/test/forms/mochitest.ini
@@ -32,6 +32,8 @@ skip-if = android_version == '18' # Android, bug 1147974
skip-if = android_version == '18' # Android, bug 1147974
[test_input_date_key_events.html]
skip-if = os == "android"
+[test_input_datetime_input_change_events.html]
+skip-if = os == "android"
[test_input_datetime_focus_blur.html]
skip-if = os == "android"
[test_input_datetime_focus_blur_events.html]
diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html
index cd974e5057..f502d6a4d6 100644
--- a/dom/html/test/forms/test_input_date_key_events.html
+++ b/dom/html/test/forms/test_input_date_key_events.html
@@ -184,16 +184,16 @@ var testData = [
expectedVal: "2016-01-31"
},
{
- // Home key on year field sets it to the minimum year, which is 0001.
+ // Home key should have no effect on year field.
keys: ["VK_TAB", "VK_TAB", "VK_HOME"],
initialVal: "2016-01-01",
- expectedVal: "0001-01-01"
+ expectedVal: "2016-01-01"
},
{
- // End key on year field sets it to the maximum year, which is 275760.
+ // End key should have no effect on year field.
keys: ["VK_TAB", "VK_TAB", "VK_END"],
initialVal: "2016-01-01",
- expectedVal: "275760-01-01"
+ expectedVal: "2016-01-01"
},
];
diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html
new file mode 100644
index 0000000000..e636995d36
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_input_change_events.html
@@ -0,0 +1,88 @@
+
+
+
+
+Test for Bug 1370858
+
+
+
+
+
+Mozilla Bug 722599
+
+
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/reftest.list b/layout/reftests/forms/input/datetime/reftest.list
index 0ce2002bd3..a62d56c7c8 100644
--- a/layout/reftests/forms/input/datetime/reftest.list
+++ b/layout/reftests/forms/input/datetime/reftest.list
@@ -11,3 +11,14 @@ skip-if(!Android&&!B2G&&!Mulet) == time-simple-unthemed.html time-simple-untheme
# type change
skip-if(Android||B2G||Mulet) == to-time-from-other-type-unthemed.html time-simple-unthemed.html
skip-if(Android||B2G||Mulet) == from-time-to-other-type-unthemed.html from-time-to-other-type-unthemed-ref.html
+
+# content should not overflow on small width/height
+skip-if(Android) == time-small-width.html time-small-width-ref.html
+skip-if(Android) == time-small-height.html time-small-height-ref.html
+skip-if(Android) == time-small-width-height.html time-small-width-height-ref.html
+
+# content (text) should be left aligned
+skip-if(Android) == time-content-left-aligned.html time-content-left-aligned-ref.html
+
+# reset button should be right aligned
+skip-if(Android) fails-if(styloVsGecko) == time-reset-button-right-aligned.html time-reset-button-right-aligned-ref.html # bug 1372062
diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html
new file mode 100644
index 0000000000..ad8be9adc1
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned.html b/layout/reftests/forms/input/datetime/time-content-left-aligned.html
new file mode 100644
index 0000000000..aa910cddf9
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-content-left-aligned.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html
new file mode 100644
index 0000000000..3d36f20680
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html
new file mode 100644
index 0000000000..72d5cc140e
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-small-height-ref.html b/layout/reftests/forms/input/datetime/time-small-height-ref.html
new file mode 100644
index 0000000000..fcda93df9a
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-height-ref.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-small-height.html b/layout/reftests/forms/input/datetime/time-small-height.html
new file mode 100644
index 0000000000..3044822fe8
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-height.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-small-width-height-ref.html b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html
new file mode 100644
index 0000000000..0979243db0
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-small-width-height.html b/layout/reftests/forms/input/datetime/time-small-width-height.html
new file mode 100644
index 0000000000..a221b28195
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width-height.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-small-width-ref.html b/layout/reftests/forms/input/datetime/time-small-width-ref.html
new file mode 100644
index 0000000000..2379c70809
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width-ref.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/layout/reftests/forms/input/datetime/time-small-width.html b/layout/reftests/forms/input/datetime/time-small-width.html
new file mode 100644
index 0000000000..f76f7fdfa9
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/layout/style/res/forms.css b/layout/style/res/forms.css
index f045540b18..e7566e183f 100644
--- a/layout/style/res/forms.css
+++ b/layout/style/res/forms.css
@@ -1135,3 +1135,8 @@ input[type="number"] > div > div > div:hover {
/* give some indication of hover state for the up/down buttons */
background-color: lightblue;
}
+
+input[type="date"],
+input[type="time"] {
+ overflow: hidden !important;
+}
diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini
index 278b2ffe02..67ba2f850c 100644
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -26,6 +26,7 @@ skip-if = !e10s
[browser_contentTitle.js]
[browser_crash_previous_frameloader.js]
run-if = e10s && crashreporter
+[browser_datetime_datepicker.js]
[browser_default_image_filename.js]
[browser_f7_caret_browsing.js]
[browser_findbar.js]
diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js
new file mode 100644
index 0000000000..a0db761b7a
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js
@@ -0,0 +1,222 @@
+/* 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/. */
+
+"use strict";
+
+const MONTH_YEAR = ".month-year",
+ DAYS_VIEW = ".days-view",
+ BTN_PREV_MONTH = ".prev",
+ BTN_NEXT_MONTH = ".next";
+const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format;
+
+// Create a list of abbreviations for calendar class names
+const W = "weekend",
+ O = "outside",
+ S = "selection",
+ R = "out-of-range",
+ T = "today",
+ P = "off-step";
+
+// Calendar classlist for 2016-12. Used to verify the classNames are correct.
+const calendarClasslist_201612 = [
+ [W, O], [O], [O], [O], [], [], [W],
+ [W], [], [], [], [], [], [W],
+ [W], [], [], [], [S], [], [W],
+ [W], [], [], [], [], [], [W],
+ [W], [], [], [], [], [], [W],
+ [W, O], [O], [O], [O], [O], [O], [W, O],
+];
+
+function getCalendarText() {
+ return helper.getChildren(DAYS_VIEW).map(child => child.textContent);
+}
+
+function getCalendarClassList() {
+ return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList));
+}
+
+function mergeArrays(a, b) {
+ return a.map((classlist, index) => classlist.concat(b[index]));
+}
+
+let helper = new DateTimeTestHelper();
+
+registerCleanupFunction(() => {
+ helper.cleanup();
+});
+
+/**
+ * Test that date picker opens to today's date when input field is blank
+ */
+add_task(async function test_datepicker_today() {
+ const date = new Date();
+
+ await helper.openPicker("data:text/html, ");
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date));
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to the correct month, with calendar days
+ * displayed correctly, given a date value is set.
+ */
+add_task(async function test_datepicker_open() {
+ const inputValue = "2016-12-15";
+
+ await helper.openPicker(`data:text/html, `);
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue)));
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "27", "28", "29", "30", "1", "2", "3",
+ "4", "5", "6", "7", "8", "9", "10",
+ "11", "12", "13", "14", "15", "16", "17",
+ "18", "19", "20", "21", "22", "23", "24",
+ "25", "26", "27", "28", "29", "30", "31",
+ "1", "2", "3", "4", "5", "6", "7",
+ ],
+ "2016-12",
+ );
+ Assert.deepEqual(
+ getCalendarClassList(),
+ calendarClasslist_201612,
+ "2016-12 classNames"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the prev month button is clicked, calendar should display the dates for
+ * the previous month.
+ */
+add_task(async function test_datepicker_prev_month_btn() {
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+
+ await helper.openPicker(`data:text/html, `);
+ helper.click(helper.getElement(BTN_PREV_MONTH));
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth)));
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "30", "31", "1", "2", "3", "4", "5",
+ "6", "7", "8", "9", "10", "11", "12",
+ "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26",
+ "27", "28", "29", "30", "1", "2", "3",
+ "4", "5", "6", "7", "8", "9", "10",
+ ],
+ "2016-11",
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the next month button is clicked, calendar should display the dates for
+ * the next month.
+ */
+add_task(async function test_datepicker_next_month_btn() {
+ const inputValue = "2016-12-15";
+ const nextMonth = "2017-01-01";
+
+ await helper.openPicker(`data:text/html, `);
+ helper.click(helper.getElement(BTN_NEXT_MONTH));
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth)));
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "25", "26", "27", "28", "29", "30", "31",
+ "1", "2", "3", "4", "5", "6", "7",
+ "8", "9", "10", "11", "12", "13", "14",
+ "15", "16", "17", "18", "19", "20", "21",
+ "22", "23", "24", "25", "26", "27", "28",
+ "29", "30", "31", "1", "2", "3", "4",
+ ],
+ "2017-01",
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When a date on the calendar is clicked, date picker should close and set
+ * value to the input box.
+ */
+add_task(async function test_datepicker_clicked() {
+ const inputValue = "2016-12-15";
+ const firstDayOnCalendar = "2016-11-27";
+
+ await helper.openPicker(`data:text/html, `);
+ // Click the first item (top-left corner) of the calendar
+ helper.click(helper.getElement(DAYS_VIEW).children[0]);
+ await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() {
+ let inputEl = content.document.querySelector("input");
+ await ContentTaskUtils.waitForEvent(inputEl, "input");
+ });
+
+ Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar);
+
+ await helper.tearDown();
+});
+
+/**
+ * When min and max attributes are set, calendar should show some dates as
+ * out-of-range.
+ */
+add_task(async function test_datepicker_min_max() {
+ const inputValue = "2016-12-15";
+ const inputMin = "2016-12-05";
+ const inputMax = "2016-12-25";
+
+ await helper.openPicker(`data:text/html, `);
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // R denotes out-of-range
+ [R], [R], [R], [R], [R], [R], [R],
+ [R], [], [], [], [], [], [],
+ [], [], [], [], [], [], [],
+ [], [], [], [], [], [], [],
+ [], [R], [R], [R], [R], [R], [R],
+ [R], [R], [R], [R], [R], [R], [R],
+ ]),
+ "2016-12 with min & max",
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When step attribute is set, calendar should show some dates as off-step.
+ */
+add_task(async function test_datepicker_step() {
+ const inputValue = "2016-12-15";
+ const inputStep = "5";
+
+ await helper.openPicker(`data:text/html, `);
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // P denotes off-step
+ [P], [P], [P], [], [P], [P], [P],
+ [P], [], [P], [P], [P], [P], [],
+ [P], [P], [P], [P], [], [P], [P],
+ [P], [P], [], [P], [P], [P], [P],
+ [], [P], [P], [P], [P], [], [P],
+ [P], [P], [P], [], [P], [P], [P],
+ ]),
+ "2016-12 with step",
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js
index 1c6c2b54f7..e3ef195382 100644
--- a/toolkit/content/tests/browser/head.js
+++ b/toolkit/content/tests/browser/head.js
@@ -31,3 +31,88 @@ function pushPrefs(...aPrefs) {
SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
return deferred.promise;
}
+
+/**
+ * Helper class for testing datetime input picker widget
+ */
+class DateTimeTestHelper {
+ constructor() {
+ this.panel = document.getElementById("DateTimePickerPanel");
+ this.panel.setAttribute("animate", false);
+ this.tab = null;
+ this.frame = null;
+ }
+
+ /**
+ * Opens a new tab with the URL of the test page, and make sure the picker is
+ * ready for testing.
+ *
+ * @param {String} pageUrl
+ */
+ async openPicker(pageUrl) {
+ this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+ await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser);
+ // If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached
+ if (!this.panel.dateTimePopupFrame) {
+ await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady")
+ }
+ this.frame = this.panel.dateTimePopupFrame;
+ await BrowserTestUtils.waitForEvent(this.frame, "load", true);
+ // Wait for picker elements to be ready and open panel transition to end
+ await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady");
+ }
+
+ /**
+ * Find an element on the picker.
+ *
+ * @param {String} selector
+ * @return {DOMElement}
+ */
+ getElement(selector) {
+ return this.frame.contentDocument.querySelector(selector);
+ }
+
+ /**
+ * Find the children of an element on the picker.
+ *
+ * @param {String} selector
+ * @return {Array}
+ */
+ getChildren(selector) {
+ return Array.from(this.getElement(selector).children);
+ }
+
+ /**
+ * Click on an element
+ *
+ * @param {DOMElement} element
+ */
+ click(element) {
+ EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow);
+ }
+
+ /**
+ * Close the panel and the tab
+ */
+ async tearDown() {
+ if (!this.panel.hidden) {
+ let pickerClosePromise = new Promise(resolve => {
+ this.panel.addEventListener("popuphidden", resolve, {once: true});
+ });
+ this.panel.closePicker();
+ await pickerClosePromise;
+ }
+ await BrowserTestUtils.removeTab(this.tab);
+ this.tab = null;
+ }
+
+ /**
+ * Clean up after tests. Remove the frame to prevent leak.
+ */
+ cleanup() {
+ this.frame.remove();
+ this.frame = null;
+ this.panel.removeAttribute("animate");
+ this.panel = null;
+ }
+}
diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js
index 72e0d9d610..80c2976e08 100644
--- a/toolkit/content/widgets/calendar.js
+++ b/toolkit/content/widgets/calendar.js
@@ -54,23 +54,21 @@ function Calendar(options, context) {
* {
* {Number} textContent
* {Array} classNames
+ * {Boolean} enabled
* }
* {Function} getDayString: Transform day number to string
* {Function} getWeekHeaderString: Transform day of week number to string
- * {Function} setValue: Set value for dateKeeper
- * {Number} selectionValue: The selection date value
+ * {Function} setSelection: Set selection for dateKeeper
* }
*/
setProps(props) {
if (props.isVisible) {
// Transform the days and weekHeaders array for rendering
- const days = props.days.map(({ dateValue, textContent, classNames }) => {
+ const days = props.days.map(({ dateObj, classNames, enabled }) => {
return {
- dateValue,
- textContent: props.getDayString(textContent),
- className: dateValue == props.selectionValue ?
- classNames.concat("selection").join(" ") :
- classNames.join(" ")
+ textContent: props.getDayString(dateObj.getUTCDate()),
+ className: classNames.join(" "),
+ enabled
};
});
const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => {
@@ -152,10 +150,10 @@ function Calendar(options, context) {
case "click": {
if (event.target.parentNode == this.context.daysView) {
let targetId = event.target.dataset.id;
- this.props.setValue({
- selectionValue: this.props.days[targetId].dateValue,
- dateValue: this.props.days[targetId].dateValue
- });
+ let targetObj = this.props.days[targetId];
+ if (targetObj.enabled) {
+ this.props.setSelection(targetObj.dateObj);
+ }
}
break;
}
diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js
index de01fdadee..9777ee647a 100644
--- a/toolkit/content/widgets/datekeeper.js
+++ b/toolkit/content/widgets/datekeeper.js
@@ -6,41 +6,72 @@
/**
* DateKeeper keeps track of the date states.
- *
- * @param {Object} date parts
- * {
- * {Number} year
- * {Number} month
- * {Number} day
- * }
- * {Object} options
- * {
- * {Number} firstDayOfWeek [optional]
- * {Array} weekends [optional]
- * {Number} calViewSize [optional]
- * }
*/
-function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
- this.state = {
- firstDayOfWeek, weekends, calViewSize,
- dateObj: new Date(0),
- years: [],
- months: [],
- days: []
- };
- this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek);
- this._update(year, month, day);
+function DateKeeper(props) {
+ this.init(props);
}
{
const DAYS_IN_A_WEEK = 7,
MONTHS_IN_A_YEAR = 12,
YEAR_VIEW_SIZE = 200,
- YEAR_BUFFER_SIZE = 10;
+ YEAR_BUFFER_SIZE = 10,
+ // The min and max values are derived from the ECMAScript spec:
+ // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
+ MIN_DATE = -8640000000000000,
+ MAX_DATE = 8640000000000000;
DateKeeper.prototype = {
+ get year() {
+ return this.state.dateObj.getUTCFullYear();
+ },
+
+ get month() {
+ return this.state.dateObj.getUTCMonth();
+ },
+
+ get day() {
+ return this.state.dateObj.getUTCDate();
+ },
+
+ get selection() {
+ return this.state.selection;
+ },
+
/**
- * Set new date
+ * Initialize DateKeeper
+ * @param {Number} year
+ * @param {Number} month
+ * @param {Number} day
+ * @param {String} min
+ * @param {String} max
+ * @param {Number} firstDayOfWeek
+ * @param {Array} weekends
+ * @param {Number} calViewSize
+ */
+ init({ year, month, day, min, max, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
+ const today = new Date();
+ const isDateSet = year != undefined && month != undefined && day != undefined;
+
+ this.state = {
+ firstDayOfWeek, weekends, calViewSize,
+ min: new Date(min != undefined ? min : MIN_DATE),
+ max: new Date(max != undefined ? max : MAX_DATE),
+ today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()),
+ weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends),
+ years: [],
+ months: [],
+ days: [],
+ selection: { year, month, day },
+ };
+
+ this.state.dateObj = isDateSet ?
+ this._newUTCDate(year, month, day) :
+ new Date(this.state.today);
+ },
+ /**
+ * Set new date. The year is always treated as full year, so the short-form
+ * is not supported.
* @param {Object} date parts
* {
* {Number} year [optional]
@@ -48,17 +79,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* {Number} date [optional]
* }
*/
- set({ year = this.state.year, month = this.state.month, day = this.state.day }) {
- this._update(year, month, day);
+ set({ year = this.year, month = this.month, day = this.day }) {
+ // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
+ this.state.dateObj.setUTCFullYear(year, month, day);
},
/**
- * Set date with value
- * @param {Number} value: Date value
+ * Set selection date
+ * @param {Number} year
+ * @param {Number} month
+ * @param {Number} day
*/
- setValue(value) {
- const dateObj = new Date(value);
- this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
+ setSelection({ year, month, day }) {
+ this.state.selection.year = year;
+ this.state.selection.month = month;
+ this.state.selection.day = day;
},
/**
@@ -66,8 +101,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* @param {Number} month
*/
setMonth(month) {
- const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate();
- this._update(this.state.year, month, Math.min(this.state.day, lastDayOfMonth));
+ const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate();
+ this.set({ year: this.year,
+ month,
+ day: Math.min(this.day, lastDayOfMonth) });
},
/**
@@ -75,8 +112,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* @param {Number} year
*/
setYear(year) {
- const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate();
- this._update(year, this.state.month, Math.min(this.state.day, lastDayOfMonth));
+ const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate();
+ this.set({ year,
+ month: this.month,
+ day: Math.min(this.day, lastDayOfMonth) });
},
/**
@@ -84,22 +123,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* @param {Number} offset
*/
setMonthByOffset(offset) {
- const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate();
- this._update(this.state.year, this.state.month + offset, Math.min(this.state.day, lastDayOfMonth));
- },
-
- /**
- * Update the states.
- * @param {Number} year [description]
- * @param {Number} month [description]
- * @param {Number} day [description]
- */
- _update(year, month, day) {
- // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
- this.state.dateObj.setUTCFullYear(year, month, day);
- this.state.year = this.state.dateObj.getUTCFullYear();
- this.state.month = this.state.dateObj.getUTCMonth();
- this.state.day = this.state.dateObj.getUTCDate();
+ const lastDayOfMonth = this._newUTCDate(this.year, this.month + offset + 1, 0).getUTCDate();
+ this.set({ year: this.year,
+ month: this.month + offset,
+ day: Math.min(this.day, lastDayOfMonth) });
},
/**
@@ -111,7 +138,6 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* }
*/
getMonths() {
- // TODO: add min/max and step support
let months = [];
for (let i = 0; i < MONTHS_IN_A_YEAR; i++) {
@@ -133,12 +159,11 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* }
*/
getYears() {
- // TODO: add min/max and step support
let years = [];
const firstItem = this.state.years[0];
const lastItem = this.state.years[this.state.years.length - 1];
- const currentYear = this.state.dateObj.getUTCFullYear();
+ const currentYear = this.year;
// Generate new years array when the year is outside of the first &
// last item range. If not, return the cached result.
@@ -161,30 +186,43 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* Get days for calendar
* @return {Array