From 1b255366afd42dd4b35a132695096f93a6846dfa Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Feb 2025 14:31:40 -0700 Subject: [PATCH 1/5] First draft --- .../js/getgov-admin/domain-request-form.js | 41 ++++++++++++++++++- .../src/js/getgov-admin/helpers-admin.js | 20 +++++++++ .../assets/src/js/getgov-admin/main.js | 4 +- .../templates/admin/change_list.html | 15 +++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index b3d14839e..9352dd6b6 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -1,4 +1,4 @@ -import { hideElement, showElement, addOrRemoveSessionBoolean } from './helpers-admin.js'; +import { hideElement, showElement, addOrRemoveSessionBoolea, announceForScreenReaders } from './helpers-admin.js'; import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js'; function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){ @@ -684,3 +684,42 @@ 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 if it exists + let lastClickedFilter = localStorage.getItem("admin_filter_focus"); + if (lastClickedFilter) { + let focusedElement = document.querySelector(`#changelist-filter li a[href='${lastClickedFilter}']`); + 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); + } + } + + // Setup listeners. When a filter is clicked, save it as the filter to focus after page refresh + filters.forEach(filter => { + filter.addEventListener("click", function() { + // Save this filter in local storage so we can focus it after page refresh + localStorage.setItem("admin_filter_focus", this.getAttribute("href")); + + // Mark that a filter was clicked so that our beforeunload listener doesn't clear the local storage + clickedFilter = true; + }); + }); + + // Clear focus selection in local storage if user is truly leaving the page + window.addEventListener("beforeunload", function(event) { + if (!clickedFilter) { + localStorage.removeItem("admin_filter_focus"); + } + }); + }); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov-admin/helpers-admin.js b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js index ff618a67d..de264f34f 100644 --- a/src/registrar/assets/src/js/getgov-admin/helpers-admin.js +++ b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js @@ -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); +} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js index 64be572b2..22266a2c8 100644 --- a/src/registrar/assets/src/js/getgov-admin/main.js +++ b/src/registrar/assets/src/js/getgov-admin/main.js @@ -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'; @@ -31,6 +32,7 @@ initRejectedEmail(); initApprovedDomain(); initCopyRequestSummary(); initDynamicDomainRequestFields(); +initFilterFocusListeners(); // Domain initDomainFormTargetBlankButtons(); diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index 43abf0861..806067025 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -39,6 +39,21 @@

{% endblock %} +{% for spec in cl.filter_specs %} +
+

{{ spec.title }}

+ +
+{% endfor %} + {% comment %} Replace the Django ul markup with a div. We'll replace the li with a p in change_list_object_tools {% endcomment %} {% block object-tools %}
From dac0841a0105cb8945f551c9fd13fc115e50f45e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Feb 2025 15:21:58 -0700 Subject: [PATCH 2/5] Add filter IDs and target those for focus --- .../js/getgov-admin/domain-request-form.js | 29 +++++++++---------- src/registrar/templates/admin/filter.html | 12 ++++++++ .../admin/multiple_choice_list_filter.html | 6 ++-- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/registrar/templates/admin/filter.html diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index 9352dd6b6..50fcbf0b1 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -690,35 +690,34 @@ export function initFilterFocusListeners() { 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 if it exists - let lastClickedFilter = localStorage.getItem("admin_filter_focus"); - if (lastClickedFilter) { - let focusedElement = document.querySelector(`#changelist-filter li a[href='${lastClickedFilter}']`); + // Restore focus from localStorage + let lastClickedFilterId = localStorage.getItem("admin_filter_focus_id"); + if (lastClickedFilterId) { + let focusedElement = document.getElementById(lastClickedFilterId); if (focusedElement) { - // Focus the element + //Focus the element focusedElement.setAttribute("tabindex", "-1"); focusedElement.focus({ preventScroll: true }); - + // Announce focus change for screen readers announceForScreenReaders("Filter refocused on " + focusedElement.textContent); } } - - // Setup listeners. When a filter is clicked, save it as the filter to focus after page refresh + + // Capture clicked filter and store its ID filters.forEach(filter => { filter.addEventListener("click", function() { - // Save this filter in local storage so we can focus it after page refresh - localStorage.setItem("admin_filter_focus", this.getAttribute("href")); - - // Mark that a filter was clicked so that our beforeunload listener doesn't clear the local storage - clickedFilter = true; + 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) { - localStorage.removeItem("admin_filter_focus"); + // 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"); } }); }); diff --git a/src/registrar/templates/admin/filter.html b/src/registrar/templates/admin/filter.html new file mode 100644 index 000000000..035d4ad30 --- /dev/null +++ b/src/registrar/templates/admin/filter.html @@ -0,0 +1,12 @@ +{% load i18n %} +
+ + {% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %} + + +
\ No newline at end of file diff --git a/src/registrar/templates/django/admin/multiple_choice_list_filter.html b/src/registrar/templates/django/admin/multiple_choice_list_filter.html index 167059594..7ae2f2bad 100644 --- a/src/registrar/templates/django/admin/multiple_choice_list_filter.html +++ b/src/registrar/templates/django/admin/multiple_choice_list_filter.html @@ -7,7 +7,7 @@

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktr {% for choice in choices %} {% if choice.reset %} - {{ choice.display }} + {{ choice.display }} {% endif %} {% endfor %} @@ -15,7 +15,7 @@

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktr {% if not choice.reset %} {% if choice.selected and choice.exclude_query_string %} - {{ choice.display }} + {{ choice.display }} @@ -25,7 +25,7 @@

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktr {% endif %} {% if not choice.selected and choice.include_query_string %} - {{ choice.display }} + {{ choice.display }} From 98f4fe773269d37bd00bf7438dfc2472fa03e31f Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Feb 2025 15:35:47 -0700 Subject: [PATCH 3/5] fix typo --- src/registrar/assets/src/js/getgov-admin/domain-request-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index 50fcbf0b1..34c6a42f3 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -1,4 +1,4 @@ -import { hideElement, showElement, addOrRemoveSessionBoolea, announceForScreenReaders } 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){ From bbcabbe1b06cb528936c64e7d98cbf558c567677 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Feb 2025 15:37:17 -0700 Subject: [PATCH 4/5] remove experiment --- src/registrar/templates/admin/change_list.html | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html index 806067025..43abf0861 100644 --- a/src/registrar/templates/admin/change_list.html +++ b/src/registrar/templates/admin/change_list.html @@ -39,21 +39,6 @@

{% endblock %} -{% for spec in cl.filter_specs %} -
-{% endfor %} - {% comment %} Replace the Django ul markup with a div. We'll replace the li with a p in change_list_object_tools {% endcomment %} {% block object-tools %}
From 0a0b9c04906253f0a494682963c9bade5448d68c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 19 Feb 2025 12:50:12 -0800 Subject: [PATCH 5/5] Fixed multi-list bug --- .../templates/django/admin/multiple_choice_list_filter.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/django/admin/multiple_choice_list_filter.html b/src/registrar/templates/django/admin/multiple_choice_list_filter.html index 7ae2f2bad..4fb70565e 100644 --- a/src/registrar/templates/django/admin/multiple_choice_list_filter.html +++ b/src/registrar/templates/django/admin/multiple_choice_list_filter.html @@ -13,7 +13,7 @@

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktr {% endfor %} {% for choice in choices %} {% if not choice.reset %} - + {% if choice.selected and choice.exclude_query_string %} {{ choice.display }}

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktr - {% endif %} - {% if not choice.selected and choice.include_query_string %} + {% elif not choice.selected and choice.include_query_string %} {{ choice.display }}