summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSkyiesac <jainsachi1202@gmail.com>2026-03-31 18:18:18 +0530
committerJacob Walls <jacobtylerwalls@gmail.com>2026-05-20 12:02:15 -0400
commit3ca621a38642bfd8fc2bfd308f489cc2d9e76fb0 (patch)
tree89fe0c429473ce10f6181fedd42e88bdbb530e4f
parent4ea7648df5f970593e3cbe222a19fb624fa7208c (diff)
Fixed #36458 -- Trapped focus in the admin calendar and clock widgets.
-rw-r--r--django/contrib/admin/static/admin/css/responsive.css12
-rw-r--r--django/contrib/admin/static/admin/css/widgets.css25
-rw-r--r--django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js114
-rw-r--r--js_tests/admin/DateTimeShortcuts.test.js2
-rw-r--r--tests/admin_widgets/tests.py16
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