зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
644635e9cb
Коммит
6c9a5814b5
|
@ -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 don’t 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"),
|
||||
|
|
Загрузка…
Ссылка в новой задаче