From 2bc83ec22d5c22675170aacfc28e8546f2b78e91 Mon Sep 17 00:00:00 2001 From: Anna Yeddi Date: Wed, 7 Dec 2022 00:56:45 +0000 Subject: [PATCH] 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 `` 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 `` 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 --- .../tests/mochitest/elm/test_HTMLSpec.html | 1 - dom/base/test/file_focus_shadow_dom.html | 4 + dom/html/test/forms/mochitest.ini | 3 +- .../test/forms/test_input_date_bad_input.html | 14 +- ... test_input_datetime_calendar_button.html} | 107 +++++++----- ...st_input_datetime_input_change_events.html | 15 -- ...eset_default_value_input_change_event.html | 31 ---- .../forms/test_input_datetime_tabindex.html | 10 +- dom/html/test/forms/utils.js | 20 --- toolkit/actors/DateTimePickerChild.jsm | 4 + .../browser/browser_datetime_datepicker.js | 14 +- .../browser_datetime_datepicker_keynav.js | 29 ++++ .../browser_datetime_datepicker_markup.js | 152 ++++++++++++++++++ toolkit/content/tests/browser/head.js | 6 +- toolkit/content/widgets/datetimebox.css | 45 +++++- toolkit/content/widgets/datetimebox.js | 129 ++++++++------- .../en-US/toolkit/global/datetimebox.ftl | 13 +- xpcom/ds/StaticAtoms.py | 1 - 18 files changed, 397 insertions(+), 201 deletions(-) rename dom/html/test/forms/{test_input_datetime_reset_button.html => test_input_datetime_calendar_button.html} (53%) delete mode 100644 dom/html/test/forms/utils.js diff --git a/accessible/tests/mochitest/elm/test_HTMLSpec.html b/accessible/tests/mochitest/elm/test_HTMLSpec.html index 2e3c354fe52d..a5cc175123d6 100644 --- a/accessible/tests/mochitest/elm/test_HTMLSpec.html +++ b/accessible/tests/mochitest/elm/test_HTMLSpec.html @@ -887,7 +887,6 @@ { role: ROLE_SPINBUTTON }, { role: ROLE_TEXT_LEAF }, { role: ROLE_ENTRY }, - { role: ROLE_PUSHBUTTON }, ], }; testElm("input_time", obj); diff --git a/dom/base/test/file_focus_shadow_dom.html b/dom/base/test/file_focus_shadow_dom.html index 1f94c185df9b..6fa9d1b88e73 100644 --- a/dom/base/test/file_focus_shadow_dom.html +++ b/dom/base/test/file_focus_shadow_dom.html @@ -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)"); diff --git a/dom/html/test/forms/mochitest.ini b/dom/html/test/forms/mochitest.ini index 745a946e3df2..2097d43a04b8 100644 --- a/dom/html/test/forms/mochitest.ini +++ b/dom/html/test/forms/mochitest.ini @@ -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] diff --git a/dom/html/test/forms/test_input_date_bad_input.html b/dom/html/test/forms/test_input_date_bad_input.html index 7c8e34e25166..846be89ca37b 100644 --- a/dom/html/test/forms/test_input_date_bad_input.html +++ b/dom/html/test/forms/test_input_date_bad_input.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); - } } diff --git a/dom/html/test/forms/test_input_datetime_reset_button.html b/dom/html/test/forms/test_input_datetime_calendar_button.html similarity index 53% rename from dom/html/test/forms/test_input_datetime_reset_button.html rename to dom/html/test/forms/test_input_datetime_calendar_button.html index 96452136413d..d7bbe28dd889 100644 --- a/dom/html/test/forms/test_input_datetime_reset_button.html +++ b/dom/html/test/forms/test_input_datetime_calendar_button.html @@ -4,13 +4,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1479708 --> -Test required date/time input can't be reset +Test required date/datetime-local input's Calendar button -Mozilla Bug 1479708 +Created for Mozilla Bug 1479708 and updated by Mozilla Bug 1676068

@@ -29,7 +29,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1479708
 
diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html
index bb95e5033915..a7ef608303c8 100644
--- a/dom/html/test/forms/test_input_datetime_input_change_events.html
+++ b/dom/html/test/forms/test_input_datetime_input_change_events.html
@@ -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).");
-    }
   }
 }
 
diff --git a/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
index 9e722c97329c..393de9fdeed9 100644
--- a/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
+++ b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
@@ -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;
diff --git a/dom/html/test/forms/test_input_datetime_tabindex.html b/dom/html/test/forms/test_input_datetime_tabindex.html
index 60dff9177d35..207a7a8a8ecd 100644
--- a/dom/html/test/forms/test_input_datetime_tabindex.html
+++ b/dom/html/test/forms/test_input_datetime_tabindex.html
@@ -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) {
diff --git a/dom/html/test/forms/utils.js b/dom/html/test/forms/utils.js
deleted file mode 100644
index fee762995ccf..000000000000
--- a/dom/html/test/forms/utils.js
+++ /dev/null
@@ -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);
-}
diff --git a/toolkit/actors/DateTimePickerChild.jsm b/toolkit/actors/DateTimePickerChild.jsm
index 7a212c383155..8f0a4b668707 100644
--- a/toolkit/actors/DateTimePickerChild.jsm
+++ b/toolkit/actors/DateTimePickerChild.jsm
@@ -89,6 +89,10 @@ class DateTimePickerChild extends JSWindowActorChild {
   receiveMessage(aMessage) {
     switch (aMessage.name) {
       case "FormDateTime:PickerClosed": {
+        if (!this._inputElement) {
+          return;
+        }
+
         this.close();
         break;
       }
diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js
index b2294a2b7e2b..0d60976c75e8 100644
--- a/toolkit/content/tests/browser/browser_datetime_datepicker.js
+++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js
@@ -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(
diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker_keynav.js b/toolkit/content/tests/browser/browser_datetime_datepicker_keynav.js
index a8236ef1aea9..e3cd271305a9 100644
--- a/toolkit/content/tests/browser/browser_datetime_datepicker_keynav.js
+++ b/toolkit/content/tests/browser/browser_datetime_datepicker_keynav.js
@@ -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();
 });
diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker_markup.js b/toolkit/content/tests/browser/browser_datetime_datepicker_markup.js
index a2ac9d315a87..f3773a59dd85 100644
--- a/toolkit/content/tests/browser/browser_datetime_datepicker_markup.js
+++ b/toolkit/content/tests/browser/browser_datetime_datepicker_markup.js
@@ -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, ");
+  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, ");
+  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, "
+  );
+
+  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);
+});
diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js
index 6944a5d5bced..9684d495e8bc 100644
--- a/toolkit/content/tests/browser/head.js
+++ b/toolkit/content/tests/browser/head.js
@@ -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();
diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css
index 65763b76ba65..b53cb59ce4ad 100644
--- a/toolkit/content/widgets/datetimebox.css
+++ b/toolkit/content/widgets/datetimebox.css
@@ -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;
+  }
+}
diff --git a/toolkit/content/widgets/datetimebox.js b/toolkit/content/widgets/datetimebox.js
index 73651e0e9e6f..623eb4695e12 100644
--- a/toolkit/content/widgets/datetimebox.js
+++ b/toolkit/content/widgets/datetimebox.js
@@ -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 
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 { - -
@@ -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(); } }; diff --git a/toolkit/locales/en-US/toolkit/global/datetimebox.ftl b/toolkit/locales/en-US/toolkit/global/datetimebox.ftl index 2ad6e2a26723..6b25a65120f2 100644 --- a/toolkit/locales/en-US/toolkit/global/datetimebox.ftl +++ b/toolkit/locales/en-US/toolkit/global/datetimebox.ftl @@ -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 +# field that opens/closes a date picker calendar dialog + +datetime-calendar = + .aria-label = Calendar diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py index ec3bab7b7ff4..7f03d0b3a8a5 100644 --- a/xpcom/ds/StaticAtoms.py +++ b/xpcom/ds/StaticAtoms.py @@ -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"),