Skip to content
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

#3140 - Maintain focus after filter selection #3533

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 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,41 @@ 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", "-1");
focusedElement.focus({ preventScroll: true });

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

// 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
});
});

// Clear focus selection in local storage if user is truly leaving the page
window.addEventListener("beforeunload", function(event) {
if (!clickedFilter) {
// If the user did not click a filter and the page is refreshing,
// clear the filter focus id from local storage
localStorage.removeItem("admin_filter_focus_id");
}
});
});
}
20 changes: 20 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 @@ -22,3 +22,23 @@ export function addOrRemoveSessionBoolean(name, add){
sessionStorage.removeItem(name);
}
}

/**
* 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.style.position = "absolute";
liveRegion.style.left = "-9999px";
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 @@ -31,6 +32,7 @@ initRejectedEmail();
initApprovedDomain();
initCopyRequestSummary();
initDynamicDomainRequestFields();
initFilterFocusListeners();

// Domain
initDomainFormTargetBlankButtons();
Expand Down
12 changes: 12 additions & 0 deletions src/registrar/templates/admin/filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% 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 @@ -7,25 +7,24 @@ <h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktr
{% 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 %}">
<li{% if choice.selected %} class="selected"{% endif %}>
{% if choice.selected and choice.exclude_query_string %}
<a class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" 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 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 }}" 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 Down