Skip to content

Fixed #36458 Move focus to admin widget popup window after button click #19632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

XChaitanyaX
Copy link

@XChaitanyaX XChaitanyaX commented Jul 10, 2025

Trac ticket number

ticket-36458

Branch description

The proposed change ensures that when a popup widget is triggered, keyboard focus is programmatically moved to the desired element (current date for the date picker and the 'Now' element in the time picker) within the popup.

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! Thank you for your contribution 💪

As it's your first contribution be sure to check out the patch review checklist.

If you're fixing a ticket from Trac make sure to set the "Has patch" flag and include a link to this PR in the ticket!

If you have any design or process questions then you can ask in the Django forum.

Welcome aboard ⛵️!

Copy link
Contributor

@Antoliny0919 Antoliny0919 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for picking up this ticket @XChaitanyaX ⭐️

I've added a couple of comments inline below.

@@ -179,6 +181,36 @@
DateTimeShortcuts.dismissClock(num);
});

// Handle tab navigation within the popup (after content is created)
clock_box.addEventListener('keydown', function(event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes added are great, but it doesn't seem like screen readers were taken into consideration.
When using a screen reader, users can navigate specific elements using the Tab key.
The internal elements of the dialogs provided by the admin (such as Date and Time pickers) are implemented as <a> tags, so they are navigable using the Tab key while a screen reader is active.
The current code added seems to be designed for navigation with a regular keyboard.

Comment on lines 473 to 475
focusNextElement: function(currentElement) {
// Get all tabbable elements in the document, excluding hidden popup elements
const allTabbableElements = document.querySelectorAll(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think most of the changes are designed with screen readers in mind.
This is the same as my comment above.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing, previously the Tab key is not even working as intended to (the users can still navigate but in a very awkward way i.e. to go through all the elements and then the time and date elements are focused which is annoying for the specific types of users) as the popup elements are not even focused when entered. And the Tab is now working (but not focusing to the current date or time as you mentioned below). Thank you for the comments, I will try to fix them.

@Antoliny0919
Copy link
Contributor

Antoliny0919 commented Jul 11, 2025

The ticket you picked aims to improve accessibility for screen readers.

The goals of this PR are as follows.

When a user using a screen reader clicks on the datepicker or timepicker provided by the admin, focus should be immediately set to the dialog that appears.

Where should the focus be set?

In my opinion, since the Date and Time elements represent time, it would be best if the initial focus (the first element accessed) is set to the element that indicates the current time. This is just my personal view, but both W3C and the <input type="date"> element set the focus directly to the element representing the current time when accessed with a screen reader.

  • Date

The element to be focused when pressing enter in a screen reader...

date_screenreader_example
  • Time

The element to be focused when pressing enter in a screen reader...

time_screenreader_example

@XChaitanyaX
Copy link
Author

popup.fix.mp4

The video shows a small lag (which actually is not there when I tested it) when the clock-box is clicked (also at the calendar-box focusing the current date) to focus the Now element.
Now the elements are properly focused.

@XChaitanyaX XChaitanyaX requested a review from Antoliny0919 July 11, 2025 14:43
@Antoliny0919
Copy link
Contributor

Antoliny0919 commented Jul 12, 2025

@XChaitanyaX
As mentioned in a previous comment, this change is part of an accessibility improvement for users who rely on screen readers.

In other words, as it's currently implemented, it's not about enabling navigation to a specific area using the Tab key without a screen reader.
It must be accessible specifically when a screen reader is being used.

Screenshot 2025-07-12 at 9 31 42 PM

When using a screen reader, users can navigate between elements using the Tab key or reader specific navigation commands, even without any special implementation on our side.
(That’s why the tab navigation you’ve implemented is actually already possible with a screen reader, even without additional code.)

The issue we need to address is ensuring that, when using a screen reader, pressing Enter on the date picker or time picker opens the dialog and allows immediate access to specific elements within it (as I pointed out in a previous comment).
Currently, it is only possible to access after navigating through all the elements.

++ If focus is set to a specific element, screen readers can immediately access that element.

@XChaitanyaX
Copy link
Author

screen.reader.fix.mp4
  • I am using Narrator (a screen reader) in windows
  • Is this good or somethings still need change

Copy link
Contributor

@Antoliny0919 Antoliny0919 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @XChaitanyaX .

I worked on the date picker separately.

index 8168172a97..73802c26b9 100644
--- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
+++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
@@ -245,11 +245,13 @@
             const cal_link = document.createElement('a');
             cal_link.href = '#';
             cal_link.id = DateTimeShortcuts.calendarLinkName + num;
-            cal_link.addEventListener('click', function(e) {
-                e.preventDefault();
-                // avoid triggering the document click handler to dismiss the calendar
-                e.stopPropagation();
-                DateTimeShortcuts.openCalendar(num);
+            ['click', 'keydown'].forEach((event) => {
+                cal_link.addEventListener(event, function(e) {
+                    e.preventDefault();
+                    // avoid triggering the document click handler to dismiss the calendar
+                    e.stopPropagation();
+                    DateTimeShortcuts.openCalendar(num, e);
+                });
             });
             quickElement(
                 'span', cal_link, '',
@@ -345,7 +347,7 @@
                 }
             });
         },
-        openCalendar: function(num) {
+        openCalendar: function(num, e) {
             const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num);
             const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num);
             const inp = DateTimeShortcuts.calendarInputs[num];
@@ -376,6 +378,11 @@
             cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px';
 
             cal_box.style.display = 'block';
+            if (e.key === 'Enter') {
+                const selected = cal_box.querySelector('td.selected a');
+                const focusElement = selected ? selected : cal_box.querySelector('td.today a');
+                focusElement.focus();
+            }
             document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
         },
         dismissCalendar: function(num) {

When a screen reader user accesses the date picker element, the above code alone is sufficient to focus on a specific element (the selected date if one exists, or today if not), allowing them to freely navigate the dialogs.

I think this change is sufficient for improving the accessibility of the date picker in this ticket for now.

Comment on lines 159 to 163
clock_box.tabIndex = -1; // Make focusable but not in tab order
// Add ARIA attributes for better screen reader support
clock_box.setAttribute('role', 'dialog');
clock_box.setAttribute('aria-label', gettext('Choose a time'));
clock_box.setAttribute('aria-modal', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using aria attributes on dialog buttons is a good idea, but it seems like something that should be handled in a separate ticket from this one.

There is currently no ticket for improving the accessibility of the clock box, but there is one for the date picker.
https://code.djangoproject.com/ticket/36459

Copy link
Author

@XChaitanyaX XChaitanyaX Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aria attributes:

  • I think aria attributes are necessary and maybe some discussion is needed on it.
  • Also, in my opinion it might be good if we resolve all the aria attributes for both time and date widget in the same place, rather than 2 separate tickets

And I am removing the aria attributes as we are having a separate ticket.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the aria attributes should be added. However, as you mentioned, this should be handled separately from this ticket.
Since the two widgets have a similar structure, I also think it might be better to handle them together for the sake of consistency, but hmm... I guess that’s something to think about later. 😄

Comment on lines 170 to 171
time_list.setAttribute('role', 'list');
time_list.setAttribute('aria-label', gettext('Time options'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment above, this applies to all other changes involving the addition of aria attributes as well.

@XChaitanyaX
Copy link
Author

XChaitanyaX commented Jul 16, 2025

Changes done

  • removed aria attributes.
  • removed accessibility via keyboard where arrow keys are used to navigate in both the date and time pickers as this change is irrelevant to this ticket
  • Use the selected element class to efficiently find the currently selected date

@thibaudcolas thibaudcolas requested a review from a team July 19, 2025 05:10
@thibaudcolas
Copy link
Member

Static demo site if anyone wants to test this: Change session - static demo

@Antoliny0919
Copy link
Contributor

@XChaitanyaX
If you’d like to have your work reviewed(fellow), please remove the "patch needs improvement" and "needs tests" flags from the ticket.
Feel free to mention me, and I’ll be happy to review it again.

@XChaitanyaX XChaitanyaX requested a review from Antoliny0919 July 19, 2025 06:10
@Antoliny0919
Copy link
Contributor

++ When the picker is activated by pressing Enter without using a screen reader, the user still has to navigate through all the elements, just like a screen reader user would. However, I believe this improvement will address that issue for both cases.

@XChaitanyaX
Copy link
Author

++ When the picker is activated by pressing Enter without using a screen reader, the user still has to navigate through all the elements, just like a screen reader user would. However, I believe this improvement will address that issue for both cases.

Yes, it will fix for both the cases

  • The accessibility can be improved even more if we add arrow keys to navigate elements in the pickers (it would be a great improvement in the date picker).
  • if the current date is 1st and the user need to select the last date of the month, pressing the Tab key 30 times is annoying, if we have arrow key navigation in the date picker it would be a lot easier.

@Antoliny0919
Copy link
Contributor

Yes, it will fix for both the cases

* The accessibility can be improved even more if we add arrow keys to navigate elements in the pickers (it would be a great improvement in the date picker).

* if the current date is 1st and the user need to select the last date of the month, pressing the Tab key 30 times is annoying, if we have arrow key navigation in the date picker it would be a lot easier.

Yes, I think so too. The W3C example date picker also provides this functionality.
However, this feature is also something that should be addressed in this ticket.

Copy link
Contributor

@sarahboyce sarahboyce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR @XChaitanyaX

Note that we will need tests for the changes, this could either be a selenium test or a javascript test in js_tests/admin/DateTimeShortcuts.test.js

I have tested and the focus does go in the popup window of the datepicker, however:

  • when I have tabbed through the options, I should loop within the datepicker. Instead I then tab through the top of the page
  • when I hit "esc" I should be focused on the trigger button (and can continue to tab from their) but instead I am at the top of the page

See: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/#keyboardinteraction

Comment on lines -128 to +130
'class', 'clock-icon',
'title', gettext('Choose a Time')
'class', 'clock-icon'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that if possible, only include changes that are relevant for the ticket
Other fixes should be included with other associated tickets (in a separate commit) 👍

@XChaitanyaX
Copy link
Author

XChaitanyaX commented Jul 22, 2025

@Antoliny0919
Any idea on how to write tests for the changes??

I wrote something like this and it failed

QUnit.test('calendar focus loop on tab navigation', function(assert) {
    const $ = django.jQuery;
    const dateField = $('<input type="text" class="vDateField" value="2015-03-16">');
    $('#qunit-fixture').append(dateField);

    DateTimeShortcuts.init();
    DateTimeShortcuts.openCalendar(0);

    const calendarDiv = $('#calendarbox0');
    assert.ok(calendarDiv.is(':visible'), 'Calendar is visible');

    const focusable = calendarDiv
        .find('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
        .filter(':visible');
    const firstEl = focusable.first()[0];
    const lastEl = focusable.last()[0];

    // Move focus to the last element
    lastEl.focus();
    assert.strictEqual(
        document.activeElement,
        lastEl,
        'Last element is focused'
    );

    $(lastEl).trigger($.Event('keydown', { which: 9 }));

    assert.strictEqual(
        document.activeElement,
        firstEl,
        'After tabbing from last element focus loops back to first'
    );

    DateTimeShortcuts.dismissCalendar(0);
});

Died on test 1: Cannot read properties of null (reading 'offsetParent')
-> happened when calling DateTimeShortcuts.openCalendar(0);

@XChaitanyaX
Copy link
Author

  • when I have tabbed through the options, I should loop within the datepicker. Instead I then tab through the top of the page
  • when I hit "esc" I should be focused on the trigger button (and can continue to tab from their) but instead I am at the top of the page

@sarahboyce

  • I have made changes and now it should work as you said.
  • But I need help with writing tests.

@Antoliny0919
Copy link
Contributor

@Antoliny0919 Any idea on how to write tests for the changes??

I wrote something like this and it failed

QUnit.test('calendar focus loop on tab navigation', function(assert) {
    const $ = django.jQuery;
    const dateField = $('<input type="text" class="vDateField" value="2015-03-16">');
    $('#qunit-fixture').append(dateField);

    DateTimeShortcuts.init();
    DateTimeShortcuts.openCalendar(0);

    const calendarDiv = $('#calendarbox0');
    assert.ok(calendarDiv.is(':visible'), 'Calendar is visible');

    const focusable = calendarDiv
        .find('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
        .filter(':visible');
    const firstEl = focusable.first()[0];
    const lastEl = focusable.last()[0];

    // Move focus to the last element
    lastEl.focus();
    assert.strictEqual(
        document.activeElement,
        lastEl,
        'Last element is focused'
    );

    $(lastEl).trigger($.Event('keydown', { which: 9 }));

    assert.strictEqual(
        document.activeElement,
        firstEl,
        'After tabbing from last element focus loops back to first'
    );

    DateTimeShortcuts.dismissCalendar(0);
});

Died on test 1: Cannot read properties of null (reading 'offsetParent') -> happened when calling DateTimeShortcuts.openCalendar(0);

Test looks good.
However, I personally prefer Selenium tests.
I think they will be easier to maintain in the long run.
I wrote a test that checks which element receives focus when the date picker is triggered with the Enter key.
Other tests could probably be written in a similar format.

index c47e0e3ec1..3a9ac3f9d9 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -1176,6 +1176,23 @@ 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)
+

@Antoliny0919
Copy link
Contributor

Antoliny0919 commented Jul 24, 2025

Thank you for the PR @XChaitanyaX

Note that we will need tests for the changes, this could either be a selenium test or a javascript test in js_tests/admin/DateTimeShortcuts.test.js

I have tested and the focus does go in the popup window of the datepicker, however:

* when I have tabbed through the options, I should loop within the datepicker. Instead I then tab through the top of the page

* when I hit "esc" I should be focused on the trigger button (and can continue to tab from their) but instead I am at the top of the page

See: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/#keyboardinteraction

Should the tab navigation within the datepicker also be part of this work?
I thought handling tab navigation inside the dialog should be addressed in the DatePicker accessibility improvement ticket.
But if it is meant to be included here, I’ll make sure to review the focus navigation as well
I’ll follow your opinion 🥰

@sarahboyce
Copy link
Contributor

Should the tab navigation within the datepicker also be part of this work?

The same is true of the clock widget. I think this ticket should ideally be scoped for keyboard navigation of widget popups 👍

@Antoliny0919
Copy link
Contributor

So, the goals we should aim to achieve in this PR are as follows:

  • Assign focus to the appropriate element when pressing Enter on the DateTime picker icon.
    As mentioned before, for the Date field, if no date is selected, the focus should default to today’s date. If a date is selected, the focus should move to that date.
    For the Time field, focusing on “now” seems to be the most appropriate behavior.

  • Enable keyboard navigation within the DateTime dialog.
    Similar to the W3C example for date pickers, users should be able to navigate between day elements using the arrow keys.
    This is important because the focus should not be limited to just date selection, users must also be able to access other interactive elements like "Yesterday," "Tomorrow," "Close," etc.
    Additionally, as Sarah suggested, we need to consider what happens when the user reaches the last focusable element.
    In the W3C example, focus moves to the next or previous month in such cases.

Thinking about it, implementing all of these features would likely require a significant amount of Javascript and test coverage.
As Sarah previously suggested, switching to a native input type="date" element instead of maintaining the current widget could reduce the maintenance burden, and at the same time, naturally improve accessibility.
That makes suggestion feel even more appealing.

I believe we need a clear direction for what we want to achieve in this PR.
In my opinion, we should discuss this further with the accessibility team.

@sarahboyce
Copy link
Contributor

I'm happy for the accessibility team to come up with a plan for the widgets
Currently as I can tab through the options in the widget (rather than arrow through), I'm less concerned about this as a "blocker"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress / Review
Development

Successfully merging this pull request may close these issues.

4 participants