diff options
| author | Skyiesac <jainsachi1202@gmail.com> | 2026-03-31 18:18:18 +0530 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-05-20 12:02:15 -0400 |
| commit | 3ca621a38642bfd8fc2bfd308f489cc2d9e76fb0 (patch) | |
| tree | 89fe0c429473ce10f6181fedd42e88bdbb530e4f | |
| parent | 4ea7648df5f970593e3cbe222a19fb624fa7208c (diff) | |
Fixed #36458 -- Trapped focus in the admin calendar and clock widgets.
| -rw-r--r-- | django/contrib/admin/static/admin/css/responsive.css | 12 | ||||
| -rw-r--r-- | django/contrib/admin/static/admin/css/widgets.css | 25 | ||||
| -rw-r--r-- | django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js | 114 | ||||
| -rw-r--r-- | js_tests/admin/DateTimeShortcuts.test.js | 2 | ||||
| -rw-r--r-- | tests/admin_widgets/tests.py | 16 |
5 files changed, 106 insertions, 63 deletions
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index 1a8a0ce600..509b4fb9ff 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -780,18 +780,6 @@ button { overflow: visible; } - .calendarbox:before, - .clockbox:before { - content: ""; - position: fixed; - top: 50%; - left: 50%; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.75); - transform: translate(-50%, -50%); - } - .calendarbox > *, .clockbox > * { position: relative; diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 928c81f376..55d91035a3 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -345,6 +345,13 @@ table p.datetime { padding-left: 0; } +.datetimeshortcuts button { + border: none; + background: none; + padding: 0; + cursor: pointer; +} + .datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon { position: relative; @@ -360,8 +367,8 @@ table p.datetime { background-size: 24px auto; } -.datetimeshortcuts a:focus .clock-icon, -.datetimeshortcuts a:hover .clock-icon { +.datetimeshortcuts button:focus .clock-icon, +.datetimeshortcuts button:hover .clock-icon { background-position: 0 -24px; } @@ -371,8 +378,8 @@ table p.datetime { top: -1px; } -.datetimeshortcuts a:focus .date-icon, -.datetimeshortcuts a:hover .date-icon { +.datetimeshortcuts button:focus .date-icon, +.datetimeshortcuts button:hover .date-icon { background-position: 0 -24px; } @@ -426,7 +433,8 @@ span.clearable-file-input label { .calendarbox, .clockbox { - margin: 5px auto; + margin: 0; + padding: 0; font-size: 0.75rem; width: 19em; text-align: center; @@ -436,11 +444,12 @@ span.clearable-file-input label { border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); overflow: hidden; - position: relative; + position: absolute; } -.clockbox { - width: auto; +.calendarbox::backdrop, +.clockbox::backdrop { + background: rgba(0, 0, 0, 0.75); } .calendar { diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index f6c61010d5..58a3523b00 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -136,8 +136,8 @@ e.preventDefault(); DateTimeShortcuts.handleClockQuicklink(num, -1); }); - const clock_link = document.createElement("a"); - clock_link.href = "#"; + const clock_link = document.createElement("button"); + clock_link.type = "button"; clock_link.id = DateTimeShortcuts.clockLinkName + num; clock_link.addEventListener("click", function (e) { e.preventDefault(); @@ -169,7 +169,7 @@ // Create clock link div // // Markup looks like: - // <div id="clockbox1" class="clockbox module" role="dialog" + // <dialog id="clockbox1" class="clockbox module" role="dialog" // aria-label="Choose a time"> // <h2>Choose a time</h2> // <ul class="timelist"> @@ -182,11 +182,9 @@ // <p class="calendar-cancel"> // <a href="#" role="button" aria-label="Close Clock">Cancel</a> // </p> - // </div> + // </dialog> - const clock_box = document.createElement("div"); - clock_box.style.display = "none"; - clock_box.style.position = "absolute"; + const clock_box = document.createElement("dialog"); clock_box.className = "clockbox module"; clock_box.id = DateTimeShortcuts.clockDivName + num; clock_box.setAttribute("role", "dialog"); @@ -239,7 +237,7 @@ }); document.addEventListener("keyup", function (event) { - if (event.which === 27) { + if (event.key === "Escape") { // ESC key closes popup DateTimeShortcuts.dismissClock(num); event.preventDefault(); @@ -261,25 +259,17 @@ } else { // since style's width is in em, it'd be tough to calculate // px value of it. let's use an estimated px for now - clock_box.style.left = findPosX(clock_link) - 110 + "px"; + clock_box.style.right = findPosX(clock_link) - 110 + "px"; } clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + "px"; // Show the clock box - clock_box.style.display = "block"; - document.addEventListener( - "click", - DateTimeShortcuts.dismissClockFunc[num], - ); + clock_box.showModal(); }, dismissClock: function (num) { - document.getElementById( - DateTimeShortcuts.clockDivName + num, - ).style.display = "none"; - document.removeEventListener( - "click", - DateTimeShortcuts.dismissClockFunc[num], - ); + document + .getElementById(DateTimeShortcuts.clockDivName + num) + .close(); }, handleClockQuicklink: function (num, val) { let d; @@ -333,8 +323,8 @@ e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, 0); }); - const cal_link = document.createElement("a"); - cal_link.href = "#"; + const cal_link = document.createElement("button"); + cal_link.type = "button"; cal_link.id = DateTimeShortcuts.calendarLinkName + num; cal_link.addEventListener("click", function (e) { e.preventDefault(); @@ -367,7 +357,7 @@ // // Markup looks like: // - // <div id="calendarbox3" class="calendarbox module" + // <dialog id="calendarbox3" class="calendarbox module" // role="dialog" aria-label="Choose a Date"> // <div> // <a href="#" class="calendarnav-previous" @@ -391,10 +381,8 @@ // <p class="calendar-cancel"> // <a href="#" role="button" aria-label="Close Calendar">Cancel</a> // </p> - // </div> - const cal_box = document.createElement("div"); - cal_box.style.display = "none"; - cal_box.style.position = "absolute"; + // </dialog> + const cal_box = document.createElement("dialog"); cal_box.className = "calendarbox module"; cal_box.id = DateTimeShortcuts.calendarDivName1 + num; cal_box.setAttribute("role", "dialog"); @@ -403,7 +391,46 @@ cal_box.addEventListener("click", function (e) { e.stopPropagation(); }); + // Handle arrow key navigation within the calendar + cal_box.addEventListener("keydown", function (event) { + const focused = cal_box.querySelector("a:focus"); + if (!focused) return; + const cells = Array.from(cal_box.querySelectorAll("td a")); + const currentIndex = cells.indexOf(focused); + if (currentIndex === -1) return; + const ltr = + window.getComputedStyle(document.body).direction !== "rtl"; + const forward = ltr + ? event.key === "ArrowRight" + : event.key === "ArrowLeft"; + const backward = ltr + ? event.key === "ArrowLeft" + : event.key === "ArrowRight"; + let nextCell = null; + const cellsPerRow = 7; + + if (forward && currentIndex < cells.length - 1) { + nextCell = cells[currentIndex + 1]; + } else if (backward && currentIndex > 0) { + nextCell = cells[currentIndex - 1]; + } else if ( + event.key === "ArrowDown" && + currentIndex + cellsPerRow < cells.length + ) { + nextCell = cells[currentIndex + cellsPerRow]; + } else if ( + event.key === "ArrowUp" && + currentIndex - cellsPerRow >= 0 + ) { + nextCell = cells[currentIndex - cellsPerRow]; + } + + if (nextCell) { + event.preventDefault(); + nextCell.focus(); + } + }); // next-prev links const cal_nav = quickElement("div", cal_box); const cal_nav_prev = quickElement("a", cal_nav, "<", "href", "#"); @@ -521,7 +548,7 @@ DateTimeShortcuts.dismissCalendar(num); }); document.addEventListener("keyup", function (event) { - if (event.which === 27) { + if (event.key === "Escape") { // ESC key closes popup DateTimeShortcuts.dismissCalendar(num); event.preventDefault(); @@ -589,32 +616,35 @@ } } - // Recalculate the clockbox position + // Recalculate the calendarbox position // is it left-to-right or right-to-left layout ? if (window.getComputedStyle(document.body).direction !== "rtl") { cal_box.style.left = findPosX(cal_link) + 17 + "px"; } else { // since style's width is in em, it'd be tough to calculate // px value of it. let's use an estimated px for now - cal_box.style.left = findPosX(cal_link) - 180 + "px"; + cal_box.style.right = findPosX(cal_link) - 180 + "px"; } cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + "px"; - cal_box.style.display = "block"; + cal_box.showModal(); DateTimeShortcuts.updateNavAriaLabels(num); - document.addEventListener( - "click", - DateTimeShortcuts.dismissCalendarFunc[num], + const calendarDiv = cal_box.querySelector( + "#" + DateTimeShortcuts.calendarDivName2 + num, ); + if (calendarDiv) { + // Focus on selected date, today, or first available date + const focusElement = + cal_box.querySelector("td.selected a") || + cal_box.querySelector("td.today a") || + cal_box.querySelector("td a"); + focusElement?.focus(); + } }, dismissCalendar: function (num) { - document.getElementById( - DateTimeShortcuts.calendarDivName1 + num, - ).style.display = "none"; - document.removeEventListener( - "click", - DateTimeShortcuts.dismissCalendarFunc[num], - ); + document + .getElementById(DateTimeShortcuts.calendarDivName1 + num) + .close(); }, drawPrev: function (num) { DateTimeShortcuts.calendars[num].drawPreviousMonth(); diff --git a/js_tests/admin/DateTimeShortcuts.test.js b/js_tests/admin/DateTimeShortcuts.test.js index fae84b72c2..865aed2e68 100644 --- a/js_tests/admin/DateTimeShortcuts.test.js +++ b/js_tests/admin/DateTimeShortcuts.test.js @@ -24,7 +24,7 @@ QUnit.test("init", function (assert) { const shortcuts = $(".datetimeshortcuts"); assert.equal(shortcuts.length, 1); assert.equal(shortcuts.find("a:first").text(), "Today"); - assert.equal(shortcuts.find("a:last .date-icon").length, 1); + assert.equal(shortcuts.find("button:last .date-icon").length, 1); // To prevent incorrect timezone warnings on date/time widgets, timezoneOffset // should be 0 when a timezone offset isn't set in the HTML body attribute. diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index b1db728f67..a607f1fedc 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1203,6 +1203,22 @@ class DateTimePickerSeleniumTests(AdminWidgetSeleniumTestCase): # The right month and year are displayed. self.wait_for_text("#calendarin0 caption", expected_caption) + def test_calendar_press_enter_focus_element(self): + from selenium.webdriver.common.by import By + from selenium.webdriver.common.keys import Keys + + self.admin_login(username="super", password="secret", login_url="/") + self.selenium.get( + self.live_server_url + reverse("admin:admin_widgets_member_add") + ) + icon = self.selenium.find_element(By.ID, "calendarlink0") + expected_focus_element = self.selenium.find_element( + By.CSS_SELECTOR, "div#calendarin0 table td.today a" + ) + icon.send_keys(Keys.ENTER) + focused_element = self.selenium.switch_to.active_element + self.assertEqual(expected_focus_element, focused_element) + @override_settings(TIME_ZONE="Asia/Seoul") def test_timezone_warning_message(self): from selenium.webdriver.common.by import By |
