Skip to content

Commit

Permalink
Merge pull request #3533 from cisagov/nl/3140-maintain-filter-focus
Browse files Browse the repository at this point in the history
#3140 - Maintain focus after filter selection [-NL]
  • Loading branch information
CocoByte authored Mar 4, 2025
2 parents db6712a + ea0f0ec commit a16bdc0
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 22 deletions.
32 changes: 31 additions & 1 deletion src/registrar/assets/src/js/getgov-admin/domain-request-form.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hideElement, showElement, addOrRemoveSessionBoolean } from './helpers-admin.js';
import { hideElement, showElement, addOrRemoveSessionBoolean, announceForScreenReaders } from './helpers-admin.js';
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';

function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){
Expand Down Expand Up @@ -684,3 +684,33 @@ export function initDynamicDomainRequestFields(){
handleSuborgFieldsAndButtons();
}
}

export function initFilterFocusListeners() {
document.addEventListener("DOMContentLoaded", function() {
let filters = document.querySelectorAll("#changelist-filter li a"); // Get list of all filter links
let clickedFilter = false; // Used to determine if we are truly navigating away or not

// Restore focus from localStorage
let lastClickedFilterId = localStorage.getItem("admin_filter_focus_id");
if (lastClickedFilterId) {
let focusedElement = document.getElementById(lastClickedFilterId);
if (focusedElement) {
//Focus the element
focusedElement.setAttribute("tabindex", "0");
focusedElement.focus({ preventScroll: true });

// Announce focus change for screen readers
announceForScreenReaders("Filter refocused on " + focusedElement.textContent);
localStorage.removeItem("admin_filter_focus_id");
}
}

// Capture clicked filter and store its ID
filters.forEach(filter => {
filter.addEventListener("click", function() {
localStorage.setItem("admin_filter_focus_id", this.id);
clickedFilter = true; // Mark that a filter was clicked
});
});
});
}
19 changes: 19 additions & 0 deletions src/registrar/assets/src/js/getgov-admin/helpers-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,22 @@ export function getParameterByName(name, url) {
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}

/**
* Creates a temporary live region to announce messages for screen readers.
*/
export function announceForScreenReaders(message) {
let liveRegion = document.createElement("div");
liveRegion.setAttribute("aria-live", "assertive");
liveRegion.setAttribute("role", "alert");
liveRegion.setAttribute("class", "usa-sr-only");
document.body.appendChild(liveRegion);

// Delay the update slightly to ensure it's recognized
setTimeout(() => {
liveRegion.textContent = message;
setTimeout(() => {
document.body.removeChild(liveRegion);
}, 1000);
}, 100);
}
4 changes: 3 additions & 1 deletion src/registrar/assets/src/js/getgov-admin/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
initRejectedEmail,
initApprovedDomain,
initCopyRequestSummary,
initDynamicDomainRequestFields } from './domain-request-form.js';
initDynamicDomainRequestFields,
initFilterFocusListeners } from './domain-request-form.js';
import { initDomainFormTargetBlankButtons } from './domain-form.js';
import { initDynamicPortfolioFields } from './portfolio-form.js';
import { initDynamicDomainInformationFields } from './domain-information-form.js';
Expand All @@ -34,6 +35,7 @@ initRejectedEmail();
initApprovedDomain();
initCopyRequestSummary();
initDynamicDomainRequestFields();
initFilterFocusListeners();

// Domain
initDomainFormTargetBlankButtons();
Expand Down
13 changes: 13 additions & 0 deletions src/registrar/templates/admin/filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% comment %} Override of this file: https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/filter.html {% endcomment %}
{% load i18n %}
<details data-filter-title="{{ title }}" open>
<summary>
{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}
</summary>
<ul>
{% for choice in choices %}
<li {% if choice.selected %} class="selected"{% endif %}>
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
{% endfor %}
</ul>
</details>
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,21 @@
{% for choice in choices %}
{% if choice.reset %}
<li{% if choice.selected %} class="selected"{% endif %}">
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
</li>
{% endif %}
{% endfor %}

{% for choice in choices %}
{% if not choice.reset %}
<li{% if choice.selected %} class="selected"{% endif %}">
{% else %}
<li{% if choice.selected %} class="selected"{% endif %}>
{% if choice.selected and choice.exclude_query_string %}
<a role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
</svg>
<svg class="usa-icon position-absolute z-100 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
</svg>
</a>
{% endif %}
{% if not choice.selected and choice.include_query_string %}
<a role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
{% elif not choice.selected and choice.include_query_string %}
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
</svg>
Expand All @@ -38,4 +33,4 @@
{% endif %}
{% endfor %}
</ul>
</details>
</details>
16 changes: 8 additions & 8 deletions src/registrar/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,14 @@ def test_get_filters(self):
)

# Assert that the filters are added
self.assertContains(response, "invited", count=5)
self.assertContains(response, "invited", count=6)
self.assertContains(response, "Invited", count=2)
self.assertContains(response, "retrieved", count=2)
self.assertContains(response, "retrieved", count=3)
self.assertContains(response, "Retrieved", count=2)

# Check for the HTML context specificially
invited_html = '<a href="?status__exact=invited">Invited</a>'
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>'
invited_html = '<a id="status-filter-invited" href="?status__exact=invited">Invited</a>'
retrieved_html = '<a id="status-filter-retrieved" href="?status__exact=retrieved">Retrieved</a>'

self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_html, count=1)
Expand Down Expand Up @@ -1269,14 +1269,14 @@ def test_get_filters(self):
)

# Assert that the filters are added
self.assertContains(response, "invited", count=4)
self.assertContains(response, "invited", count=5)
self.assertContains(response, "Invited", count=2)
self.assertContains(response, "retrieved", count=2)
self.assertContains(response, "retrieved", count=3)
self.assertContains(response, "Retrieved", count=2)

# Check for the HTML context specificially
invited_html = '<a href="?status__exact=invited">Invited</a>'
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>'
invited_html = '<a id="status-filter-invited" href="?status__exact=invited">Invited</a>'
retrieved_html = '<a id="status-filter-retrieved" href="?status__exact=retrieved">Retrieved</a>'

self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_html, count=1)
Expand Down

0 comments on commit a16bdc0

Please sign in to comment.