Przeglądaj źródła

Fixes #16851: Add missing Aria Labels (#22178)

* #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
Arthur Hanson 1 tydzień temu
rodzic
commit
7a3397798a
34 zmienionych plików z 219 dodań i 103 usunięć
  1. 2 1
      netbox/dcim/forms/model_forms.py
  2. 4 0
      netbox/netbox/forms/mixins.py
  3. 8 3
      netbox/netbox/tables/columns.py
  4. 2 2
      netbox/templates/account/preferences.html
  5. 1 1
      netbox/templates/dcim/device/base.html
  6. 6 6
      netbox/templates/dcim/device_edit.html
  7. 4 4
      netbox/templates/dcim/devicebay_populate.html
  8. 4 4
      netbox/templates/dcim/inc/interface_vlans_table.html
  9. 1 1
      netbox/templates/dcim/manufacturer.html
  10. 1 1
      netbox/templates/dcim/module.html
  11. 1 1
      netbox/templates/dcim/panels/rack_elevations.html
  12. 2 2
      netbox/templates/dcim/virtualchassis_edit.html
  13. 3 3
      netbox/templates/generic/object_edit.html
  14. 2 2
      netbox/templates/generic/object_list.html
  15. 2 2
      netbox/templates/inc/filter_list.html
  16. 4 4
      netbox/templates/inc/light_toggle.html
  17. 5 3
      netbox/templates/inc/notification_bell.html
  18. 12 12
      netbox/templates/inc/paginator.html
  19. 7 5
      netbox/templates/inc/table_controls_htmx.html
  20. 3 2
      netbox/templates/inc/toast.html
  21. 9 9
      netbox/templates/inc/user_menu.html
  22. 1 1
      netbox/templates/virtualization/virtualmachine/base.html
  23. 1 1
      netbox/utilities/forms/fields/fields.py
  24. 2 2
      netbox/utilities/templates/buttons/bookmark.html
  25. 2 2
      netbox/utilities/templates/buttons/subscribe.html
  26. 11 11
      netbox/utilities/templates/form_helpers/render_field.html
  27. 7 7
      netbox/utilities/templates/form_helpers/render_fieldset.html
  28. 2 0
      netbox/utilities/templates/helpers/utilization_graph.html
  29. 5 5
      netbox/utilities/templates/navigation/menu.html
  30. 4 2
      netbox/utilities/templates/widgets/apiselect.html
  31. 2 2
      netbox/utilities/templates/widgets/markdown_input.html
  32. 55 1
      netbox/utilities/templatetags/form_helpers.py
  33. 42 0
      netbox/utilities/tests/test_templatetags.py
  34. 2 1
      netbox/virtualization/forms/model_forms.py

+ 2 - 1
netbox/dcim/forms/model_forms.py

@@ -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'),

+ 4 - 0
netbox/netbox/forms/mixins.py

@@ -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)

+ 8 - 3
netbox/netbox/tables/columns.py

@@ -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):

+ 2 - 2
netbox/templates/account/preferences.html

@@ -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>

+ 1 - 1
netbox/templates/dcim/device/base.html

@@ -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 %}

+ 6 - 6
netbox/templates/dcim/device_edit.html

@@ -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>&nbsp;{% 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>&nbsp;{% trans "Remove" %}
               </a>
             </div>
             </div>

+ 4 - 4
netbox/templates/dcim/devicebay_populate.html

@@ -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 %}

+ 4 - 4
netbox/templates/dcim/inc/interface_vlans_table.html

@@ -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 %}

+ 1 - 1
netbox/templates/dcim/manufacturer.html

@@ -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" %}

+ 1 - 1
netbox/templates/dcim/module.html

@@ -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 - 1
netbox/templates/dcim/panels/rack_elevations.html

@@ -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>

+ 2 - 2
netbox/templates/dcim/virtualchassis_edit.html

@@ -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 %}

+ 3 - 3
netbox/templates/generic/object_edit.html

@@ -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 %}

+ 2 - 2
netbox/templates/generic/object_list.html

@@ -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>

+ 2 - 2
netbox/templates/inc/filter_list.html

@@ -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>

+ 4 - 4
netbox/templates/inc/light_toggle.html

@@ -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>

+ 5 - 3
netbox/templates/inc/notification_bell.html

@@ -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 %}

+ 12 - 12
netbox/templates/inc/paginator.html

@@ -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>&hellip;</span>
+                <span class="page-link" disabled aria-hidden="true">&hellip;</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>

+ 7 - 5
netbox/templates/inc/table_controls_htmx.html

@@ -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">

+ 3 - 2
netbox/templates/inc/toast.html

@@ -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 }}

+ 9 - 9
netbox/templates/inc/user_menu.html

@@ -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 %}

+ 1 - 1
netbox/templates/virtualization/virtualmachine/base.html

@@ -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" %}

+ 1 - 1
netbox/utilities/forms/fields/fields.py

@@ -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/'))
 

+ 2 - 2
netbox/utilities/templates/buttons/bookmark.html

@@ -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>

+ 2 - 2
netbox/utilities/templates/buttons/subscribe.html

@@ -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>

+ 11 - 11
netbox/utilities/templates/form_helpers/render_field.html

@@ -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 #}

+ 7 - 7
netbox/utilities/templates/form_helpers/render_fieldset.html

@@ -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 %}

+ 2 - 0
netbox/utilities/templates/helpers/utilization_graph.html

@@ -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 }}%;"
   >

+ 5 - 5
netbox/utilities/templates/navigation/menu.html

@@ -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>

+ 4 - 2
netbox/utilities/templates/widgets/apiselect.html

@@ -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>

+ 2 - 2
netbox/utilities/templates/widgets/markdown_input.html

@@ -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>

+ 55 - 1
netbox/utilities/templatetags/form_helpers.py

@@ -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
 #

+ 42 - 0
netbox/utilities/tests/test_templatetags.py

@@ -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)

+ 2 - 1
netbox/virtualization/forms/model_forms.py

@@ -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(),