* #16851 - Add missing Aria Labels * #16851 - Add missing Aria Labels * #16851 - Add missing Aria Labels * fixes for form field labels * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup
This commit is contained in:
parent
eeca184cb8
commit
7a3397798a
|
|
@ -690,7 +690,8 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
|
|||
)
|
||||
local_context_data = JSONField(
|
||||
required=False,
|
||||
label=''
|
||||
label='',
|
||||
widget=forms.Textarea(attrs={'aria-label': _('Local config context data')})
|
||||
)
|
||||
virtual_chassis = DynamicModelChoiceField(
|
||||
label=_('Virtual chassis'),
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ class SavedFiltersMixin(forms.Form):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Ensure the underlying <select> has an accessible name even when Tom Select
|
||||
# hides the original element (the visible UI is a JS-built combobox).
|
||||
self.fields['filter_id'].widget.attrs['aria-label'] = _('Saved filter')
|
||||
|
||||
# Limit saved filters to those applicable to the form's model
|
||||
if hasattr(self, 'model'):
|
||||
object_type = ObjectType.objects.get_for_model(self.model)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ from django.db.models import DateField, DateTimeField
|
|||
from django.template import Context, Template
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.html import escape
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2.columns import library
|
||||
from django_tables2.utils import Accessor
|
||||
|
|
@ -186,7 +187,8 @@ class ToggleColumn(tables.CheckBoxColumn):
|
|||
'class': 'w-1',
|
||||
},
|
||||
'input': {
|
||||
'class': 'form-check-input'
|
||||
'class': 'form-check-input',
|
||||
'aria-label': lambda record, value: format_lazy(_('Select {object}'), object=record),
|
||||
}
|
||||
}
|
||||
super().__init__(*args, default=default, visible=visible, **kwargs)
|
||||
|
|
@ -194,7 +196,10 @@ class ToggleColumn(tables.CheckBoxColumn):
|
|||
@property
|
||||
def header(self):
|
||||
title_text = _('Toggle all')
|
||||
return mark_safe(f'<input type="checkbox" class="toggle form-check-input" title="{title_text}" />')
|
||||
return format_html(
|
||||
'<input type="checkbox" class="toggle form-check-input" title="{}" aria-label="{}" />',
|
||||
title_text, title_text,
|
||||
)
|
||||
|
||||
|
||||
class BooleanColumn(tables.Column):
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" class="toggle form-check-input" title="{% trans "Toggle All" %}">
|
||||
<input type="checkbox" class="toggle form-check-input" title="{% trans "Toggle All" %}" aria-label="{% trans "Toggle All" %}">
|
||||
</th>
|
||||
<th>{% trans "Table" %}</th>
|
||||
<th>{% trans "Ordering" %}</th>
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
{% for table, prefs in request.user.config.data.tables.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="pk" value="tables.{{ table }}" class="form-check-input" />
|
||||
<input type="checkbox" name="pk" value="tables.{{ table }}" class="form-check-input" aria-label="{% blocktrans with name=table %}Select {{ name }}{% endblocktrans %}" />
|
||||
</td>
|
||||
<td>{{ table }}</td>
|
||||
<td>{{ prefs.ordering|join:", "|placeholder }}</td>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<button id="add-components" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
<ul class="dropdown-menu" aria-labelledby="add-components">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">{% trans "Console Ports" %}</a></li>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -37,18 +37,18 @@
|
|||
|
||||
{% if object.device_type.is_child_device and object.parent_bay %}
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Parent Device" %}</label>
|
||||
<label for="id_parent_device" class="col-sm-3 col-form-label">{% trans "Parent Device" %}</label>
|
||||
<div class="col">
|
||||
<input class="form-control" value="{{ object.parent_bay.device }}" disabled />
|
||||
<input id="id_parent_device" class="form-control" value="{{ object.parent_bay.device }}" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Parent Bay" %}</label>
|
||||
<label for="id_parent_bay" class="col-sm-3 col-form-label">{% trans "Parent Bay" %}</label>
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<input class="form-control" value="{{ object.parent_bay.name }}" disabled />
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=object.parent_bay.pk %}" title="{% trans "Regenerate Slug" %}" class="btn btn-danger d-inline-flex align-items-center">
|
||||
<i class="mdi mdi-close-thick"></i> {% trans "Remove" %}
|
||||
<input id="id_parent_bay" class="form-control" value="{{ object.parent_bay.name }}" disabled />
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=object.parent_bay.pk %}" title="{% trans "Remove" %}" aria-label="{% trans "Remove" %}" class="btn btn-danger d-inline-flex align-items-center">
|
||||
<i class="mdi mdi-close-thick" aria-hidden="true"></i> {% trans "Remove" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@
|
|||
<h2 class="card-header">{% block title %}{% trans "Populate" %} {{ device_bay }}{% endblock %}</h2>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label text-lg-end">{% trans "Parent Device" %}</label>
|
||||
<label for="id_parent_device" class="col-sm-3 col-form-label text-lg-end">{% trans "Parent Device" %}</label>
|
||||
<div class="col">
|
||||
<input class="form-control" value="{{ device_bay.device }}" disabled />
|
||||
<input id="id_parent_device" class="form-control" value="{{ device_bay.device }}" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label text-lg-end">{% trans "Bay" %}</label>
|
||||
<label for="id_bay" class="col-sm-3 col-form-label text-lg-end">{% trans "Bay" %}</label>
|
||||
<div class="col">
|
||||
<input class="form-control" value="{{ device_bay }}" disabled />
|
||||
<input id="id_bay" class="form-control" value="{{ device_bay }}" disabled />
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
<td>{{ obj.untagged_vlan|linkify:"vid" }}</td>
|
||||
<td>{{ obj.untagged_vlan.name }}</td>
|
||||
<td>
|
||||
<input type="radio" name="untagged_vlan" value="{{ obj.untagged_vlan.pk }}" checked="checked" />
|
||||
<input type="radio" name="untagged_vlan" value="{{ obj.untagged_vlan.pk }}" checked="checked" aria-label="{% blocktrans with vid=obj.untagged_vlan.vid name=obj.untagged_vlan.name %}Set VLAN {{ vid }} ({{ name }}) as untagged{% endblocktrans %}" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="tagged_vlans" value="{{ obj.untagged_vlan.pk }}" />
|
||||
<input type="checkbox" name="tagged_vlans" value="{{ obj.untagged_vlan.pk }}" aria-label="{% blocktrans with vid=obj.untagged_vlan.vid name=obj.untagged_vlan.name %}Tag VLAN {{ vid }} ({{ name }}){% endblocktrans %}" />
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
|
@ -24,10 +24,10 @@
|
|||
<td>{{ vlan|linkify:"vid" }}</td>
|
||||
<td>{{ vlan.name }}</td>
|
||||
<td>
|
||||
<input type="radio" name="untagged_vlan" value="{{ vlan.pk }}"{% if vlan == obj.untagged_vlan %} checked="checked"{% endif %} />
|
||||
<input type="radio" name="untagged_vlan" value="{{ vlan.pk }}"{% if vlan == obj.untagged_vlan %} checked="checked"{% endif %} aria-label="{% blocktrans with vid=vlan.vid name=vlan.name %}Set VLAN {{ vid }} ({{ name }}) as untagged{% endblocktrans %}" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="tagged_vlans" value="{{ vlan.pk }}" checked="checked" />
|
||||
<input type="checkbox" name="tagged_vlans" value="{{ vlan.pk }}" checked="checked" aria-label="{% blocktrans with vid=vlan.vid name=vlan.name %}Tag VLAN {{ vid }} ({{ name }}){% endblocktrans %}" />
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<button id="add-components" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
<ul class="dropdown-menu" aria-labelledby="add-components">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}">
|
||||
{% trans "Add Device Type" %}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<button id="add-components" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
<ul class="dropdown-menu" aria-labelledby="add-components">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={{ object.get_absolute_url }}">{% trans "Console Ports" %}</a></li>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% load i18n %}
|
||||
<div class="text-end mb-4">
|
||||
<select class="btn btn-outline-secondary no-ts rack-view">
|
||||
<select class="btn btn-outline-secondary no-ts rack-view" aria-label="{% trans "Rack elevation view" %}">
|
||||
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
||||
<option value="images-only">{% trans "Images only" %}</option>
|
||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab">
|
||||
<form action="" method="post" enctype="multipart/form-data" class="object-edit">
|
||||
{% render_errors vc_form %}
|
||||
{% for form in formset %}
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if virtual_chassis.pk %}
|
||||
<a href="{% url 'dcim:virtualchassis_remove_member' pk=device.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=virtual_chassis.pk %}" class="btn btn-danger{% if virtual_chassis.master == device %} disabled{% endif %}">
|
||||
<a href="{% url 'dcim:virtualchassis_remove_member' pk=device.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=virtual_chassis.pk %}" title="{% trans "Remove member" %}" aria-label="{% trans "Remove member" %}" class="btn btn-danger{% if virtual_chassis.master == device %} disabled{% endif %}">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ Context:
|
|||
|
||||
{# Link to model documentation #}
|
||||
{% if settings.DOCS_ROOT and object.docs_url %}
|
||||
<a href="{{ object.docs_url }}" target="_blank" class="btn btn-outline-secondary" title="{% trans "View model documentation" %}">
|
||||
<i class="mdi mdi-help-circle"></i> {% trans "Help" %}
|
||||
<a href="{{ object.docs_url }}" target="_blank" class="btn btn-outline-secondary" title="{% trans "View model documentation" %}" aria-label="{% trans "View model documentation" %}">
|
||||
<i class="mdi mdi-help-circle" aria-hidden="true"></i> {% trans "Help" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ Context:
|
|||
{% endblock tabs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab">
|
||||
|
||||
{# Warn about missing prerequisite objects #}
|
||||
{% if prerequisite_model %}
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ Context:
|
|||
{% block tabs %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||
<a class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="object-list" aria-selected="true">
|
||||
{% trans "Results" %}
|
||||
<span class="badge text-bg-secondary total-object-count">{% if table.page.paginator.count %}{{ table.page.paginator.count }}{% else %}{{ total_count|default:"0" }}{% endif %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if filter_form %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="filters-form" aria-selected="false">
|
||||
{% trans "Filters" %}
|
||||
{% if filter_form %}{% badge filter_form.changed_data|length bg_color="primary" %}{% endif %}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@
|
|||
<div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always">
|
||||
<div class="btn-list">
|
||||
<button type="button" class="btn btn-outline-danger" data-reset-select>
|
||||
<i class="mdi mdi-backspace"></i> {% trans "Reset" %}
|
||||
<i class="mdi mdi-backspace" aria-hidden="true"></i> {% trans "Reset" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="mdi mdi-magnify"></i> {% trans "Search" %}
|
||||
<i class="mdi mdi-magnify" aria-hidden="true"></i> {% trans "Search" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
<div class="d-flex ms-2">
|
||||
<button class="nav-link color-mode-toggle hide-theme-dark fs-2 p-0 text-secondary" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||
<i class="mdi mdi-lightbulb"></i>
|
||||
<button type="button" class="nav-link color-mode-toggle hide-theme-dark fs-2 p-0 text-secondary" title="{% trans "Enable dark mode" %}" aria-label="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||
<i class="mdi mdi-lightbulb" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="nav-link color-mode-toggle hide-theme-light fs-2 p-0 text-secondary" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||
<i class="mdi mdi-lightbulb-on"></i>
|
||||
<button type="button" class="nav-link color-mode-toggle hide-theme-light fs-2 p-0 text-secondary" title="{% trans "Enable light mode" %}" aria-label="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||
<i class="mdi mdi-lightbulb-on" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
{% load i18n %}
|
||||
{% if notifications %}
|
||||
<span class="text-primary" id="notifications-alert" hx-swap-oob="true">
|
||||
<i class="mdi mdi-bell-ring"></i>
|
||||
<span class="badge bg-red"></span>
|
||||
<i class="mdi mdi-bell-ring" aria-hidden="true"></i>
|
||||
<span class="badge bg-red" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">{% trans "You have unread notifications" %}</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted" id="notifications-alert" hx-swap-oob="true">
|
||||
<i class="mdi mdi-bell"></i>
|
||||
<i class="mdi mdi-bell" aria-hidden="true"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@
|
|||
{% if page.has_previous %}
|
||||
<li class="page-item">
|
||||
{% if htmx %}
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}" class="page-link">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}" class="page-link" aria-label="{% trans "Previous page" %}">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% querystring request page=page.previous_page_number %}" class="page-link">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
<a href="{% querystring request page=page.previous_page_number %}" class="page-link" aria-label="{% trans "Previous page" %}">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
|
@ -33,17 +33,17 @@
|
|||
|
||||
{# Page numbers #}
|
||||
{% for p in page.smart_pages %}
|
||||
<li class="page-item{% if page.number == p %} active" aria-current="page{% endif %}">
|
||||
<li class="page-item{% if page.number == p %} active{% endif %}"{% if page.number == p %} aria-current="page"{% endif %}>
|
||||
{% if p and htmx %}
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=p %}" class="page-link">
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=p %}" class="page-link" aria-label="{% blocktrans with num=p %}Page {{ num }}{% endblocktrans %}">
|
||||
{{ p }}
|
||||
</a>
|
||||
{% elif p %}
|
||||
<a href="{% querystring request page=p %}" class="page-link {% if page.number == p %} active{% endif %}">
|
||||
<a href="{% querystring request page=p %}" class="page-link {% if page.number == p %} active{% endif %}" aria-label="{% blocktrans with num=p %}Page {{ num }}{% endblocktrans %}">
|
||||
{{ p }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="page-link" disabled>…</span>
|
||||
<span class="page-link" disabled aria-hidden="true">…</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
@ -53,12 +53,12 @@
|
|||
{% if page.has_next %}
|
||||
<li class="page-item">
|
||||
{% if htmx %}
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}" class="page-link">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}" class="page-link" aria-label="{% trans "Next page" %}">
|
||||
<i class="mdi mdi-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% querystring request page=page.next_page_number %}" class="page-link">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{% querystring request page=page.next_page_number %}" class="page-link" aria-label="{% trans "Next page" %}">
|
||||
<i class="mdi mdi-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
<div class="row mb-3" id="results">
|
||||
<div class="col-auto d-print-none">
|
||||
<div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap">
|
||||
<label for="quicksearch" class="visually-hidden">{% trans "Quick search" %}</label>
|
||||
<input type="search" results="5" name="q" id="quicksearch" class="form-control" placeholder="{% trans "Quick search" %}"
|
||||
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search"/>
|
||||
<span class="input-group-text py-1">
|
||||
<a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a>
|
||||
<a href="#" id="quicksearch_clear" class="invisible text-secondary" aria-label="{% trans "Clear quick search" %}"><i class="mdi mdi-close-circle" aria-hidden="true"></i></a>
|
||||
</span>
|
||||
{% block extra_table_controls %}{% endblock %}
|
||||
</div>
|
||||
|
|
@ -16,9 +17,10 @@
|
|||
{% if filter_form %}
|
||||
<div class="col-auto d-print-none">
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<i class="mdi mdi-filter" title="{% trans "Saved filter" %}"></i>
|
||||
</div>
|
||||
<label for="{{ filter_form.filter_id.id_for_label }}" class="input-group-text">
|
||||
<i class="mdi mdi-filter" title="{% trans "Saved filter" %}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{% trans "Saved filter" %}</span>
|
||||
</label>
|
||||
{{ filter_form.filter_id }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -28,7 +30,7 @@
|
|||
{% if request.user.is_authenticated and table_modal %}
|
||||
<div class="table-configure btn-group">
|
||||
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}" class="btn">
|
||||
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
|
||||
<i class="mdi mdi-cog" aria-hidden="true"></i> {% trans "Configure Table" %}
|
||||
</button>
|
||||
{% if table.config_params or table_configs %}
|
||||
<button type="button" class="btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="toast toast-dark border-0 shadow-sm" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="10000">
|
||||
<div class="toast-header text-bg-{{ status }}">
|
||||
<i class="mdi mdi-{{ status|icon_from_status }} me-1"></i>
|
||||
<i class="mdi mdi-{{ status|icon_from_status }} me-1" aria-hidden="true"></i>
|
||||
{{ title }}
|
||||
<button type="button" class="btn-close me-0 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close me-0 m-auto" data-bs-dismiss="toast" aria-label="{% trans "Close" %}"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
{{ message }}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{% if settings.RELEASE.features.help_center %}
|
||||
{# Help center control #}
|
||||
<a href="#" class="nav-link px-1" aria-label="{% trans "Help center" %}">
|
||||
<i class="mdi mdi-forum-outline"></i>
|
||||
<i class="mdi mdi-forum-outline" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
{# User menu #}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="{% trans "Open user menu" %}" aria-expanded="false" role="button">
|
||||
<div class="d-xl-block ps-2">
|
||||
<div>{{ request.user }}</div>
|
||||
<div class="mt-1 small text-secondary">
|
||||
|
|
@ -35,30 +35,30 @@
|
|||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<a href="{% url 'account:profile' %}" class="dropdown-item">
|
||||
<i class="dropdown-item-icon mdi mdi-account"></i> {% trans "Profile" %}
|
||||
<i class="dropdown-item-icon mdi mdi-account" aria-hidden="true"></i> {% trans "Profile" %}
|
||||
</a>
|
||||
<a href="{% url 'account:bookmarks' %}" class="dropdown-item">
|
||||
<i class="dropdown-item-icon mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
|
||||
<i class="dropdown-item-icon mdi mdi-bookmark" aria-hidden="true"></i> {% trans "Bookmarks" %}
|
||||
</a>
|
||||
<a href="{% url 'account:subscriptions' %}" class="dropdown-item">
|
||||
<i class="dropdown-item-icon mdi mdi-bell"></i> {% trans "Subscriptions" %}
|
||||
<i class="dropdown-item-icon mdi mdi-bell" aria-hidden="true"></i> {% trans "Subscriptions" %}
|
||||
</a>
|
||||
<a href="{% url 'account:preferences' %}" class="dropdown-item">
|
||||
<i class="dropdown-item-icon mdi mdi-wrench"></i> {% trans "Preferences" %}
|
||||
<i class="dropdown-item-icon mdi mdi-wrench" aria-hidden="true"></i> {% trans "Preferences" %}
|
||||
</a>
|
||||
<a href="{% url 'account:usertoken_list' %}" class="dropdown-item">
|
||||
<i class="dropdown-item-icon mdi mdi-key"></i> {% trans "API Tokens" %}
|
||||
<i class="dropdown-item-icon mdi mdi-key" aria-hidden="true"></i> {% trans "API Tokens" %}
|
||||
</a>
|
||||
<hr class="dropdown-divider" />
|
||||
<a href="{% url 'logout' %}" hx-disable="true" class="dropdown-item">
|
||||
<i class="dropdown-item-icon mdi mdi-logout-variant"></i> {% trans "Log Out" %}
|
||||
<i class="dropdown-item-icon mdi mdi-logout-variant" aria-hidden="true"></i> {% trans "Log Out" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="btn-group align-items-center ps-2">
|
||||
<a class="btn btn-primary" type="button" href="{% url 'login' %}?next={{ request.path }}">
|
||||
<i class="mdi mdi-login-variant"></i> {% trans "Log In" %}
|
||||
<i class="mdi mdi-login-variant" aria-hidden="true"></i> {% trans "Log In" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<button id="add-components" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
<ul class="dropdown-menu" aria-labelledby="add-components">
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<li><a class="dropdown-item" href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">
|
||||
{% trans "Interfaces" %}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class CommentField(forms.CharField):
|
|||
widget = widgets.MarkdownWidget
|
||||
label = _('Comments')
|
||||
help_text = _(
|
||||
'<i class="mdi mdi-information-outline"></i> '
|
||||
'<i class="mdi mdi-information-outline" aria-hidden="true"></i> '
|
||||
'<a href="{url}" target="_blank" tabindex="-1">Markdown</a> syntax is supported'
|
||||
).format(url=static('docs/reference/markdown/'))
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
{% endfor %}
|
||||
{% if bookmark %}
|
||||
<button type="submit" class="btn btn-cyan">
|
||||
<i class="mdi mdi-bookmark-minus"></i> {% trans "Unbookmark" %}
|
||||
<i class="mdi mdi-bookmark-minus" aria-hidden="true"></i> {% trans "Unbookmark" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-cyan">
|
||||
<i class="mdi mdi-bookmark-plus"></i> {% trans "Bookmark" %}
|
||||
<i class="mdi mdi-bookmark-plus" aria-hidden="true"></i> {% trans "Bookmark" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@
|
|||
{% endfor %}
|
||||
{% if subscription %}
|
||||
<button type="submit" class="btn btn-cyan">
|
||||
<i class="mdi mdi-bell-minus"></i> {% trans "Unsubscribe" %}
|
||||
<i class="mdi mdi-bell-minus" aria-hidden="true"></i> {% trans "Unsubscribe" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-cyan">
|
||||
<i class="mdi mdi-bell-plus"></i> {% trans "Subscribe" %}
|
||||
<i class="mdi mdi-bell-plus" aria-hidden="true"></i> {% trans "Subscribe" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -18,45 +18,45 @@
|
|||
{# Include the "regenerate" button on slug fields #}
|
||||
{% if field|widget_type == 'slugwidget' %}
|
||||
<div class="input-group">
|
||||
{{ field }}
|
||||
<button type="button" title="{% trans "Regenerate Slug" %}" class="btn reslug">
|
||||
<i class="mdi mdi-reload"></i>
|
||||
{% render_field_with_aria field %}
|
||||
<button type="button" title="{% trans "Regenerate Slug" %}" aria-label="{% trans "Regenerate Slug" %}" class="btn reslug">
|
||||
<i class="mdi mdi-reload" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{# Render checkbox labels to the right of the field #}
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<div class="form-check mb-0">
|
||||
{{ field }}
|
||||
{% render_field_with_aria field %}
|
||||
<label for="{{ field.id_for_label }}" class="form-check-label">
|
||||
{{ label }}
|
||||
</label>
|
||||
{% if field.help_text %}
|
||||
<span class="form-text">{{ field.help_text|safe }}</span>
|
||||
<span class="form-text" id="{{ field.auto_id }}_helptext">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# Include a copy-to-clipboard button #}
|
||||
{% elif 'data-clipboard' in field.field.widget.attrs %}
|
||||
<div class="input-group">
|
||||
{{ field }}
|
||||
<button type="button" title="{% trans "Copy to clipboard" %}" class="btn copy-content" data-clipboard-target="#{{ field.id_for_label }}">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
{% render_field_with_aria field %}
|
||||
<button type="button" title="{% trans "Copy to clipboard" %}" aria-label="{% trans "Copy to clipboard" %}" class="btn copy-content" data-clipboard-target="#{{ field.id_for_label }}">
|
||||
<i class="mdi mdi-content-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{# Default field rendering #}
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% render_field_with_aria field %}
|
||||
{% endif %}
|
||||
|
||||
{# Display any error messages #}
|
||||
{% if field.errors %}
|
||||
<div class="form-text text-danger">
|
||||
<div class="form-text text-danger" id="{{ field.auto_id }}_errors" role="alert">
|
||||
{% for error in field.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Help text #}
|
||||
{% if field.help_text and field|widget_type != 'checkboxinput' %}
|
||||
<span class="form-text">{{ field.help_text|safe }}</span>
|
||||
<span class="form-text" id="{{ field.auto_id }}_helptext">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
{# For bulk edit forms, include an option to nullify the field #}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load form_helpers %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="field-group mb-5"{% if heading %} role="group" aria-label="{{ heading }}"{% endif %}>
|
||||
{% if heading %}
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{{ heading }}</h2>
|
||||
|
|
@ -25,14 +25,14 @@
|
|||
|
||||
{% elif layout == 'inline' %}
|
||||
{# Multiple form fields on the same line #}
|
||||
<div class="row mb-3">
|
||||
<div class="row mb-3"{% if title %} role="group" aria-label="{{ title }}"{% endif %}>
|
||||
<label class="col col-3 col-form-label text-lg-end">{{ title|default:'' }}</label>
|
||||
{% for field in items %}
|
||||
<div class="col mb-1">
|
||||
{{ field }}
|
||||
<div class="form-text">{% trans field.label %}</div>
|
||||
{% render_field_with_aria field has_helptext=True %}
|
||||
<div class="form-text" id="{{ field.auto_id }}_helptext">{% trans field.label %}</div>
|
||||
{% if field.errors %}
|
||||
<div class="form-text text-danger">
|
||||
<div class="form-text text-danger" id="{{ field.auto_id }}_errors" role="alert">
|
||||
{% for error in field.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<ul class="nav nav-pills mb-1" role="tablist">
|
||||
{% for tab in items %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<button role="tab" type="button" id="{{ tab.id }}_tab" data-bs-toggle="tab" aria-controls="{{ tab.id }}" data-bs-target="#{{ tab.id }}" class="nav-link {% if tab.active %}active{% endif %}">
|
||||
<button role="tab" type="button" id="{{ tab.id }}_tab" data-bs-toggle="tab" aria-controls="{{ tab.id }}" aria-selected="{% if tab.active %}true{% else %}false{% endif %}" data-bs-target="#{{ tab.id }}" class="nav-link {% if tab.active %}active{% endif %}">
|
||||
{% trans tab.title %}
|
||||
</button>
|
||||
</li>
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
</div>
|
||||
<div class="tab-content p-0 border-0">
|
||||
{% for tab in items %}
|
||||
<div class="tab-pane {% if tab.active %}active{% endif %}" id="{{ tab.id }}" role="tabpanel" aria-labeled-by="{{ tab.id }}_tab">
|
||||
<div class="tab-pane {% if tab.active %}active{% endif %}" id="{{ tab.id }}" role="tabpanel" aria-labelledby="{{ tab.id }}_tab">
|
||||
{% for field in tab.fields %}
|
||||
{% render_field field %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
<div class="progress">
|
||||
<div
|
||||
|
|
@ -5,6 +6,7 @@
|
|||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="{{ utilization }}"
|
||||
aria-label="{% blocktrans with value=utilization|floatformat:1 %}Utilization: {{ value }}%{% endblocktrans %}"
|
||||
class="progress-bar {{ bar_class }}"
|
||||
style="width: {{ utilization|unlocalize }}%;"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div class="input-group mb-1 mt-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<i class="mdi mdi-magnify" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" name="q" value="" class="form-control" placeholder="{% trans "Search…" %}" aria-label="{% trans "Search NetBox" %}">
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
<li class="nav-item dropdown">
|
||||
|
||||
{# Menu heading #}
|
||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false" >
|
||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false" aria-label="{{ menu.label }}">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<i class="{{ menu.icon_class }}"></i>
|
||||
<i class="{{ menu.icon_class }}" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="nav-link-title">
|
||||
{{ menu.label }}
|
||||
|
|
@ -45,8 +45,8 @@
|
|||
{% if buttons %}
|
||||
<div class="btn-group ms-1">
|
||||
{% for button in buttons %}
|
||||
<a href="{{ button.url }}" class="btn btn-sm btn-{{ button.color|default:"outline" }} lh-2 px-2" title="{{ button.title }}">
|
||||
<i class="{{ button.icon_class }}"></i>
|
||||
<a href="{{ button.url }}" class="btn btn-sm btn-{{ button.color|default:"outline" }} lh-2 px-2" title="{{ button.title }}" aria-label="{{ button.title }}">
|
||||
<i class="{{ button.icon_class }}" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@
|
|||
<button
|
||||
type="button"
|
||||
title="{% trans "Open selector" %}"
|
||||
aria-label="{% trans "Open selector" %}"
|
||||
class="btn btn-outline-secondary ms-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
hx-get="{% url 'htmx_object_selector' %}?_model={{ widget.attrs.selector }}&target={{ widget.attrs.id }}"
|
||||
hx-target="#htmx-modal-content"
|
||||
>
|
||||
<i class="mdi mdi-database-search-outline"></i>
|
||||
<i class="mdi mdi-database-search-outline" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if quick_add and not widget.attrs.disabled %}
|
||||
|
|
@ -20,13 +21,14 @@
|
|||
<button
|
||||
type="button"
|
||||
title="{% trans "Quick add" %}"
|
||||
aria-label="{% trans "Quick add" %}"
|
||||
class="btn btn-outline-secondary ms-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
hx-get="{{ quick_add.url }}?_quickadd=True&target={{ widget.attrs.id }}{% for k, v in quick_add.params.items %}&{{ k }}={{ v }}{% endfor %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
>
|
||||
<i class="mdi mdi-plus-circle"></i>
|
||||
<i class="mdi mdi-plus-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% load i18n %}
|
||||
<div class="markdown-widget">
|
||||
<ul class="nav nav-pills mb-1">
|
||||
<ul class="nav nav-pills mb-1" role="tablist" aria-label="{% trans "Markdown editor mode" %}">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active " id="{{ widget.name }}-input-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-input" type="button" role="tab" aria-controls="{{ widget.name }}-input" aria-selected="true">
|
||||
{% trans "Write" %}
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
{% include "django/forms/widgets/textarea.html" %}
|
||||
</div>
|
||||
<div class="tab-pane show" id="{{ widget.name }}-markdown-preview" role="tabpanel" aria-labelledby="{{ widget.name }}-markdown-preview-tab">
|
||||
<div id="{{ widget.name }}-preview" class="preview px-3 py-2 border-1"></div>
|
||||
<div id="{{ widget.name }}-preview" class="preview px-3 py-2 border-1" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from django import template
|
||||
import warnings
|
||||
|
||||
from django import forms, template
|
||||
from django.conf import settings
|
||||
|
||||
from utilities.forms.rendering import InlineFields, M2MAddRemoveFields, ObjectAttribute, TabbedGroups
|
||||
|
||||
|
|
@ -7,6 +10,7 @@ __all__ = (
|
|||
'render_custom_fields',
|
||||
'render_errors',
|
||||
'render_field',
|
||||
'render_field_with_aria',
|
||||
'render_form',
|
||||
'widget_type',
|
||||
)
|
||||
|
|
@ -42,6 +46,56 @@ def widget_type(field):
|
|||
return None
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def render_field_with_aria(field, has_helptext=None):
|
||||
"""Render a bound form field with aria-describedby/aria-invalid/aria-label wired up."""
|
||||
if has_helptext is None:
|
||||
has_helptext = bool(field.help_text)
|
||||
widget_attrs = field.field.widget.attrs
|
||||
described_by = []
|
||||
if field.errors:
|
||||
described_by.append(f'{field.auto_id}_errors')
|
||||
if has_helptext:
|
||||
described_by.append(f'{field.auto_id}_helptext')
|
||||
extra_attrs = {}
|
||||
if described_by:
|
||||
# Merge with any aria-describedby already set on the widget so we
|
||||
# append to (rather than clobber) descriptions defined elsewhere.
|
||||
existing = widget_attrs.get('aria-describedby', '').strip()
|
||||
extra_attrs['aria-describedby'] = ' '.join(
|
||||
filter(None, [existing, *described_by])
|
||||
)
|
||||
if field.errors:
|
||||
extra_attrs['aria-invalid'] = 'true'
|
||||
# Mirror field.label onto <select> widgets hidden by Tom Select
|
||||
# (ts-hidden-accessible, tabindex=-1), where scanners drop the <label for=>
|
||||
# association. Skip selects opted out of Tom Select (``.no-ts`` class or a
|
||||
# ``size`` attribute) since they stay visible and keep their association.
|
||||
#
|
||||
# When a field has no label at all (label=''), we deliberately do NOT
|
||||
# synthesize one from the field name: that would inject an untranslated
|
||||
# English string into the rendered DOM and degrade the experience for
|
||||
# non-English locales. In DEBUG we emit a warning so developers add a
|
||||
# proper translated label on the field.
|
||||
if 'aria-label' not in widget_attrs:
|
||||
if isinstance(field.field.widget, forms.Select) and field.label:
|
||||
tom_select_excluded = (
|
||||
'no-ts' in widget_attrs.get('class', '').split()
|
||||
or 'size' in widget_attrs
|
||||
)
|
||||
if not tom_select_excluded:
|
||||
extra_attrs['aria-label'] = str(field.label)
|
||||
elif not field.label and settings.DEBUG:
|
||||
form_name = getattr(getattr(field, 'form', None), '__class__', type(None)).__name__
|
||||
warnings.warn(
|
||||
f"Form field {form_name}.{field.name} has no label; no aria-label "
|
||||
"will be set. Add a translated label to the field for proper "
|
||||
"accessibility.",
|
||||
stacklevel=2,
|
||||
)
|
||||
return field.as_widget(attrs=extra_attrs)
|
||||
|
||||
|
||||
#
|
||||
# Inclusion tags
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import warnings
|
||||
from unittest.mock import patch
|
||||
|
||||
from django import forms
|
||||
from django.template.loader import render_to_string
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
|
|
@ -8,6 +10,7 @@ from dcim.models import Site
|
|||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField, CustomFieldChoiceSet
|
||||
from utilities.templatetags.builtins.tags import badge, customfield_value, static_with_params
|
||||
from utilities.templatetags.form_helpers import render_field_with_aria
|
||||
from utilities.templatetags.helpers import _humanize_capacity, humanize_speed
|
||||
|
||||
|
||||
|
|
@ -242,3 +245,42 @@ class HumanizeSpeedTestCase(TestCase):
|
|||
def test_trailing_zeros_stripped(self):
|
||||
"""Ensure trailing fractional zeros are stripped (5.500 → 5.5)."""
|
||||
self.assertEqual(humanize_speed(5_500_000), '5.5 Gbps')
|
||||
|
||||
|
||||
class RenderFieldWithAriaTestCase(TestCase):
|
||||
"""
|
||||
Test the render_field_with_aria template tag.
|
||||
"""
|
||||
|
||||
def test_aria_describedby_includes_errors_and_helptext(self):
|
||||
class TestForm(forms.Form):
|
||||
name = forms.CharField(help_text='Hello', required=True)
|
||||
|
||||
form = TestForm({'name': ''})
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
html = render_field_with_aria(form['name'])
|
||||
|
||||
self.assertIn('aria-invalid="true"', html)
|
||||
self.assertIn('id_name_errors', html)
|
||||
self.assertIn('id_name_helptext', html)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_missing_label_emits_debug_warning(self):
|
||||
class TestForm(forms.Form):
|
||||
dns_name = forms.CharField(label='')
|
||||
|
||||
form = TestForm()
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter('always')
|
||||
html = render_field_with_aria(form['dns_name'])
|
||||
|
||||
messages = [str(w.message) for w in caught]
|
||||
self.assertTrue(
|
||||
any('TestForm.dns_name' in m for m in messages),
|
||||
f'Expected a warning naming TestForm.dns_name; got: {messages}',
|
||||
)
|
||||
# No aria-label should be synthesized — an untranslated English fallback
|
||||
# would degrade accessibility under non-English locales.
|
||||
self.assertNotIn('aria-label', html)
|
||||
|
|
|
|||
|
|
@ -249,7 +249,8 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
|
|||
)
|
||||
local_context_data = JSONField(
|
||||
required=False,
|
||||
label=''
|
||||
label='',
|
||||
widget=forms.Textarea(attrs={'aria-label': _('Local config context data')})
|
||||
)
|
||||
config_template = DynamicModelChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue