Skip to content

Commit 7e21de9

Browse files
committed
Bug 1812669 - Add picker button to <input type="time"> fields when time picker enabled. r=fluent-reviewers,mkennedy,flod
Extends the calendar picker button to also support a time picker button. This restores keyboard access to time input fields when the picker is enabled. I reused the history icon here, as I didn't see an existing purpose-built icon. Differential Revision: https://phabricator.services.mozilla.com/D303644
1 parent 792f96b commit 7e21de9

10 files changed

Lines changed: 178 additions & 138 deletions

File tree

dom/html/test/forms/test_input_datetime_calendar_button.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
function is_calendar_button_hidden(id) {
169169
const input = document.getElementById(id);
170170
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
171-
const calendarButton = shadowRoot.getElementById("calendar-button");
171+
const calendarButton = shadowRoot.getElementById("picker-button");
172172
const calendarButtonDisplay = SpecialPowers.wrap(window).getComputedStyle(calendarButton).display;
173173
return calendarButtonDisplay === "none";
174174
}

toolkit/actors/UAWidgetsChild.sys.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ export class UAWidgetsChild extends JSWindowActorChild {
9595
case "input":
9696
uri = "chrome://global/content/elements/datetimebox.js";
9797
widgetName = "DateTimeBoxWidget";
98-
prefKeys = ["privacy.resistFingerprinting"];
98+
prefKeys = [
99+
"dom.forms.datetime.timepicker",
100+
"privacy.resistFingerprinting",
101+
];
99102
break;
100103
case "marquee":
101104
uri = "chrome://global/content/elements/marquee.js";

toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ add_task(async function test_datepicker_keyboard_nav() {
1919
let browser = helper.tab.linkedBrowser;
2020
Assert.equal(helper.panel.state, "open", "Panel should be opened");
2121

22-
await testCalendarBtnAttribute("aria-expanded", "true");
22+
await testPickerBtnAttribute("aria-expanded", "true");
2323

2424
let closed = helper.promisePickerClosed();
2525

@@ -34,7 +34,7 @@ add_task(async function test_datepicker_keyboard_nav() {
3434
"Panel should be closed after Escape from anywhere on the window"
3535
);
3636

37-
await testCalendarBtnAttribute("aria-expanded", "false");
37+
await testPickerBtnAttribute("aria-expanded", "false");
3838

3939
let ready = helper.waitForPickerReady();
4040

@@ -57,7 +57,7 @@ add_task(async function test_datepicker_keyboard_nav() {
5757

5858
await ready;
5959

60-
await testCalendarBtnAttribute("aria-expanded", "true");
60+
await testPickerBtnAttribute("aria-expanded", "true");
6161

6262
Assert.equal(
6363
helper.panel.state,
@@ -134,7 +134,7 @@ add_task(async function test_datepicker_keyboard_nav() {
134134

135135
await ready;
136136

137-
await testCalendarBtnAttribute("aria-expanded", "true");
137+
await testPickerBtnAttribute("aria-expanded", "true");
138138

139139
Assert.equal(helper.panel.state, "open", "Panel should be opened on Space");
140140

@@ -168,7 +168,7 @@ add_task(async function test_datepicker_keyboard_nav() {
168168
"Panel should be closed on Escape"
169169
);
170170

171-
await testCalendarBtnAttribute("aria-expanded", "false");
171+
await testPickerBtnAttribute("aria-expanded", "false");
172172

173173
// Check the focus is returned to the Day field
174174
await SpecialPowers.spawn(browser, [], async () => {
@@ -206,7 +206,7 @@ add_task(async function test_datepicker_keyboard_nav() {
206206
"Panel should be opened on Enter from the Calendar button"
207207
);
208208

209-
await testCalendarBtnAttribute("aria-expanded", "true");
209+
await testPickerBtnAttribute("aria-expanded", "true");
210210

211211
// Move focus from 2016-11-16 to 2016-11-17
212212
EventUtils.synthesizeKey("KEY_ArrowRight", {});
@@ -221,13 +221,13 @@ add_task(async function test_datepicker_keyboard_nav() {
221221
"closed",
222222
"Panel should be closed on Space from the date gridcell"
223223
);
224-
await testCalendarBtnAttribute("aria-expanded", "false");
224+
await testPickerBtnAttribute("aria-expanded", "false");
225225

226226
// Check the focus is returned to the Calendar button
227227
await SpecialPowers.spawn(browser, [], async () => {
228228
const input = content.document.querySelector("input");
229229
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
230-
const calendarBtn = shadowRoot.getElementById("calendar-button");
230+
const calendarBtn = shadowRoot.getElementById("picker-button");
231231
// Testing the focus position within content:
232232
Assert.equal(
233233
input,
@@ -249,7 +249,7 @@ add_task(async function test_datepicker_keyboard_nav() {
249249
await SpecialPowers.spawn(browser, [], () => {
250250
const input = content.document.querySelector("input");
251251
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
252-
const calendarBtn = shadowRoot.getElementById("calendar-button");
252+
const calendarBtn = shadowRoot.getElementById("picker-button");
253253
Assert.equal(
254254
calendarBtn.children[0].tagName,
255255
"svg",
@@ -269,7 +269,7 @@ add_task(async function test_datepicker_keyboard_nav() {
269269
"Panel should be opened on Space from the Calendar button"
270270
);
271271

272-
await testCalendarBtnAttribute("aria-expanded", "true");
272+
await testPickerBtnAttribute("aria-expanded", "true");
273273

274274
await helper.tearDown();
275275
});

toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js

Lines changed: 10 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,14 @@ add_task(async function test_calendar_button_markup_date() {
340340

341341
let closed = helper.promisePickerClosed();
342342

343-
await testCalendarBtnAttribute("aria-expanded", "true");
344-
await testCalendarBtnAttribute("aria-label", null, true);
345-
await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar");
343+
await testPickerBtnAttribute("aria-expanded", "true");
344+
await testPickerBtnAttribute("aria-label", null, true);
345+
await testPickerBtnAttribute("data-l10n-id", "datetime-calendar");
346346

347347
await SpecialPowers.spawn(browser, [], () => {
348348
const input = content.document.querySelector("input");
349349
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
350-
const calendarBtn = shadowRoot.getElementById("calendar-button");
350+
const calendarBtn = shadowRoot.getElementById("picker-button");
351351

352352
Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button");
353353
Assert.ok(
@@ -366,7 +366,7 @@ add_task(async function test_calendar_button_markup_date() {
366366
"Panel should be closed on click on the Calendar button"
367367
);
368368

369-
await testCalendarBtnAttribute("aria-expanded", "false");
369+
await testPickerBtnAttribute("aria-expanded", "false");
370370

371371
await helper.tearDown();
372372
});
@@ -387,14 +387,14 @@ add_task(async function test_calendar_button_markup_datetime() {
387387

388388
let closed = helper.promisePickerClosed();
389389

390-
await testCalendarBtnAttribute("aria-expanded", "true");
391-
await testCalendarBtnAttribute("aria-label", null, true);
392-
await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar");
390+
await testPickerBtnAttribute("aria-expanded", "true");
391+
await testPickerBtnAttribute("aria-label", null, true);
392+
await testPickerBtnAttribute("data-l10n-id", "datetime-calendar");
393393

394394
await SpecialPowers.spawn(browser, [], () => {
395395
const input = content.document.querySelector("input");
396396
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
397-
const calendarBtn = shadowRoot.getElementById("calendar-button");
397+
const calendarBtn = shadowRoot.getElementById("picker-button");
398398

399399
Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button");
400400
Assert.ok(
@@ -413,71 +413,7 @@ add_task(async function test_calendar_button_markup_datetime() {
413413
"Panel should be closed on click on the Calendar button"
414414
);
415415

416-
await testCalendarBtnAttribute("aria-expanded", "false");
416+
await testPickerBtnAttribute("aria-expanded", "false");
417417

418418
await helper.tearDown();
419419
});
420-
421-
/**
422-
* Test that time input field does not include a Calendar button,
423-
* but opens a time picker panel on click within the field (with a pref)
424-
*/
425-
add_task(async function test_calendar_button_markup_time() {
426-
info("Test that type=time input field does not include a Calendar button");
427-
428-
// Toggle a pref to allow a time picker to be shown
429-
await SpecialPowers.pushPrefEnv({
430-
set: [["dom.forms.datetime.timepicker", true]],
431-
});
432-
433-
let testTab = await BrowserTestUtils.openNewForegroundTab(
434-
gBrowser,
435-
"data:text/html, <input type='time'>"
436-
);
437-
438-
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
439-
const input = content.document.querySelector("input");
440-
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
441-
const calendarBtn = shadowRoot.getElementById("calendar-button");
442-
443-
Assert.ok(
444-
ContentTaskUtils.isHidden(calendarBtn),
445-
"The Calendar control within a type=time input field is programmatically hidden"
446-
);
447-
});
448-
449-
let ready = helper.waitForPickerReady();
450-
451-
await BrowserTestUtils.synthesizeMouseAtCenter(
452-
"input",
453-
{},
454-
gBrowser.selectedBrowser
455-
);
456-
457-
await ready;
458-
459-
Assert.equal(
460-
helper.panel.state,
461-
"open",
462-
"Time picker panel should be opened on click from anywhere within the time input field"
463-
);
464-
465-
let closed = helper.promisePickerClosed();
466-
467-
await BrowserTestUtils.synthesizeMouseAtCenter(
468-
"input",
469-
{},
470-
gBrowser.selectedBrowser
471-
);
472-
473-
await closed;
474-
475-
Assert.equal(
476-
helper.panel.state,
477-
"closed",
478-
"Time picker panel should be closed on click from anywhere within the time input field"
479-
);
480-
481-
BrowserTestUtils.removeTab(testTab);
482-
await SpecialPowers.popPrefEnv();
483-
});

toolkit/content/tests/browser/datetime/browser_datetime_timepicker.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,84 @@ add_task(async function test_time_spinner_markup() {
112112

113113
await helper.tearDown();
114114
});
115+
116+
/**
117+
* Test that time input field has a picker button
118+
* with an accessible markup with the time picker enabled
119+
*/
120+
add_task(async function test_picker_button_markup_time() {
121+
info(
122+
"Test that type=time input field has a picker button with an accessible markup with the time picker enabled"
123+
);
124+
125+
await helper.openPicker("data:text/html, <input type='time'>");
126+
let browser = helper.tab.linkedBrowser;
127+
128+
Assert.equal(helper.panel.state, "open", "Panel is visible");
129+
130+
let closed = helper.promisePickerClosed();
131+
132+
await testPickerBtnAttribute("aria-expanded", "true");
133+
await testPickerBtnAttribute("aria-label", null, true);
134+
await testPickerBtnAttribute("data-l10n-id", "datetime-time");
135+
136+
await SpecialPowers.spawn(browser, [], () => {
137+
const input = content.document.querySelector("input");
138+
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
139+
const pickerBtn = shadowRoot.getElementById("picker-button");
140+
141+
Assert.equal(pickerBtn.tagName, "BUTTON", "Picker control is a button");
142+
Assert.ok(
143+
ContentTaskUtils.isVisible(pickerBtn),
144+
"The picker button is visible"
145+
);
146+
147+
pickerBtn.click();
148+
});
149+
150+
await closed;
151+
152+
Assert.equal(
153+
helper.panel.state,
154+
"closed",
155+
"Panel should be closed on click on the picker button"
156+
);
157+
158+
await testPickerBtnAttribute("aria-expanded", "false");
159+
160+
await helper.tearDown();
161+
});
162+
163+
/**
164+
* Test that time input field does not include a picker button
165+
* with the time picker disabled
166+
*/
167+
add_task(async function test_picker_button_markup_time_picker_off() {
168+
info(
169+
"Test that type=time input field does not include a picker button with the time picker disabled"
170+
);
171+
172+
// Toggle off the time picker pref
173+
await SpecialPowers.pushPrefEnv({
174+
set: [["dom.forms.datetime.timepicker", false]],
175+
});
176+
177+
let testTab = await BrowserTestUtils.openNewForegroundTab(
178+
gBrowser,
179+
"data:text/html, <input type='time'>"
180+
);
181+
182+
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
183+
const input = content.document.querySelector("input");
184+
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
185+
const pickerBtn = shadowRoot.getElementById("picker-button");
186+
187+
Assert.ok(
188+
ContentTaskUtils.isHidden(pickerBtn),
189+
"The Calendar control within a type=time input field is programmatically hidden with the time picker disabled"
190+
);
191+
});
192+
193+
await SpecialPowers.popPrefEnv();
194+
BrowserTestUtils.removeTab(testTab);
195+
});

toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ add_task(async function () {
1212
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
1313

1414
EventUtils.synthesizeMouseAtCenter(
15-
shadowRoot.getElementById("calendar-button"),
15+
shadowRoot.getElementById("picker-button"),
1616
{}
1717
);
1818

toolkit/content/tests/browser/datetime/head.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,8 @@ class DateTimeTestHelper {
4444
if (openMethod === "click") {
4545
await SpecialPowers.spawn(bc, [], () => {
4646
const input = content.document.querySelector("input");
47-
if (input.type == "time") {
48-
input.click();
49-
} else {
50-
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
51-
shadowRoot.getElementById("calendar-button").click();
52-
}
47+
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
48+
shadowRoot.getElementById("picker-button").click();
5349
});
5450
} else if (openMethod === "showPicker") {
5551
await SpecialPowers.spawn(bc, [], function () {
@@ -257,13 +253,13 @@ function testAttributeL10n(el, attr, id, args = null) {
257253
}
258254

259255
/**
260-
* Helper function to check the value of a Calendar button's specific attribute
256+
* Helper function to check the value of a picker button's specific attribute
261257
*
262258
* @param {string} attr: The name of the attribute to be tested
263259
* @param {string} val: Value that is expected to be assigned to the attribute.
264260
* @param {boolean} presenceOnly: If "true", test only the presence of the attribute
265261
*/
266-
async function testCalendarBtnAttribute(attr, val, presenceOnly = false) {
262+
async function testPickerBtnAttribute(attr, val, presenceOnly = false) {
267263
let browser = helper.tab.linkedBrowser;
268264

269265
await SpecialPowers.spawn(
@@ -272,18 +268,18 @@ async function testCalendarBtnAttribute(attr, val, presenceOnly = false) {
272268
(attr, val, presenceOnly) => {
273269
const input = content.document.querySelector("input");
274270
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
275-
const calendarBtn = shadowRoot.getElementById("calendar-button");
271+
const pickerBtn = shadowRoot.getElementById("picker-button");
276272

277273
if (presenceOnly) {
278274
Assert.ok(
279-
calendarBtn.hasAttribute(attr),
280-
`Calendar button has ${attr} attribute`
275+
pickerBtn.hasAttribute(attr),
276+
`Picker button has ${attr} attribute`
281277
);
282278
} else {
283279
Assert.equal(
284-
calendarBtn.getAttribute(attr),
280+
pickerBtn.getAttribute(attr),
285281
val,
286-
`Calendar button has ${attr} attribute set to ${val}`
282+
`Picker button has ${attr} attribute set to ${val}`
287283
);
288284
}
289285
}

0 commit comments

Comments
 (0)