diff options
34 files changed, 1267 insertions, 248 deletions
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 ae531e1677..62c2b122ff 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"> <iframe id="dateTimePopupFrame"/> </panel> 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> 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<OwningFileOrDirectory>& 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<OwningFileOrDirect { SetFilesOrDirectories(aFilesOrDirectories, true); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { UpdateEntries(aFilesOrDirectories); } RefPtr<DispatchChangeEventCallback> 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 (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() { - 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)); + 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 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1370858 +--> +<head> +<title>Test for Bug 1370858</title> +<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" 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=1370858">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onchange="++changeEvents[0]" + oninput="++inputEvents[0]"> +<input type="date" id="input_date" onchange="++changeEvents[1]" + oninput="++inputEvents[1]"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** + * Test for Bug 1370858. + * Test that change and input events are (not) fired for date/time inputs. + **/ + +var inputTypes = ["time", "date"]; +var changeEvents = [0, 0]; +var inputEvents = [0, 0]; +var values = ["10:30", "2017-06-08"]; +var expectedValues = [["09:30", "01:30"], ["2017-05-08", "2017-01-08"]]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + for (var i = 0; i < inputTypes.length; i++) { + var input = document.getElementById("input_" + inputTypes[i]); + var inputRect = input.getBoundingClientRect(); + + // Points over the input's reset button + var resetButton_X = inputRect.width - 15; + var resetButton_Y = inputRect.height / 2; + + is(changeEvents[i], 0, "Number of change events should be 0 at start."); + is(inputEvents[i], 0, "Number of input events should be 0 at start."); + + // Test that change and input events are not dispatched setting .value by + // script. + input.value = values[i]; + is(input.value, values[i], "Check that value was set correctly (0)."); + is(changeEvents[i], 0, "Change event should not have dispatched (0)."); + is(inputEvents[i], 0, "Input event should not have dispatched (0)."); + + // Test that change and input events are fired when changing the value using + // up/down keys. + input.focus(); + synthesizeKey("VK_DOWN", {}); + is(input.value, expectedValues[i][0], "Check that value was set correctly (1)."); + is(changeEvents[i], 1, "Change event should be dispatched (1)."); + is(inputEvents[i], 1, "Input event should ne dispatched (1)."); + + // Test that change and input events are fired when changing the value with + // the keyboard. + synthesizeKey("0", {}); + synthesizeKey("1", {}); + is(input.value, expectedValues[i][1], "Check that value was set correctly (2)."); + is(changeEvents[i], 2, "Change event should be dispatched (2)."); + is(inputEvents[i], 2, "Input event should be dispatched (2)."); + + // Test that change and input events are fired when clearing the value using + // the reset button. + synthesizeMouse(input, resetButton_X, resetButton_Y, {}); + is(input.value, "", "Check that value was set correctly (3)."); + is(changeEvents[i], 3, "Change event should be dispatched (3)."); + is(inputEvents[i], 3, "Input event should be dispatched (3)."); + } +} + +</script> +</pre> +</body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" style="width: 200px;"> + <!-- div to cover the right area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; left:40px; width:200px; height:100px;"></div> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" style="width: 50px;"> + <!-- div to cover the right area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; left:40px; width:200px; height:100px;"></div> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" value="10:00" style="float: right; color: white;"> + <!-- div to cover the left area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; right:30px; width:500px; height:100px;"></div> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" value="10:00" style="width: 150px; float: right; + color: white;"> + <!-- div to cover the left area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; right:30px; width:500px; height:100px;"></div> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 200px; + height: 5px; + outline: 1px dotted black; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 200px; + height: 5px; + outline: 1px dotted black; + color: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 8px; + height: 8px; + outline: 1px dotted black; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 8px; + height: 8px; + outline: 1px dotted black; + color: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 10px; + height: 1.5em; + outline: 1px dotted black; + background: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> 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 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 10px; + height: 1.5em; + outline: 1px dotted black; + color: white; + background: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> 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, <input type='date'>"); + + 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, <input type="date" value="${inputValue}">`); + + 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, <input type="date" value="${inputValue}">`); + 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, <input type="date" value="${inputValue}">`); + 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, <input type="date" value="${inputValue}">`); + // 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, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`); + + 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, <input type="date" value="${inputValue}" step="${inputStep}">`); + + 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<DOMElement>} + */ + 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<String>} 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<Number>} 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; + }, + + /** + * Initialize DateKeeper + * @param {Number} year + * @param {Number} month + * @param {Number} day + * @param {String} min + * @param {String} max + * @param {Number} firstDayOfWeek + * @param {Array<Number>} 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 + * 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<Object>} * { - * {Number} dateValue - * {Number} textContent + * {Date} dateObj * {Array<String>} classNames + * {Boolean} enabled * } */ getDays() { - // TODO: add min/max and step support - let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + // TODO: add step support + const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + const month = this.month; let days = []; - let month = this.state.dateObj.getUTCMonth(); for (let i = 0; i < this.state.calViewSize; i++) { - let dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); + const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); let classNames = []; + let enabled = true; if (this.state.weekends.includes(dateObj.getUTCDay())) { classNames.push("weekend"); } if (month != dateObj.getUTCMonth()) { classNames.push("outside"); } + if (this.state.selection.year == dateObj.getUTCFullYear() && + this.state.selection.month == dateObj.getUTCMonth() && + this.state.selection.day == dateObj.getUTCDate()) { + classNames.push("selection"); + } + if (dateObj.getTime() < this.state.min.getTime() || dateObj.getTime() > this.state.max.getTime()) { + classNames.push("out-of-range"); + enabled = false; + } + if (this.state.today.getTime() == dateObj.getTime()) { + classNames.push("today"); + } days.push({ - dateValue: dateObj.getTime(), - textContent: dateObj.getUTCDate(), - classNames + dateObj, + classNames, + enabled, }); } return days; @@ -193,20 +231,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], /** * Get week headers for calendar * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends * @return {Array<Object>} * { * {Number} textContent * {Array<String>} classNames * } */ - _getWeekHeaders(firstDayOfWeek) { + _getWeekHeaders(firstDayOfWeek, weekends) { let headers = []; let dayOfWeek = firstDayOfWeek; for (let i = 0; i < DAYS_IN_A_WEEK; i++) { headers.push({ textContent: dayOfWeek % DAYS_IN_A_WEEK, - classNames: this.state.weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] + classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] }); dayOfWeek++; } diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 210ca856cf..317f0ae94c 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -20,6 +20,12 @@ function DatePicker(context) { * {Number} year [optional] * {Number} month [optional] * {Number} date [optional] + * {String} min + * {String} max + * {Number} firstDayOfWeek + * {Array<Number>} weekends + * {Array<String>} monthStrings + * {Array<String>} weekdayStrings * {String} locale [optional]: User preferred locale * } */ @@ -28,57 +34,54 @@ function DatePicker(context) { this._setDefaultState(); this._createComponents(); this._update(); + document.dispatchEvent(new CustomEvent("PickerReady")); }, /* * Set initial date picker states. */ _setDefaultState() { - const now = new Date(); - const { year = now.getFullYear(), - month = now.getMonth(), - day = now.getDate(), - locale } = this.props; - - // TODO: Use calendar info API to get first day of week & weekends - // (Bug 1287503) + const { year, month, day, min, max, firstDayOfWeek, weekends, + monthStrings, weekdayStrings, locale } = this.props; const dateKeeper = new DateKeeper({ - year, month, day - }, { - calViewSize: CAL_VIEW_SIZE, - firstDayOfWeek: 0, - weekends: [0] + year, month, day, min, max, firstDayOfWeek, weekends, + calViewSize: CAL_VIEW_SIZE }); this.state = { dateKeeper, locale, isMonthPickerVisible: false, - isYearSet: false, - isMonthSet: false, - isDateSet: false, getDayString: new Intl.NumberFormat(locale).format, - // TODO: use calendar terms when available (Bug 1287677) - getWeekHeaderString: weekday => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday], - setValue: ({ dateValue, selectionValue }) => { - dateKeeper.setValue(dateValue); - this.state.selectionValue = selectionValue; - this.state.isYearSet = true; - this.state.isMonthSet = true; - this.state.isDateSet = true; + getWeekHeaderString: weekday => weekdayStrings[weekday], + getMonthString: month => monthStrings[month], + setSelection: date => { + dateKeeper.setSelection({ + year: date.getUTCFullYear(), + month: date.getUTCMonth(), + day: date.getUTCDate(), + }); this._update(); this._dispatchState(); this._closePopup(); }, setYear: year => { dateKeeper.setYear(year); - this.state.isYearSet = true; + dateKeeper.setSelection({ + year, + month: dateKeeper.selection.month, + day: dateKeeper.selection.day, + }); this._update(); this._dispatchState(); }, setMonth: month => { dateKeeper.setMonth(month); - this.state.isMonthSet = true; + dateKeeper.setSelection({ + year: dateKeeper.selection.year, + month, + day: dateKeeper.selection.day, + }); this._update(); this._dispatchState(); }, @@ -104,6 +107,7 @@ function DatePicker(context) { monthYear: new MonthYear({ setYear: this.state.setYear, setMonth: this.state.setMonth, + getMonthString: this.state.getMonthString, locale: this.state.locale }, { monthYear: this.context.monthYear, @@ -116,7 +120,7 @@ function DatePicker(context) { * Update date picker and its components. */ _update() { - const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state; + const { dateKeeper, isMonthPickerVisible } = this.state; if (isMonthPickerVisible) { this.state.months = dateKeeper.getMonths(); @@ -128,9 +132,7 @@ function DatePicker(context) { this.components.monthYear.setProps({ isVisible: isMonthPickerVisible, dateObj: dateKeeper.state.dateObj, - month: dateKeeper.state.month, months: this.state.months, - year: dateKeeper.state.year, years: this.state.years, toggleMonthPicker: this.state.toggleMonthPicker }); @@ -138,10 +140,9 @@ function DatePicker(context) { isVisible: !isMonthPickerVisible, days: this.state.days, weekHeaders: dateKeeper.state.weekHeaders, - setValue: this.state.setValue, + setSelection: this.state.setSelection, getDayString: this.state.getDayString, - getWeekHeaderString: this.state.getWeekHeaderString, - selectionValue + getWeekHeaderString: this.state.getWeekHeaderString }); isMonthPickerVisible ? @@ -162,8 +163,7 @@ function DatePicker(context) { * Use postMessage to pass the state of picker to the panel. */ _dispatchState() { - const { year, month, day } = this.state.dateKeeper.state; - const { isYearSet, isMonthSet, isDaySet } = this.state; + const { year, month, day } = this.state.dateKeeper.selection; // The panel is listening to window for postMessage event, so we // do postMessage to itself to send data to input boxes. window.postMessage({ @@ -172,9 +172,6 @@ function DatePicker(context) { year, month, day, - isYearSet, - isMonthSet, - isDaySet } }, "*"); }, @@ -255,19 +252,12 @@ function DatePicker(context) { set({ year, month, day }) { const { dateKeeper } = this.state; - if (year != undefined) { - this.state.isYearSet = true; - } - if (month != undefined) { - this.state.isMonthSet = true; - } - if (day != undefined) { - this.state.isDaySet = true; - } - dateKeeper.set({ year, month, day }); + dateKeeper.setSelection({ + year, month, day + }); this._update(); } }; @@ -280,12 +270,12 @@ function DatePicker(context) { * {String} locale * {Function} setYear * {Function} setMonth + * {Function} getMonthString * } * @param {DOMElement} context */ function MonthYear(options, context) { const spinnerSize = 5; - const monthFormat = new Intl.DateTimeFormat(options.locale, { month: "short" }).format; const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric" }).format; const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long" }).format; @@ -298,7 +288,7 @@ function DatePicker(context) { this.state.isMonthSet = true; options.setMonth(month); }, - getDisplayString: month => monthFormat(new Date(0, month)), + getDisplayString: options.getMonthString, viewportSize: spinnerSize }, context.monthYearView), year: new Spinner({ @@ -323,8 +313,6 @@ function DatePicker(context) { * { * {Boolean} isVisible * {Date} dateObj - * {Number} month - * {Number} year * {Array<Object>} months * {Array<Object>} years * {Function} toggleMonthPicker @@ -336,14 +324,14 @@ function DatePicker(context) { if (props.isVisible) { this.context.monthYear.classList.add("active"); this.components.month.setState({ - value: props.month, + value: props.dateObj.getUTCMonth(), items: props.months, isInfiniteScroll: true, isValueSet: this.state.isMonthSet, smoothScroll: !this.state.firstOpened }); this.components.year.setState({ - value: props.year, + value: props.dateObj.getUTCFullYear(), items: props.years, isInfiniteScroll: false, isValueSet: this.state.isYearSet, diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css index 18ff024c78..ce638078fa 100644 --- a/toolkit/content/widgets/datetimebox.css +++ b/toolkit/content/widgets/datetimebox.css @@ -8,9 +8,17 @@ .datetime-input-box-wrapper { -moz-appearance: none; display: inline-flex; + flex: 1; cursor: default; background-color: inherit; color: inherit; + min-width: 0; + justify-content: space-between; +} + +.datetime-input-edit-wrapper { + overflow: hidden; + white-space: nowrap; } .datetime-input { @@ -43,5 +51,5 @@ height: 12px; width: 12px; align-self: center; - justify-content: flex-end; + flex: none; } diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 5859f80dd1..c276265a3d 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -4,6 +4,11 @@ - 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/. --> +<!DOCTYPE bindings [ +<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd"> +%datetimeboxDTD; +]> + <bindings id="datetimeboxBindings" xmlns="http://www.mozilla.org/xbl" xmlns:html="http://www.w3.org/1999/xhtml" @@ -21,12 +26,13 @@ <implementation> <constructor> <![CDATA[ - // TODO: Bug 1320227 - [DateTimeInput] localization for - // <input type=date> input box - this.mMonthPlaceHolder = "mm"; - this.mDayPlaceHolder = "dd"; - this.mYearPlaceHolder = "yyyy"; + /* eslint-disable no-multi-spaces */ + this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[; + this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[; + this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[; this.mSeparatorText = "/"; + /* eslint-enable no-multi-spaces */ + this.mMinMonth = 1; this.mMaxMonth = 12; this.mMinDay = 1; @@ -76,6 +82,7 @@ if (this.mInputElement.value) { this.setFieldsFromInputValue(); } + this.updateResetButtonVisibility(); ]]> </constructor> @@ -107,9 +114,11 @@ this.mYearField.setAttribute("typeBuffer", ""); } - if (!aFromInputElement) { + if (!aFromInputElement && this.mInputElement.value) { this.mInputElement.setUserInput(""); } + + this.updateResetButtonVisibility(); ]]> </body> </method> @@ -170,6 +179,13 @@ <method name="setInputValueFromFields"> <body> <![CDATA[ + if (!this.isAnyValueAvailable(false) && this.mInputElement.value) { + // Values in the input box was cleared, clear the input element's + // value if not empty. + this.mInputElement.setUserInput(""); + return; + } + if (this.isFieldInvalid(this.mYearField) || this.isFieldInvalid(this.mMonthField) || this.isFieldInvalid(this.mDayField)) { @@ -192,6 +208,10 @@ let date = [year, month, day].join("-"); + if (date == this.mInputElement.value) { + return; + } + this.log("setInputValueFromFields: " + date); this.mInputElement.setUserInput(date); ]]> @@ -217,6 +237,9 @@ if (!this.isEmpty(day)) { this.setFieldValue(this.mDayField, day); } + + // Update input element's .value if needed. + this.setInputValueFromFields(); ]]> </body> </method> @@ -302,6 +325,12 @@ let targetField = aEvent.originalTarget; let key = aEvent.key; + // Home/End key does nothing on year field. + if (targetField == this.mYearField && (key == "Home" || + key == "End")) { + return; + } + switch (key) { case "ArrowUp": this.incrementFieldValue(targetField, 1); @@ -406,11 +435,13 @@ } aField.value = value; + this.updateResetButtonVisibility(); ]]> </body> </method> - <method name="isValueAvailable"> + <method name="isAnyValueAvailable"> + <parameter name="aForPicker"/> <body> <![CDATA[ return !this.isEmpty(this.mMonthField.value) || @@ -492,6 +523,7 @@ if (this.mInputElement.value) { this.setFieldsFromInputValue(); } + this.updateResetButtonVisibility(); ]]> </constructor> @@ -551,7 +583,7 @@ } this.log("setFieldsFromInputValue: " + value); - let [hour, minute, second] = value.split(':'); + let [hour, minute, second] = value.split(":"); this.setFieldValue(this.mHourField, hour); this.setFieldValue(this.mMinuteField, minute); @@ -617,6 +649,13 @@ <method name="setInputValueFromFields"> <body> <![CDATA[ + if (!this.isAnyValueAvailable(false) && this.mInputElement.value) { + // Values in the input box was cleared, clear the input element's + // value if not empty. + this.mInputElement.setUserInput(""); + return; + } + if (this.isEmpty(this.mHourField.value) || this.isEmpty(this.mMinuteField.value) || (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) || @@ -652,6 +691,10 @@ time += "." + this.mMillisecField.value; } + if (time == this.mInputElement.value) { + return; + } + this.log("setInputValueFromFields: " + time); this.mInputElement.setUserInput(time); ]]> @@ -678,6 +721,9 @@ if (!this.isEmpty(minute)) { this.setFieldValue(this.mMinuteField, minute); } + + // Update input element's .value if needed. + this.setInputValueFromFields(); ]]> </body> </method> @@ -721,9 +767,11 @@ this.mDayPeriodField.value = ""; } - if (!aFromInputElement) { + if (!aFromInputElement && this.mInputElement.value) { this.mInputElement.setUserInput(""); } + + this.updateResetButtonVisibility(); ]]> </body> </method> @@ -793,6 +841,7 @@ this.mDayPeriodField.value == this.mAMIndicator ? this.mPMIndicator : this.mAMIndicator; this.mDayPeriodField.select(); + this.updateResetButtonVisibility(); this.setInputValueFromFields(); return; } @@ -850,6 +899,7 @@ this.mDayPeriodField.value = this.mPMIndicator; this.mDayPeriodField.select(); } + this.updateResetButtonVisibility(); return; } @@ -905,16 +955,30 @@ } aField.value = value; + this.updateResetButtonVisibility(); ]]> </body> </method> - <method name="isValueAvailable"> + <method name="isAnyValueAvailable"> + <parameter name="aForPicker"/> <body> <![CDATA[ + let available = !this.isEmpty(this.mHourField.value) || + !this.isEmpty(this.mMinuteField.value); + + if (available) { + return true; + } + // Picker only cares about hour:minute. - return !this.isEmpty(this.mHourField.value) || - !this.isEmpty(this.mMinuteField.value); + if (aForPicker) { + return false; + } + + return (this.mDayPeriodField && !this.isEmpty(this.mDayPeriodField.value)) || + (this.mSecondField && !this.isEmpty(this.mSecondField.value)) || + (this.mMillisecField && !this.isEmpty(this.mMillisecField.value)); ]]> </body> </method> @@ -963,7 +1027,8 @@ <content> <html:div class="datetime-input-box-wrapper" xbl:inherits="context,disabled,readonly"> - <html:span> + <html:span class="datetime-input-edit-wrapper" + anonid="edit-wrapper"> <html:input anonid="input-one" class="textbox-input datetime-input numeric" size="2" maxlength="2" @@ -980,9 +1045,8 @@ xbl:inherits="disabled,readonly,tabindex"/> </html:span> - <html:button class="datetime-reset-button" anoid="reset-button" - tabindex="-1" xbl:inherits="disabled" - onclick="document.getBindingParent(this).clearInputFields(false);"/> + <html:button class="datetime-reset-button" anonid="reset-button" + tabindex="-1" xbl:inherits="disabled"/> </html:div> </content> @@ -997,6 +1061,9 @@ this.mStep = this.mInputElement.step; this.mIsPickerOpen = false; + this.mResetButton = + document.getAnonymousElementByAttribute(this, "anonid", "reset-button"); + this.EVENTS.forEach((eventName) => { this.addEventListener(eventName, this, { mozSystemGroup: true }); }); @@ -1005,6 +1072,9 @@ capture: true, mozSystemGroup: true }); + // This is to close the picker when input element blurs. + this.mInputElement.addEventListener("blur", this, + { mozSystemGroup: true }); ]]> </constructor> @@ -1025,7 +1095,7 @@ <property name="EVENTS" readonly="true"> <getter> <![CDATA[ - return ["click", "focus", "blur", "copy", "cut", "paste"]; + return ["click", "focus", "blur", "copy", "cut", "paste", "mousedown"]; ]]> </getter> </property> @@ -1041,6 +1111,18 @@ </body> </method> + <method name="updateResetButtonVisibility"> + <body> + <![CDATA[ + if (this.isAnyValueAvailable(false)) { + this.mResetButton.style.visibility = "visible"; + } else { + this.mResetButton.style.visibility = "hidden"; + } + ]]> + </body> + </method> + <method name="focusInnerTextBox"> <body> <![CDATA[ @@ -1164,10 +1246,16 @@ </body> </method> + <method name="isAnyValueAvailable"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + <method name="notifyPicker"> <body> <![CDATA[ - if (this.mIsPickerOpen && this.isValueAvailable()) { + if (this.mIsPickerOpen && this.isAnyValueAvailable(true)) { this.mInputElement.updateDateTimePicker(this.getCurrentValue()); } ]]> @@ -1213,6 +1301,12 @@ this.onBlur(aEvent); break; } + case "mousedown": { + if (aEvent.originalTarget == this.mResetButton) { + aEvent.preventDefault(); + } + break; + } case "copy": case "cut": case "paste": { @@ -1245,7 +1339,12 @@ <parameter name="aEvent"/> <body> <![CDATA[ - this.log("onBlur originalTarget: " + aEvent.originalTarget); + this.log("onBlur originalTarget: " + aEvent.originalTarget + + " target: " + aEvent.target); + + if (aEvent.target == this.mInputElement && this.mIsPickerOpen) { + this.mInputElement.closeDateTimePicker(); + } let target = aEvent.originalTarget; target.setAttribute("typeBuffer", ""); @@ -1261,16 +1360,23 @@ this.log("onKeyPress key: " + aEvent.key); switch (aEvent.key) { - // Close picker on Enter or Space key. + // Close picker on Enter, Escape or Space key. case "Enter": + case "Escape": case " ": { - this.mInputElement.closeDateTimePicker(); - aEvent.preventDefault(); + if (this.mIsPickerOpen) { + this.mInputElement.closeDateTimePicker(); + aEvent.preventDefault(); + } break; } case "Backspace": { let targetField = aEvent.originalTarget; + targetField.value = ""; targetField.setAttribute("typeBuffer", ""); + this.updateResetButtonVisibility(); + this.setInputValueFromFields(); + aEvent.preventDefault(); break; } case "ArrowRight": @@ -1312,12 +1418,14 @@ // XXX: .originalTarget is not expected. // When clicking on one of the inner text boxes, the .originalTarget is // a HTMLDivElement and when clicking on the reset button, it's a - // HTMLButtonElement but it's not equal to our reset-button. + // HTMLButtonElement. if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) { return; } - if (!(aEvent.originalTarget instanceof HTMLButtonElement)) { + if (aEvent.originalTarget == this.mResetButton) { + this.clearInputFields(false); + } else if (!this.mIsPickerOpen) { this.mInputElement.openDateTimePicker(this.getCurrentValue()); } ]]> diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml index 5f16f1ff0c..1d6a5e7722 100644 --- a/toolkit/content/widgets/datetimepicker.xml +++ b/toolkit/content/widgets/datetimepicker.xml @@ -999,13 +999,13 @@ <body> <![CDATA[ var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory"; - var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"}); + var dtfMonth = Intl.DateTimeFormat(locale, {month: "long", timeZone: "UTC"}); var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"}); var monthLabel = this.monthField.firstChild; - var tempDate = new Date(2005, 0, 1); + var tempDate = new Date(Date.UTC(2005, 0, 1)); for (var month = 0; month < 12; month++) { - tempDate.setMonth(month); + tempDate.setUTCMonth(month); monthLabel.setAttribute("value", dtfMonth.format(tempDate)); monthLabel = monthLabel.nextSibling; } diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 86e8780c15..52df7de750 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -22,11 +22,20 @@ <field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field> <field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field> <field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field> - <method name="loadPicker"> + <constructor><![CDATA[ + this.l10n = {}; + const mozIntl = Components.classes["@mozilla.org/mozintl;1"] + .getService(Components.interfaces.mozIMozIntl); + mozIntl.addGetCalendarInfo(l10n); + mozIntl.addGetDisplayNames(l10n); + // Notify DateTimePickerHelper.jsm that binding is ready. + this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady")); + ]]></constructor> + <method name="openPicker"> <parameter name="type"/> + <parameter name="anchor"/> <parameter name="detail"/> <body><![CDATA[ - this.hidden = false; this.type = type; this.pickerState = {}; // TODO: Resize picker according to content zoom level @@ -49,17 +58,20 @@ break; } } + this.hidden = false; + this.openPopup(anchor, "after_start", 0, 0); ]]></body> </method> <method name="closePicker"> <body><![CDATA[ - this.hidden = true; this.setInputBoxValue(true); this.pickerState = {}; this.type = undefined; this.dateTimePopupFrame.removeEventListener("load", this, true); this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false); this.dateTimePopupFrame.setAttribute("src", ""); + this.hidePopup(); + this.hidden = true; ]]></body> </method> <method name="setPopupValue"> @@ -115,6 +127,34 @@ } case "date": { const { year, month, day } = detail.value; + const { firstDayOfWeek, weekends } = + this.getCalendarInfo(locale); + const monthStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/months/january", + "dates/gregorian/months/february", + "dates/gregorian/months/march", + "dates/gregorian/months/april", + "dates/gregorian/months/may", + "dates/gregorian/months/june", + "dates/gregorian/months/july", + "dates/gregorian/months/august", + "dates/gregorian/months/september", + "dates/gregorian/months/october", + "dates/gregorian/months/november", + "dates/gregorian/months/december", + ], "short"); + const weekdayStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/weekdays/sunday", + "dates/gregorian/weekdays/monday", + "dates/gregorian/weekdays/tuesday", + "dates/gregorian/weekdays/wednesday", + "dates/gregorian/weekdays/thursday", + "dates/gregorian/weekdays/friday", + "dates/gregorian/weekdays/saturday", + ], "short"); + this.postMessageToPicker({ name: "PickerInit", detail: { @@ -122,7 +162,13 @@ // Month value from input box starts from 1 instead of 0 month: month == undefined ? undefined : month - 1, day, - locale + firstDayOfWeek, + weekends, + monthStrings, + weekdayStrings, + locale, + min: detail.min, + max: detail.max, } }); break; @@ -184,6 +230,46 @@ } ]]></body> </method> + <method name="getCalendarInfo"> + <parameter name="locale"/> + <body><![CDATA[ + const calendarInfo = this.l10n.getCalendarInfo(locale); + + // Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday, + // so they need to be mapped to JavaScript convention with 0 as Sunday + // and 6 as Saturday + let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1, + weekendStart = calendarInfo.weekendStart - 1, + weekendEnd = calendarInfo.weekendEnd - 1; + + let weekends = []; + + // Make sure weekendEnd is greater than weekendStart + if (weekendEnd < weekendStart) { + weekendEnd += 7; + } + + // We get the weekends by incrementing weekendStart up to weekendEnd. + // If the start and end is the same day, then weekends only has one day. + for (let day = weekendStart; day <= weekendEnd; day++) { + weekends.push(day % 7); + } + + return { + firstDayOfWeek, + weekends + } + ]]></body> + </method> + <method name="getDisplayNames"> + <parameter name="locale"/> + <parameter name="keys"/> + <parameter name="style"/> + <body><![CDATA[ + const displayNames = this.l10n.getDisplayNames(locale, {keys, style}); + return keys.map(key => displayNames.values[key]); + ]]></body> + </method> <method name="handleEvent"> <parameter name="aEvent"/> <body><![CDATA[ @@ -230,12 +316,5 @@ </method> </implementation> - <handlers> - <handler event="popuphiding"> - <![CDATA[ - this.closePicker(); - ]]> - </handler> - </handlers> </binding> </bindings> diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 059e151fc0..6ef929f8ad 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -299,11 +299,11 @@ function Spinner(props, context) { // An "active" class is needed to simulate :active pseudo-class // because element is not focused. event.target.classList.add("active"); - this._smoothScrollToIndex(index + 1); + this._smoothScrollToIndex(index - 1); } if (event.target == down) { event.target.classList.add("active"); - this._smoothScrollToIndex(index - 1); + this._smoothScrollToIndex(index + 1); } if (event.target.parentNode == spinner) { // Listen to dragging events diff --git a/toolkit/locales/en-US/chrome/global/datetimebox.dtd b/toolkit/locales/en-US/chrome/global/datetimebox.dtd new file mode 100644 index 0000000000..0deffa6b31 --- /dev/null +++ b/toolkit/locales/en-US/chrome/global/datetimebox.dtd @@ -0,0 +1,9 @@ +<!-- 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/. --> + +<!-- Placeholders for input type=date --> + +<!ENTITY date.year.placeholder "yyyy"> +<!ENTITY date.month.placeholder "mm"> +<!ENTITY date.day.placeholder "dd"> diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index e49e978f51..abc96086f5 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -39,6 +39,7 @@ locale/@AB_CD@/global/customizeToolbar.dtd (%chrome/global/customizeToolbar.dtd) locale/@AB_CD@/global/customizeToolbar.properties (%chrome/global/customizeToolbar.properties) #endif + locale/@AB_CD@/global/datetimebox.dtd (%chrome/global/datetimebox.dtd) locale/@AB_CD@/global/datetimepicker.dtd (%chrome/global/datetimepicker.dtd) locale/@AB_CD@/global/dateFormat.properties (%chrome/global/dateFormat.properties) locale/@AB_CD@/global/dialogOverlay.dtd (%chrome/global/dialogOverlay.dtd) diff --git a/toolkit/modules/DateTimePickerHelper.jsm b/toolkit/modules/DateTimePickerHelper.jsm index 769ae0094c..0ea96f226a 100644 --- a/toolkit/modules/DateTimePickerHelper.jsm +++ b/toolkit/modules/DateTimePickerHelper.jsm @@ -21,6 +21,7 @@ this.EXPORTED_SYMBOLS = [ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); /* * DateTimePickerHelper receives message from content side (input box) and @@ -66,6 +67,9 @@ this.DateTimePickerHelper = { break; } case "FormDateTime:UpdatePicker": { + if (!this.picker) { + return; + } this.picker.setPopupValue(aMessage.data); break; } @@ -105,7 +109,7 @@ this.DateTimePickerHelper = { }, // Get picker from browser and show it anchored to the input box. - showPicker: function(aBrowser, aData) { + showPicker: Task.async(function* (aBrowser, aData) { let rect = aData.rect; let dir = aData.dir; let type = aData.type; @@ -135,13 +139,23 @@ this.DateTimePickerHelper = { debug("aBrowser.dateTimePicker not found, exiting now."); return; } - this.picker.loadPicker(type, detail); + // The datetimepopup binding is only attached when it is needed. + // Check if openPicker method is present to determine if binding has + // been attached. If not, attach the binding first before calling it. + if (!this.picker.openPicker) { + let bindingPromise = new Promise(resolve => { + this.picker.addEventListener("DateTimePickerBindingReady", + resolve, {once: true}); + }); + this.picker.setAttribute("active", true); + yield bindingPromise; + } // The arrow panel needs an anchor to work. The popupAnchor (this._anchor) // is a transparent div that the arrow can point to. - this.picker.openPopup(this._anchor, "after_start", 0, 0); + this.picker.openPicker(type, this._anchor, detail); this.addPickerListeners(); - }, + }), // Picker is closed, do some cleanup. close: function() { diff --git a/toolkit/themes/shared/datetimeinputpickers.css b/toolkit/themes/shared/datetimeinputpickers.css index 741f15281f..a0c046f6f1 100644 --- a/toolkit/themes/shared/datetimeinputpickers.css +++ b/toolkit/themes/shared/datetimeinputpickers.css @@ -12,7 +12,8 @@ --colon-width: 2rem; --day-period-spacing-width: 1rem; --calendar-width: 23.1rem; - --date-picker-item-height: 2.4rem; + --date-picker-item-height: 2.5rem; + --date-picker-item-width: 3.3rem; --border: 0.1rem solid #D6D6D6; --border-radius: 0.3rem; @@ -21,6 +22,8 @@ --font-color: #191919; --fill-color: #EBEBEB; + --today-fill-color: rgb(212, 212, 212); + --selected-font-color: #FFFFFF; --selected-fill-color: #0996F8; @@ -29,10 +32,16 @@ --button-font-color-active: #191919; --button-fill-color-active: #D4D4D4; - --weekday-font-color: #6C6C6C; - --weekday-outside-font-color: #6C6C6C; - --weekend-font-color: #DA4E44; - --weekend-outside-font-color: #FF988F; + --weekday-header-font-color: #6C6C6C; + --weekend-header-font-color: rgb(218, 78, 68); + + --weekend-font-color: rgb(218, 78, 68); + --weekday-outside-font-color: rgb(153, 153, 153); + --weekend-outside-font-color: rgb(255, 152, 143); + + --weekday-disabled-font-color: rgba(25, 25, 25, 0.2); + --weekend-disabled-font-color: rgba(218, 78, 68, 0.2); + --disabled-fill-color: rgba(235, 235, 235, 0.8); --disabled-opacity: 0.2; } @@ -181,11 +190,11 @@ button.month-year.active::after { } .week-header > div { - color: var(--weekday-font-color); + color: var(--weekday-header-font-color); } .week-header > div.weekend { - color: var(--weekend-font-color); + color: var(--weekend-header-font-color); } .days-viewport { @@ -206,24 +215,49 @@ button.month-year.active::after { align-items: center; display: flex; height: var(--date-picker-item-height); - margin: 0.05rem 0.15rem; position: relative; justify-content: center; - width: 3rem; + width: var(--date-picker-item-width); } -.days-view > div.outside { +.days-view > .outside { color: var(--weekday-outside-font-color); } -.days-view > div.weekend { +.days-view > .weekend { color: var(--weekend-font-color); } -.days-view > div.weekend.outside { +.days-view > .weekend.outside { color: var(--weekend-outside-font-color); } +.days-view > .out-of-range { + color: var(--weekday-disabled-font-color); + background: var(--disabled-fill-color); +} + +.days-view > .out-of-range.weekend { + color: var(--weekend-disabled-font-color); +} + +.days-view > .today { + font-weight: bold; +} + +.days-view > .out-of-range::before { + display: none; +} + +.days-view > div:hover::before, +.days-view > .select::before, +.days-view > .today::before { + top: 5%; + bottom: 5%; + left: 5%; + right: 5%; +} + #time-picker, .month-year-view { display: flex; @@ -283,22 +317,31 @@ button.month-year.active::after { scroll-snap-coordinate: 0 0; } +.spinner-container > .spinner > div::before, +.calendar-container .days-view > div::before { + position: absolute; + top: 5%; + bottom: 5%; + left: 5%; + right: 5%; + z-index: -10; + border-radius: var(--border-radius); +} + .spinner-container > .spinner > div:hover::before, .calendar-container .days-view > div:hover::before { background: var(--fill-color); border: var(--border); - border-radius: var(--border-radius); content: ""; - position: absolute; - top: 0%; - bottom: 0%; - left: 0%; - right: 0%; - z-index: -10; +} + +.calendar-container .days-view > div.today::before { + background: var(--today-fill-color); + content: ""; } .spinner-container > .spinner:not(.scrolling) > div.selection, -.calendar-container .days-view > div.selection { +.calendar-container .days-view > div.selection:not(.out-of-range) { color: var(--selected-font-color); } @@ -306,14 +349,7 @@ button.month-year.active::after { .calendar-container .days-view > div.selection::before { background: var(--selected-fill-color); border: none; - border-radius: var(--border-radius); content: ""; - position: absolute; - top: 0%; - bottom: 0%; - left: 0%; - right: 0%; - z-index: -10; } .spinner-container > .spinner > div.disabled::before, |