Merge pull request #3545 from cisagov/za/3526-fix-link-accessibility-issues

#3526: Django admin - Fix link and button accessibility issues - [HOTGOV]
This commit is contained in:
zandercymatics 2025-02-27 08:13:09 -07:00 committed by GitHub
commit 9bd3aad2b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 90 additions and 23 deletions

View file

@ -2289,11 +2289,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
@admin.display(description=_("Requested Domain"))
def custom_requested_domain(self, obj):
# Example: Show different icons based on `status`
url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}"
text = obj.requested_domain
if obj.portfolio:
return format_html('<a href="{}"><img src="/public/admin/img/icon-yes.svg"> {}</a>', url, text)
return format_html('<a href="{}">{}</a>', url, text)
return format_html(
f'<img class="padding-right-05" src="/public/admin/img/icon-yes.svg" aria-hidden="true">{escape(text)}'
)
return text
custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore

View file

@ -0,0 +1,15 @@
/**
* Initializes buttons to behave like links by navigating to their data-url attribute
* Example usage: <button class="use-button-as-link" data-url="/some/path">Click me</button>
*/
export function initButtonLinks() {
document.querySelectorAll('button.use-button-as-link').forEach(button => {
button.addEventListener('click', function() {
// Equivalent to button.getAttribute("data-href")
const href = this.dataset.href;
if (href) {
window.location.href = href;
}
});
});
}

View file

@ -16,6 +16,7 @@ import { initDynamicPortfolioFields } from './portfolio-form.js';
import { initDynamicDomainInformationFields } from './domain-information-form.js';
import { initDynamicDomainFields } from './domain-form.js';
import { initAnalyticsDashboard } from './analytics.js';
import { initButtonLinks } from './button-utils.js';
// General
initModals();
@ -23,6 +24,7 @@ initCopyToClipboard();
initFilterHorizontalWidget();
initDescriptions();
initSubmitBar();
initButtonLinks();
// Domain request
initIneligibleModal();

View file

@ -498,7 +498,7 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
font-size: 13px;
}
.object-tools li button {
.object-tools li button, button.addlink {
font-family: Source Sans Pro Web, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif;
text-transform: none !important;
font-size: 14px !important;
@ -520,6 +520,14 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
}
}
// Mimic the style for <a>
.object-tools > p > button.addlink {
background-image: url(../admin/img/tooltag-add.svg) !important;
background-repeat: no-repeat !important;
background-position: right 7px center !important;
padding-right: 25px;
}
.usa-modal--django-admin .usa-prose ul > li {
list-style-type: inherit;
// Styling based off of the <p> styling in django admin
@ -984,3 +992,7 @@ ul.add-list-reset {
}
}
#result_list > tbody tr > th > a {
text-decoration: underline;
}

View file

@ -7,10 +7,10 @@
{% if has_absolute_url %}
<ul>
<li>
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
<button data-href="{% add_preserved_filters history_url %}" class="historylink use-button-as-link">{% translate "History" %}</button>
</li>
<li>
<a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a>
<button data-href="{{ absolute_url }}" class="viewsitelink use-button-as-link">{% translate "View on site" %}</button>
</li>
</ul>
{% else %}
@ -30,18 +30,18 @@
{% endif %}
<li>
<a href="{% add_preserved_filters history_url %}">{% translate "History" %}</a>
<button data-href="{% add_preserved_filters history_url %}" class="historylink use-button-as-link">{% translate "History" %}</button>
</li>
{% if opts.model_name == 'domainrequest' %}
<li>
<a id="id-copy-to-clipboard-summary" class="usa-button--dja" type="button" href="#">
<button id="id-copy-to-clipboard-summary" class="usa-button--dja">
<svg class="usa-icon">
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<!-- the span is targeted in JS, do not remove -->
<span>{% translate "Copy request summary" %}</span>
</a>
</button>
</li>
{% endif %}
</ul>

View file

@ -5,9 +5,9 @@
{% if has_add_permission %}
<p class="margin-0 padding-0">
{% url cl.opts|admin_urlname:'add' as add_url %}
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
<button data-href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink use-button-as-link">
{% blocktranslate with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktranslate %}
</a>
</button>
</p>
{% endif %}
{% endblock %}

View file

@ -19,11 +19,11 @@ Load our custom filters to extract info from the django generated markup.
{% if results.0|contains_checkbox %}
{# .gov - hardcode the select all checkbox #}
<th scope="col" class="action-checkbox-column" title="Toggle all">
<th scope="col" class="action-checkbox-column" title="Toggle">
<div class="text">
<span>
<input type="checkbox" id="action-toggle">
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
<input type="checkbox" id="action-toggle">
</span>
</div>
<div class="clear"></div>
@ -34,9 +34,9 @@ Load our custom filters to extract info from the django generated markup.
{% if header.sortable %}
{% if header.sort_priority > 0 %}
<div class="sortoptions">
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
<a class="sortremove" href="{{ header.url_remove }}" aria-label="{{ header.text }}" title="{% translate "Remove from sorting" %}"></a>
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
<a href="{{ header.url_toggle }}" aria-label="{{ header.text }} sorting {% if header.ascending %}ascending{% else %}descending{% endif %}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
</div>
{% endif %}
{% endif %}
@ -61,10 +61,10 @@ Load our custom filters to extract info from the django generated markup.
{% endif %}
<tr>
{% with result_value=result.0|extract_value %}
{% with result_label=result.1|extract_a_text %}
{% with result_label=result.1|extract_a_text checkbox_id="select-"|add:result_value %}
<td>
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}" class="action-select">
<label class="usa-sr-only" for="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}">{{ result_label|default:'label' }}</label>
<label class="usa-sr-only" for="{{ checkbox_id }}">Select row {{ result_label|default:'label' }}</label>
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ checkbox_id }}" class="action-select">
</td>
{% endwith %}
{% endwith %}

View file

@ -0,0 +1,7 @@
{% load i18n %}
{% load admin_urls %}
{% if has_export_permission %}
{% comment %} Uses the initButtonLinks {% endcomment %}
<li><button class="export_link use-button-as-link" data-href="{% url opts|admin_urlname:"export" %}">{% trans "Export" %}</button></li>
{% endif %}

View file

@ -3,6 +3,6 @@
{% if has_import_permission %}
{% if not IS_PRODUCTION %}
<li><a href='{% url opts|admin_urlname:"import" %}' class="import_link">{% trans "Import" %}</a></li>
<li><button class="import_link use-button-as-link" data-href="{% url opts|admin_urlname:"import" %}">{% trans "Import" %}</button></li>
{% endif %}
{% endif %}

View file

@ -0,0 +1,26 @@
{% comment %} This is an override of the django search bar to add better accessibility compliance.
There are no blocks defined here, so we had to copy the code.
https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/search_form.html
{% endcomment %}
{% load i18n static %}
{% if cl.search_fields %}
<div id="toolbar"><form id="changelist-search" method="get" role="search">
<div><!-- DIV needed for valid HTML -->
{% comment %} .gov override - removed for="searchbar" {% endcomment %}
<label><img src="{% static "admin/img/search.svg" %}" alt="Search"></label>
<input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar"{% if cl.search_help_text %} aria-describedby="searchbar_helptext"{% endif %}>
<input type="submit" value="{% translate 'Search' %}">
{% if show_result_count %}
<span class="small quiet">{% blocktranslate count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktranslate %} (<a href="?{% if cl.is_popup %}{{ is_popup_var }}=1{% if cl.add_facets %}&{% endif %}{% endif %}{% if cl.add_facets %}{{ is_facets_var }}{% endif %}">{% if cl.show_full_result_count %}{% blocktranslate with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktranslate %}{% else %}{% translate "Show all" %}{% endif %}</a>)</span>
{% endif %}
{% for pair in cl.params.items %}
{% if pair.0 != search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ pair.1 }}">{% endif %}
{% endfor %}
</div>
{% if cl.search_help_text %}
<br class="clear">
{% comment %} .gov override - added for="searchbar" {% endcomment %}
<label class="help" id="searchbar_helptext" for="searchbar">{{ cl.search_help_text }}</label>
{% endif %}
</form></div>
{% endif %}

View file

@ -25,11 +25,15 @@ def extract_a_text(value):
pattern = r"<a\b[^>]*>(.*?)</a>"
match = re.search(pattern, value)
if match:
extracted_text = match.group(1)
else:
extracted_text = ""
# Get the content and strip any nested HTML tags
content = match.group(1)
# Remove any nested HTML tags (like <img>)
text_pattern = r"<[^>]+>"
text_only = re.sub(text_pattern, "", content)
# Clean up any extra whitespace
return text_only.strip()
return extracted_text
return ""
@register.filter