Bug 1676068 - Datepicker Pt.3 - Replace Reset button in the DateTimeBox with Calendar one. r=Jamie,fluent-reviewers,mconley,kcochrane

Done:
- Functionality of the button was changed from cleaning the field value to toggling the datepicker dialog.
- Pre-existing issues resolved: Updated datetimebox.js to use `keydown` event instead of the deprecated `keypress` (which does not preventDefault for buttons), added default handling of digits for `keydown`, and added a check to avoid running duplicate cleanup when the picker is closed
- Removed ability to open a date picker from editable elements of the `<input type="date">` field and ensured keyboard and mouse/touch click are working for the Calendar button, while Escape functionality remained
- Updated `onBlur` logic for the button in accordance with its new functionaility
- New Calendar SVG icon was created by Katie Caldwell and optimized by Sam Foster
- Provided HCM support for the Calendar button
- Ensured the Calendar button is not shown on `<input type=time>` to preserve the existent UX
- Added Fluent l10n to the content process and provided `title` to the image button (SVG is marked as `role="none"` to avoid exposure to assistive technology)
- Added functional and markup tests for the Calendar button and its localization, updated Reset button tests to the Calendar one

ToDo (further patch):
1. Pt.4 - Ensure keyboard support when focus moves between processes

ToDo (other dependencies/bugs):
1. Investigations into if we should show a calendar button for read-only fields and if a Reset button would be benefitial to be shown for a `type=time` inputs

Depends on D139981

Differential Revision: https://phabricator.services.mozilla.com/D141175
This commit is contained in:
Anna Yeddi 2022-12-01 19:50:03 +00:00
Родитель 644635e9cb
Коммит 6c9a5814b5
18 изменённых файлов: 403 добавлений и 196 удалений

Просмотреть файл

@ -887,7 +887,6 @@
{ role: ROLE_SPINBUTTON },
{ role: ROLE_TEXT_LEAF },
{ role: ROLE_ENTRY },
{ role: ROLE_PUSHBUTTON },
],
};
testElm("input_time", obj);

Просмотреть файл

@ -78,6 +78,8 @@
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(lastFocusTarget, shadowDate, "Should have focused date element with a calendar button in shadow DOM. (3)");
synthesizeKey("KEY_Tab");
opener.is(shadowIframe.contentDocument.activeElement,
shadowIframe.contentDocument.documentElement,
"Should have focused document element in shadow iframe. (3)");
@ -102,6 +104,8 @@
shadowIframe.contentDocument.documentElement,
"Should have focused document element in shadow iframe. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowDate, "Should have focused date element with a calendar button in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
synthesizeKey("KEY_Tab", {shiftKey: true});
opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");

Просмотреть файл

@ -2,7 +2,6 @@
support-files =
save_restore_radio_groups.sjs
test_input_number_data.js
utils.js
!/dom/html/test/reflect.js
FAIL.html
PASS.html
@ -43,7 +42,7 @@ support-files = file_double_submit.html
[test_input_datetime_focus_state.html]
[test_input_datetime_hidden.html]
[test_input_datetime_readonly.html]
[test_input_datetime_reset_button.html]
[test_input_datetime_calendar_button.html]
[test_input_datetime_reset_default_value_input_change_event.html]
[test_input_datetime_tabindex.html]
[test_input_defaultValue.html]

Просмотреть файл

@ -49,7 +49,7 @@ function checkValidity(aElement, aIsBadInput) {
is(window.getComputedStyle(aElement).getPropertyValue('background-color'),
aIsBadInput ? "rgb(255, 0, 0)" : "rgb(0, 255, 0)",
(aIsBadInput ? ":invalid" : "valid") + " pseudo-classs should apply");
(aIsBadInput ? ":invalid" : "valid") + " pseudo-class should apply");
}
function sendKeys(aKey) {
@ -62,11 +62,6 @@ function sendKeys(aKey) {
function test() {
var elem = document.getElementById("input");
var inputRect = input.getBoundingClientRect();
// Points over the input's reset button
var resetButton_X = inputRect.width - 15;
var resetButton_Y = inputRect.height / 2;
elem.focus();
sendKeys("02312017");
@ -102,13 +97,6 @@ function test() {
sendKeys("02292017");
elem.blur();
checkValidity(elem, true);
// Reset button is desktop only.
if (isDesktop) {
// Clearing all fields should clear bad input validity state as well.
synthesizeMouse(input, resetButton_X, resetButton_Y, {});
checkValidity(elem, false);
}
}
</script>

Просмотреть файл

@ -4,13 +4,13 @@
https://bugzilla.mozilla.org/show_bug.cgi?id=1479708
-->
<head>
<title>Test required date/time input can't be reset</title>
<title>Test required date/datetime-local input's Calendar button</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1479708">Mozilla Bug 1479708</a>
Created for <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1479708">Mozilla Bug 1479708</a> and updated by <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1676068">Mozilla Bug 1676068</a>
<p id="display"></p>
<div id="content">
<input type="date" id="id_date" value="2017-06-08">
@ -39,19 +39,24 @@ function id_for_type(type, kind) {
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
if (!isDesktop) {
ok(true, "Mobile and tablet dont show reset button");
for (const input of document.querySelectorAll("input")) {
ok(
get_calendar_button(input.id).hidden,
`Calendar button is hidden on mobile/tablet (${input.id})`
);
}
return SimpleTest.finish();
}
// Initial load.
assert_reset_visible_all("");
assert_reset_hidden_all("required");
assert_calendar_visible_all("");
assert_calendar_visible_all("required");
assert_calendar_hidden_all("readonly");
assert_calendar_hidden_all("disabled");
// Dynamic toggling.
test_make_required("");
test_make_optional("required");
test_readonly_field_disabled();
test_make_readonly("");
test_make_editable("readonly");
test_disabled_field_disabled();
// Now toggle the inputs to the initial state, but while being
@ -61,8 +66,8 @@ SimpleTest.waitForFocus(function() {
is(input.getBoundingClientRect().width, 0, "Should be undisplayed");
}
test_make_required("required");
test_make_optional("");
test_make_readonly("readonly");
test_make_editable("");
// And test other toggling as well.
test_readonly_field_disabled();
@ -76,12 +81,22 @@ function test_disabled_field_disabled() {
const id = id_for_type(type, "disabled");
const input = document.getElementById(id);
ok(input.disabled, "Should be disabled");
ok(get_reset_button(id).disabled, `disabled's reset button is disabled (${id})`);
ok(input.disabled, `#${id} Should be disabled`);
ok(
get_calendar_button(id).hidden,
`disabled's Calendar button is hidden (${id})`
);
input.disabled = false;
ok(!input.disabled, "Should not be disabled anymore");
ok(!get_reset_button(id).disabled, `enabled field's reset button is not disabled (${id})`);
ok(!input.disabled, `#${id} Should not be disabled anymore`);
if (type === "time") {
assert_calendar_hidden(id);
} else {
ok(
!get_calendar_button(id).hidden,
`enabled field's Calendar button is not hidden (${id})`
);
}
input.disabled = true; // reset to the original state.
}
@ -92,64 +107,79 @@ function test_readonly_field_disabled() {
const id = id_for_type(type, "readonly");
const input = document.getElementById(id);
ok(input.readOnly, "Should be read-only");
ok(get_reset_button(id).disabled, `readonly field's reset button is disabled (${id})`);
ok(input.readOnly, `#${id} Should be read-only`);
ok(get_calendar_button(id).hidden, `readonly field's Calendar button is hidden (${id})`);
input.readOnly = false;
ok(!input.readOnly, "Should not be read-only anymore");
ok(!get_reset_button(id).disabled, `non-readonly field's reset button is not disabled (${id})`);
ok(!input.readOnly, `#${id} Should not be read-only anymore`);
if (type === "time") {
assert_calendar_hidden(id);
} else {
ok(
!get_calendar_button(id).hidden,
`non-readonly field's Calendar button is not hidden (${id})`
);
}
input.readOnly = true; // reset to the original state.
}
}
function test_make_required(kind) {
function test_make_readonly(kind) {
for (let type of kTypes) {
const id = id_for_type(type, kind);
const input = document.getElementById(id);
is(input.required, false, `Precondition: input #${id} is optional`);
is(input.readOnly, false, `Precondition: input #${id} is editable`);
input.required = true;
assert_reset_hidden(id);
input.readOnly = true;
assert_calendar_hidden(id);
}
}
function test_make_optional(kind) {
function test_make_editable(kind) {
for (let type of kTypes) {
const id = id_for_type(type, kind);
const input = document.getElementById(id);
is(input.required, true, `Precondition: input #${id} is required`);
is(input.readOnly, true, `Precondition: input #${id} is read-only`);
input.required = false;
assert_reset_visible(id);
input.readOnly = false;
if (type === "time") {
assert_calendar_hidden(id);
} else {
assert_calendar_visible(id);
}
}
}
function assert_reset_visible_all(kind) {
function assert_calendar_visible_all(kind) {
for (let type of kTypes) {
assert_reset_visible(id_for_type(type, kind));
if (type === "time") {
assert_calendar_hidden(id_for_type(type, kind));
} else {
assert_calendar_visible(id_for_type(type, kind));
}
}
}
function assert_reset_visible(id) {
const resetButton = get_reset_button(id);
is(resetButton.style.visibility, "", `Reset button is visible on #${id}`);
function assert_calendar_visible(id) {
const calendarButton = get_calendar_button(id);
is(calendarButton.hidden, false, `Calendar button is not hidden on #${id}`);
}
function assert_reset_hidden_all(kind) {
function assert_calendar_hidden_all(kind) {
for (let type of kTypes) {
assert_reset_hidden(id_for_type(type, kind));
assert_calendar_hidden(id_for_type(type, kind));
}
}
function assert_reset_hidden(id) {
const resetButton = get_reset_button(id);
is(resetButton.style.visibility, "hidden", `Reset button is hidden on #${id}`);
function assert_calendar_hidden(id) {
const calendarButton = get_calendar_button(id);
is(calendarButton.hidden, true, `Calendar button is hidden on #${id}`);
}
function get_reset_button(id) {
function get_calendar_button(id) {
const input = document.getElementById(id);
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
return shadowRoot.getElementById("reset-button");
return shadowRoot.getElementById("calendar-button");
}
</script>

Просмотреть файл

@ -45,11 +45,6 @@ SimpleTest.waitForFocus(function() {
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.");
@ -76,16 +71,6 @@ function test() {
is(input.value, expectedValues[i][1], "Check that value was set correctly (2).");
is(changeEvents[i], 3, "Change event should be dispatched (2).");
is(inputEvents[i], 3, "Input event should be dispatched (2).");
// Reset button is desktop only.
if (isDesktop) {
// 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], 4, "Change event should be dispatched (3).");
is(inputEvents[i], 4, "Input event should be dispatched (3).");
}
}
}

Просмотреть файл

@ -51,13 +51,6 @@ var numberInputEvents = 0;
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
if (isDesktopUserAgent(navigator)) {
test_reset_in_ui_triggers_change_and_input_event(
"time", numberChangeEvents, numberInputEvents);
test_reset_in_ui_triggers_change_and_input_event(
"date", numberChangeEvents, numberInputEvents);
}
test_reset_in_script_does_not_trigger_change_and_input_event(
"time2", numberChangeEvents, numberInputEvents);
test_reset_in_script_does_not_trigger_change_and_input_event(
@ -71,30 +64,6 @@ SimpleTest.waitForFocus(function() {
SimpleTest.finish();
});
function test_reset_in_ui_triggers_change_and_input_event(
inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) {
const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
var input = document.getElementById(inputFieldName);
is(input.value, input.defaultValue,
"Check " + inputFieldName + "'s default value is initialized correctly.");
is(numberChangeEvents, oldNumberChangeEvents,
"Check numberChangeEvents is initialized correctly for " + inputFieldName +
".");
is(numberInputEvents, oldNumberInputEvents,
"Check numberInputEvents is initialized correctly for " + inputFieldName +
".");
simulateUserClicksResetButton(input);
is(input.value, "",
"Check " + inputFieldName + "'s value was set correctly.");
is(numberChangeEvents, oldNumberChangeEvents + 1,
"Change event should be dispatched for " + inputFieldName + ".");
is(numberInputEvents, oldNumberInputEvents + 1,
"Input event should be dispatched for " + inputFieldName + ".");
}
function test_reset_in_script_does_not_trigger_change_and_input_event(
inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) {
const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;

Просмотреть файл

@ -58,7 +58,15 @@ function testTabindex(type) {
is(document.activeElement, input1,
"input element with tabindex=0 is focusable");
let fieldCount = type == "datetime-local" ? 6 : 3;
// Time input does not include a Calendar button
let fieldCount;
if (type == "datetime-local") {
fieldCount = 7;
} else if (type == "date") {
fieldCount = 4;
} else {
fieldCount = 3;
};
// Advance through inner fields.
for (let i = 0; i < fieldCount - 1; ++i) {

Просмотреть файл

@ -1,20 +0,0 @@
/**
* Simulate the user clicks the reset button of the given date or time element.
*
* @param inputElement A date or time input element of default size.
*/
function simulateUserClicksResetButton(inputElement) {
var inputRectangle = inputElement.getBoundingClientRect();
const offsetX = inputRectangle.width - 15;
const offsetY = inputRectangle.height / 2;
synthesizeMouse(inputElement, offsetX, offsetY, {});
}
/**
* @param navigator https://www.w3schools.com/jsref/obj_navigator.asp.
* @return true, iff it's a desktop user agent.
*/
function isDesktopUserAgent(navigator) {
return !/Mobile|Tablet/.test(navigator.userAgent);
}

Просмотреть файл

@ -89,6 +89,10 @@ class DateTimePickerChild extends JSWindowActorChild {
receiveMessage(aMessage) {
switch (aMessage.name) {
case "FormDateTime:PickerClosed": {
if (!this._inputElement) {
return;
}
this.close();
break;
}

Просмотреть файл

@ -314,11 +314,15 @@ add_task(async function test_datepicker_reopen_state() {
Assert.equal(helper.panel.state, "closed", "Panel should be closed");
// Ensures the picker opens to the month of the input value
await BrowserTestUtils.synthesizeMouseAtCenter(
"input",
{},
gBrowser.selectedBrowser
);
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
let input = content.document.querySelector("input");
function getCalendarButton(input) {
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
return shadowRoot.getElementById("calendar-button");
}
getCalendarButton(input).click();
});
await helper.waitForPickerReady();
Assert.equal(

Просмотреть файл

@ -11,6 +11,27 @@ const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
timeZone: "UTC",
}).format;
/**
* Helper function to check the value of a Calendar button's specific attribute
*
* @param {String} attr: The name of the attribute to be tested
* @param {String} val: Value that is expected to be assigned to the attribute
*/
async function testCalendarBtnAttribute(attr, val) {
let browser = helper.tab.linkedBrowser;
await SpecialPowers.spawn(browser, [attr, val], (attr, val) => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
const calendarBtn = shadowRoot.getElementById("calendar-button");
Assert.equal(
calendarBtn.getAttribute(attr),
val,
`Calendar button has ${attr} attribute set to ${val}`
);
});
}
let helper = new DateTimeTestHelper();
registerCleanupFunction(() => {
@ -46,6 +67,8 @@ add_task(async function test_datepicker_keyboard_nav() {
"Panel should be closed after Escape from anywhere on the window"
);
await testCalendarBtnAttribute("aria-expanded", "false");
let ready = helper.waitForPickerReady();
// Ensure focus is on the input field
@ -67,6 +90,8 @@ add_task(async function test_datepicker_keyboard_nav() {
await ready;
await testCalendarBtnAttribute("aria-expanded", "true");
Assert.equal(
helper.panel.state,
"open",
@ -122,6 +147,8 @@ add_task(async function test_datepicker_keyboard_nav() {
await ready;
await testCalendarBtnAttribute("aria-expanded", "true");
Assert.equal(helper.panel.state, "open", "Panel should be opened on Space");
await BrowserTestUtils.waitForCondition(() => {
@ -146,5 +173,7 @@ add_task(async function test_datepicker_keyboard_nav() {
"Panel should be closed on Escape"
);
await testCalendarBtnAttribute("aria-expanded", "false");
await helper.tearDown();
});

Просмотреть файл

@ -17,6 +17,40 @@ const MONTH_YEAR = ".month-year",
SPINNER_MONTH = "#spinner-month",
SPINNER_YEAR = "#spinner-year";
/**
* Helper function to check the value of a Calendar button's specific attribute
*
* @param {String} attr: The name of the attribute to be tested
* @param {String} val: Value that is expected to be assigned to the attribute.
* @param {Boolean} presenceOnly: If "true", test only the presence of the attribute
*/
async function testCalendarBtnAttribute(attr, val, presenceOnly = false) {
let browser = helper.tab.linkedBrowser;
await SpecialPowers.spawn(
browser,
[attr, val, presenceOnly],
(attr, val, presenceOnly) => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
const calendarBtn = shadowRoot.getElementById("calendar-button");
if (presenceOnly) {
Assert.ok(
calendarBtn.hasAttribute(attr),
`Calendar button has ${attr} attribute`
);
} else {
Assert.equal(
calendarBtn.getAttribute(attr),
val,
`Calendar button has ${attr} attribute set to ${val}`
);
}
}
);
}
let helper = new DateTimeTestHelper();
registerCleanupFunction(() => {
@ -326,3 +360,121 @@ add_task(async function test_datepicker_markup_refresh() {
await helper.tearDown();
});
/**
* Test that date input field has a Calendar button with an accessible markup
*/
add_task(async function test_calendar_button_markup_date() {
info(
"Test that type=date input field has a Calendar button with an accessible markup"
);
await helper.openPicker("data:text/html, <input type='date'>");
let browser = helper.tab.linkedBrowser;
Assert.equal(helper.panel.state, "open", "Panel is visible");
let closed = helper.promisePickerClosed();
await testCalendarBtnAttribute("aria-expanded", "true");
await testCalendarBtnAttribute("aria-label", null, true);
await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar");
await SpecialPowers.spawn(browser, [], () => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
const calendarBtn = shadowRoot.getElementById("calendar-button");
Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button");
Assert.ok(
ContentTaskUtils.is_visible(calendarBtn),
"The Calendar button is visible"
);
calendarBtn.click();
});
await closed;
Assert.equal(
helper.panel.state,
"closed",
"Panel should be closed on click on the Calendar button"
);
await testCalendarBtnAttribute("aria-expanded", "false");
await helper.tearDown();
});
/**
* Test that datetime-local input field has a Calendar button
* with an accessible markup
*/
add_task(async function test_calendar_button_markup_datetime() {
info(
"Test that type=datetime-local input field has a Calendar button with an accessible markup"
);
await helper.openPicker("data:text/html, <input type='datetime-local'>");
let browser = helper.tab.linkedBrowser;
Assert.equal(helper.panel.state, "open", "Panel is visible");
let closed = helper.promisePickerClosed();
await testCalendarBtnAttribute("aria-expanded", "true");
await testCalendarBtnAttribute("aria-label", null, true);
await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar");
await SpecialPowers.spawn(browser, [], () => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
const calendarBtn = shadowRoot.getElementById("calendar-button");
Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button");
Assert.ok(
ContentTaskUtils.is_visible(calendarBtn),
"The Calendar button is visible"
);
calendarBtn.click();
});
await closed;
Assert.equal(
helper.panel.state,
"closed",
"Panel should be closed on click on the Calendar button"
);
await testCalendarBtnAttribute("aria-expanded", "false");
await helper.tearDown();
});
/**
* Test that time input field does not include a Calendar button
*/
add_task(async function test_calendar_button_markup_time() {
info("Test that type=time input field does not include a Calendar button");
let testTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html, <input type='time'>"
);
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
const calendarBtn = shadowRoot.getElementById("calendar-button");
Assert.ok(
ContentTaskUtils.is_hidden(calendarBtn),
"The Calendar control within a type=time input field is programmatically hidden"
);
});
BrowserTestUtils.removeTab(testTab);
});

Просмотреть файл

@ -206,7 +206,11 @@ class DateTimeTestHelper {
});
if (openMethod === "click") {
await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, bc);
await SpecialPowers.spawn(bc, [], () => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
shadowRoot.getElementById("calendar-button").click();
});
} else if (openMethod === "showPicker") {
await SpecialPowers.spawn(bc, [], function() {
content.document.notifyUserGestureActivation();

Просмотреть файл

@ -45,23 +45,58 @@
user-select: none;
}
.datetime-reset-button {
.datetime-calendar-button {
-moz-context-properties: fill;
color: inherit;
font-size: inherit;
fill: currentColor;
opacity: .5;
opacity: .65;
background-color: transparent;
border: none;
border-radius: 0.2em;
flex: none;
padding-inline: 2px;
padding-block: 0;
margin-block: 0;
margin-inline: 0.075em 0.15em;
padding: 0 0.15em;
line-height: 1;
}
.datetime-reset-button-svg {
.datetime-calendar-button:focus-visible,
.datetime-calendar-button:hover {
opacity: 1;
outline: 0.15em solid SelectedItem;
}
.datetime-calendar-button-svg {
pointer-events: none;
/* When using a very small font-size, we don't want the button to take extra
* space (which will affect the baseline of the form control) */
max-width: 1em;
max-height: 1em;
}
@media (prefers-contrast) {
.datetime-calendar-button {
opacity: 1;
background-color: ButtonFace;
color: ButtonText;
}
.datetime-calendar-button:focus-visible,
.datetime-calendar-button:hover {
background-color: SelectedItem;
}
.datetime-calendar-button:focus-visible > .datetime-calendar-button-svg,
.datetime-calendar-button:hover > .datetime-calendar-button-svg {
background-color: SelectedItem;
-moz-context-properties: fill;
fill: SelectedItemText;
}
.datetime-calendar-button-svg {
background-color: ButtonFace;
-moz-context-properties: fill;
fill: ButtonText;
}
}

Просмотреть файл

@ -58,11 +58,7 @@ this.DateTimeBoxWidget = class {
}
teardown() {
this.mResetButton.removeEventListener("mousedown", this, {
mozSystemGroup: true,
});
this.mInputElement.removeEventListener("keypress", this, {
this.mInputElement.removeEventListener("keydown", this, {
capture: true,
mozSystemGroup: true,
});
@ -77,6 +73,7 @@ this.DateTimeBoxWidget = class {
this.l10n.disconnectRoot(this.shadowRoot);
this.removeEditFields();
this.removeEventListenersToField(this.mCalendarButton);
this.mInputElement = null;
@ -134,6 +131,7 @@ this.DateTimeBoxWidget = class {
this.generateContent();
this.mDateTimeBoxElement = this.shadowRoot.firstChild;
this.mCalendarButton = this.shadowRoot.getElementById("calendar-button");
this.mInputElement = this.element;
this.mLocales = this.window.getWebExposedLocales();
@ -189,14 +187,8 @@ this.DateTimeBoxWidget = class {
this.mHourPageUpDownInterval = 3;
this.mMinSecPageUpDownInterval = 10;
this.mResetButton = this.shadowRoot.getElementById("reset-button");
this.mResetButton.style.visibility = "hidden";
this.mResetButton.addEventListener("mousedown", this, {
mozSystemGroup: true,
});
this.mInputElement.addEventListener(
"keypress",
"keydown",
this,
{
capture: true,
@ -204,14 +196,6 @@ this.DateTimeBoxWidget = class {
},
false
);
// This is to open the picker when input element is clicked (this
// includes padding area).
this.mInputElement.addEventListener(
"click",
this,
{ mozSystemGroup: true },
false
);
// Those events are dispatched to <div class="datetimebox"> with bubble set
// to false. They are trapped inside UA Widget Shadow DOM and are not
@ -221,6 +205,7 @@ this.DateTimeBoxWidget = class {
});
this.buildEditFields();
this.buildCalendarBtn();
this.updateEditAttributes();
if (this.mInputElement.value) {
@ -243,10 +228,9 @@ this.DateTimeBoxWidget = class {
<!-- Each of the date/time input types will append their input child
- elements here -->
</span>
<button class="datetime-reset-button" id="reset-button" tabindex="-1" data-l10n-id="datetime-reset">
<svg xmlns="http://www.w3.org/2000/svg" class="datetime-reset-button-svg" width="12" height="12" viewBox="0 0 12 12">
<path d="M 3.9,3 3,3.9 5.1,6 3,8.1 3.9,9 6,6.9 8.1,9 9,8.1 6.9,6 9,3.9 8.1,3 6,5.1 Z M 12,6 A 6,6 0 0 1 6,12 6,6 0 0 1 0,6 6,6 0 0 1 6,0 6,6 0 0 1 12,6 Z"/>
<button data-l10n-id="datetime-calendar" class="datetime-calendar-button" id="calendar-button" aria-expanded="false">
<svg role="none" class="datetime-calendar-button-svg" xmlns="http://www.w3.org/2000/svg" id="calendar-16" viewBox="0 0 16 16" width="16" height="16" fill="context-fill">
<path d="M13.5 2H13V1c0-.6-.4-1-1-1s-1 .4-1 1v1H5V1c0-.6-.4-1-1-1S3 .4 3 1v1h-.5C1.1 2 0 3.1 0 4.5v9C0 14.9 1.1 16 2.5 16h11c1.4 0 2.5-1.1 2.5-2.5v-9C16 3.1 14.9 2 13.5 2zm0 12.5h-11c-.6 0-1-.4-1-1V6h13v7.5c0 .6-.4 1-1 1z"/>
</svg>
</button>
</div>
@ -374,12 +358,8 @@ this.DateTimeBoxWidget = class {
return field;
}
updateResetButtonVisibility() {
if (this.isAnyFieldAvailable(false) && !this.isRequired()) {
this.mResetButton.style.visibility = "";
} else {
this.mResetButton.style.visibility = "hidden";
}
updateCalendarButtonState(isExpanded) {
this.mCalendarButton.setAttribute("aria-expanded", isExpanded);
}
notifyInputElementValueChanged() {
@ -423,6 +403,8 @@ this.DateTimeBoxWidget = class {
setPickerState(aIsOpen) {
this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
this.mIsPickerOpen = aIsOpen;
// Calendar button's expanded state mirrors this.mIsPickerOpen
this.updateCalendarButtonState(this.mIsPickerOpen);
}
setFieldTabIndexAttribute(field) {
@ -454,9 +436,10 @@ this.DateTimeBoxWidget = class {
this.setFieldTabIndexAttribute(child);
}
this.mResetButton.disabled =
this.mInputElement.disabled || this.mInputElement.readOnly;
this.updateResetButtonVisibility();
this.mCalendarButton.hidden =
this.mInputElement.disabled ||
this.mInputElement.readOnly ||
this.mInputElement.type === "time";
}
isEmpty(aValue) {
@ -480,7 +463,6 @@ this.DateTimeBoxWidget = class {
if (aField.classList.contains("numeric")) {
aField.setAttribute("typeBuffer", "");
}
this.updateResetButtonVisibility();
}
openDateTimePicker() {
@ -547,8 +529,8 @@ this.DateTimeBoxWidget = class {
this.setPickerState(aEvent.detail);
break;
}
case "keypress": {
this.onKeyPress(aEvent);
case "keydown": {
this.onKeyDown(aEvent);
break;
}
case "click": {
@ -626,7 +608,7 @@ this.DateTimeBoxWidget = class {
);
}
shouldOpenDateTimePickerOnKeyPress() {
shouldOpenDateTimePickerOnKeyDown() {
if (!this.mLastFocusedField) {
return true;
}
@ -647,20 +629,34 @@ this.DateTimeBoxWidget = class {
return this.isTimeField(field);
}
onKeyPress(aEvent) {
this.log("onKeyPress key: " + aEvent.key);
onKeyDown(aEvent) {
this.log("onKeyDown key: " + aEvent.key);
switch (aEvent.key) {
// Toggle the picker on space/enter, close on Escape.
// Toggle the picker on Space/Enter on Calendar button or Space on input,
// close on Escape anywhere.
case "Escape": {
if (this.mIsPickerOpen) {
this.closeDateTimePicker();
aEvent.preventDefault();
}
break;
}
case "Enter":
case "Escape":
case " ": {
// always close, if opened
if (this.mIsPickerOpen) {
this.closeDateTimePicker();
} else if (
aEvent.key != "Escape" &&
aEvent.key != "Enter" &&
this.shouldOpenDateTimePickerOnKeyPress()
// open on Space from anywhere within the input
aEvent.key == " " &&
this.shouldOpenDateTimePickerOnKeyDown()
) {
this.openDateTimePicker();
} else if (
// open from the Calendar button on either keydown
aEvent.originalTarget == this.mCalendarButton &&
this.shouldOpenDateTimePickerOnKeyDown()
) {
this.openDateTimePicker();
} else {
@ -698,12 +694,16 @@ this.DateTimeBoxWidget = class {
break;
}
default: {
// printable characters
// digits and printable characters
const regex = new RegExp("Digit\\d");
const isDigit = regex.test(aEvent.code);
if (
aEvent.keyCode == 0 &&
!(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
isDigit ||
(aEvent.key == 0 &&
!(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey))
) {
this.handleKeypress(aEvent);
this.handleKeydown(aEvent);
aEvent.preventDefault();
}
break;
@ -723,13 +723,16 @@ this.DateTimeBoxWidget = class {
return;
}
if (aEvent.originalTarget == this.mResetButton) {
this.clearInputFields(false);
} else if (
!this.mIsPickerOpen &&
this.shouldOpenDateTimePickerOnClick(aEvent.originalTarget)
) {
this.openDateTimePicker();
// Toggle the picker on click on the Calendar button only
if (aEvent.originalTarget == this.mCalendarButton) {
if (
!this.mIsPickerOpen &&
this.shouldOpenDateTimePickerOnClick(aEvent.originalTarget)
) {
this.openDateTimePicker();
} else {
this.closeDateTimePicker();
}
}
}
@ -873,6 +876,18 @@ this.DateTimeBoxWidget = class {
});
}
buildCalendarBtn() {
this.addEventListenersToField(this.mCalendarButton);
// This is to open the picker when a Calendar button is clicked (this
// includes padding area).
this.mCalendarButton.addEventListener(
"click",
this,
{ mozSystemGroup: true },
false
);
}
clearInputFields(aFromInputElement) {
this.log("clearInputFields");
@ -1085,7 +1100,7 @@ this.DateTimeBoxWidget = class {
this.setInputValueFromFields();
}
handleKeypress(aEvent) {
handleKeydown(aEvent) {
if (!this.isEditable()) {
return;
}
@ -1228,7 +1243,6 @@ this.DateTimeBoxWidget = class {
aField.textContent = formatted;
aField.setAttribute("aria-valuetext", formatted);
this.updateResetButtonVisibility();
}
isAnyFieldAvailable(aForPicker = false) {
@ -1522,6 +1536,5 @@ this.DateTimeBoxWidget = class {
this.mDayPeriodField.textContent = aValue;
this.mDayPeriodField.setAttribute("value", aValue);
this.updateResetButtonVisibility();
}
};

Просмотреть файл

@ -2,10 +2,6 @@
# 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/.
# Date/time clear button
datetime-reset =
.aria-label = Clear
## Placeholders for date and time inputs
datetime-year-placeholder = yyyy
@ -34,3 +30,12 @@ datetime-millisecond =
.aria-label = Milliseconds
datetime-dayperiod =
.aria-label = AM/PM
## Calendar button for input type=date
# This label is used by screenreaders and other assistive technology
# to indicate the purpose of a toggle button inside of the <input type="date">
# field that opens/closes a date picker calendar dialog
datetime-calendar =
.aria-label = Calendar

Просмотреть файл

@ -310,7 +310,6 @@ STATIC_ATOMS = [
Atom("datetime", "datetime"),
Atom("datetime_local", "datetime-local"),
Atom("datetimeInputBoxWrapper", "datetime-input-box-wrapper"),
Atom("datetimeResetButton", "datetime-reset-button"),
Atom("dd", "dd"),
Atom("decimal", "decimal"),
Atom("decimalFormat", "decimal-format"),