moebius#92: HTML - input - datetime

+ native in moebius:

Bug 1317600: https://bugzilla.mozilla.org/show_bug.cgi?id=1317600
A note - not implemented: Bug 1282768:
https://bugzilla.mozilla.org/show_bug.cgi?id=1282768
*.css: filter: url("chrome://global/skin/filters.svg#fill");, fill:

Bug 1283385: https://bugzilla.mozilla.org/show_bug.cgi?id=1283385
Bug 1323109: https://bugzilla.mozilla.org/show_bug.cgi?id=1323109
Bug 1314544: https://bugzilla.mozilla.org/show_bug.cgi?id=1314544
Bug 1286182: https://bugzilla.mozilla.org/show_bug.cgi?id=1286182

Bug 1325922: https://bugzilla.mozilla.org/show_bug.cgi?id=1325922
A note - not implemented: Bug 1282768:
https://bugzilla.mozilla.org/show_bug.cgi?id=1282768
*.css: filter: url("chrome://global/skin/filters.svg#fill");, fill:

Bug 1320225: https://bugzilla.mozilla.org/show_bug.cgi?id=1320225
Bug 1341190: https://bugzilla.mozilla.org/show_bug.cgi?id=1341190
This commit is contained in:
janekptacijarabaci 2018-03-30 12:17:17 +02:00 • committed by Roy Tam
commit cac5c5adc7
34 changed files with 1267 additions and 248 deletions

View file

@ -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");
}

View file

@ -160,8 +160,10 @@
hidden="true"
orient="vertical"
noautofocus="true"
noautohide="true"
consumeoutsideclicks="false"
level="parent">
level="parent"
tabspecific="true">
<iframe id="dateTimePopupFrame"/>
</panel>

View file

@ -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 (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);
}

View file

@ -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) {}

View file

@ -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]

View file

@ -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"
},
];

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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]

View file

@ -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();
});

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
},
/**
* 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<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. 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++;
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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());
}
]]>

View file

@ -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;
}

View file

@ -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>

View file

@ -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

View file

@ -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">

View file

@ -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)

View file

@ -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() {

View file

@ -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,