diff options
| author | antoliny0919 <antoliny0919@gmail.com> | 2025-07-28 07:59:26 +0900 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2025-08-19 16:35:56 +0200 |
| commit | 4187da258fe212d494cb578a0bc2b52c4979ab95 (patch) | |
| tree | 67f6205a16b1f6785f1ed4e06348d332c36a50f3 /django | |
| parent | ad4a9e0f3b1de261409bc083aa49dba705531824 (diff) | |
Fixed #35892 -- Supported Widget.use_fieldset in admin forms.
Diffstat (limited to 'django')
11 files changed, 81 insertions, 14 deletions
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 969167f0e2..bd37f519cd 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -173,6 +173,7 @@ class AdminField: self.is_first = is_first # Whether this field is first on the line self.is_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput) self.is_readonly = False + self.is_fieldset = self.field.field.widget.use_fieldset def label_tag(self): classes = [] @@ -185,12 +186,14 @@ class AdminField: if not self.is_first: classes.append("inline") attrs = {"class": " ".join(classes)} if classes else {} + tag = "legend" if self.is_fieldset else None # checkboxes should not have a label suffix as the checkbox appears # to the left of the label. return self.field.label_tag( contents=mark_safe(contents), attrs=attrs, label_suffix="" if self.is_checkbox else None, + tag=tag, ) def errors(self): diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index cf96331dea..057c5258f2 100644 --- a/django/contrib/admin/static/admin/css/forms.css +++ b/django/contrib/admin/static/admin/css/forms.css @@ -36,12 +36,13 @@ form .form-row p { /* FORM LABELS */ -label { +legend, label { font-weight: normal; color: var(--body-quiet-color); font-size: 0.8125rem; } +.required legend, legend.required, .required label, label.required { font-weight: bold; } @@ -91,6 +92,20 @@ fieldset .inline-heading, /* ALIGNED FIELDSETS */ +.aligned fieldset { + width: 100%; + border-top: none; +} + +.aligned fieldset > div { + width: 100%; +} + +.aligned legend { + float: left; +} + +.aligned legend, .aligned label { display: block; padding: 4px 10px 0 0; @@ -138,6 +153,10 @@ form .aligned div.radiolist { padding: 0; } +form .aligned fieldset div.help { + margin-left: 0; +} + form .aligned p.help, form .aligned div.help { margin-top: 0; diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index bf6cd6a56c..fbf0d88b40 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -170,6 +170,7 @@ input[type="submit"], button { /* Forms */ + legend, label { font-size: 1rem; } @@ -484,6 +485,7 @@ input[type="submit"], button { padding-top: 15px; } + .aligned legend, .aligned label { width: 100%; min-width: auto; diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 6a61821254..ea967018fb 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -301,6 +301,10 @@ p.datetime { font-weight: bold; } +p.datetime label { + display: inline; +} + .datetime span { white-space: nowrap; font-weight: normal; diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js index 970b511b0c..2100280220 100644 --- a/django/contrib/admin/static/admin/js/SelectFilter2.js +++ b/django/contrib/admin/static/admin/js/SelectFilter2.js @@ -15,7 +15,8 @@ Requires core.js and SelectBox.js. const from_box = document.getElementById(field_id); from_box.id += '_from'; // change its ID from_box.className = 'filtered'; - from_box.setAttribute('aria-labelledby', field_id + '_from_title'); + from_box.setAttribute('aria-labelledby', field_id + '_from_label'); + from_box.setAttribute('aria-describedby', `${field_id}_helptext ${field_id}_choose_helptext`); for (const p of from_box.parentNode.getElementsByTagName('p')) { if (p.classList.contains("info")) { @@ -42,12 +43,20 @@ Requires core.js and SelectBox.js. const selector_available_title = quickElement('div', selector_available); selector_available_title.id = field_id + '_from_title'; selector_available_title.className = 'selector-available-title'; - quickElement('label', selector_available_title, interpolate(gettext('Available %s') + ' ', [field_name]), 'for', field_id + '_from'); + quickElement( + 'label', + selector_available_title, + interpolate(gettext('Available %s') + ' ', [field_name]), + 'id', + field_id + '_from_label', + 'for', + field_id + '_from' + ); quickElement( 'p', selector_available_title, interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]), - 'class', 'helptext' + 'id', `${field_id}_choose_helptext`, 'class', 'helptext' ); const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); @@ -102,12 +111,20 @@ Requires core.js and SelectBox.js. const selector_chosen_title = quickElement('div', selector_chosen); selector_chosen_title.className = 'selector-chosen-title'; selector_chosen_title.id = field_id + '_to_title'; - quickElement('label', selector_chosen_title, interpolate(gettext('Chosen %s') + ' ', [field_name]), 'for', field_id + '_to'); + quickElement( + 'label', + selector_chosen_title, + interpolate(gettext('Chosen %s') + ' ', [field_name]), + 'id', + field_id + '_to_label', + 'for', + field_id + '_to' + ); quickElement( 'p', selector_chosen_title, interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]), - 'class', 'helptext' + 'id', `${field_id}_remove_helptext`, 'class', 'helptext' ); const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected'); @@ -134,7 +151,8 @@ Requires core.js and SelectBox.js. 'multiple', '', 'size', from_box.size, 'name', from_box.name, - 'aria-labelledby', field_id + '_to_title', + 'aria-labelledby', field_id + '_to_label', + 'aria-describedby', `${field_id}_helptext ${field_id}_remove_helptext`, 'class', 'filtered' ); const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display'); diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index 8168172a97..6251614863 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -91,7 +91,10 @@ message = interpolate(message, [timezoneOffset]); const warning = document.createElement('div'); + const id = inp.id; + const field_id = inp.closest('p.datetime') ? id.slice(0, id.lastIndexOf("_")) : id; warning.classList.add('help', warningClass); + warning.id = `${field_id}_timezone_warning_helptext`; warning.textContent = message; inp.parentNode.appendChild(warning); }, diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index ce20c8ac44..01c357d8c7 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -40,7 +40,7 @@ <div class="form-row"> {{ form.usable_password.errors }} - <div class="flex-container">{{ form.usable_password.label_tag }} {{ form.usable_password }}</div> + <fieldset class="flex-container">{{ form.usable_password.legend_tag }} {{ form.usable_password }}</fieldset> {% if form.usable_password.help_text %} <div class="help"{% if form.usable_password.id_for_label %} id="{{ form.usable_password.id_for_label }}_helptext"{% endif %}> <p>{{ form.usable_password.help_text|safe }}</p> diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index 9c9b31965a..aa8d94b41d 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -11,13 +11,14 @@ <div class="form-row{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}"> {% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %} {% for field in line %} + {% if field.is_fieldset %}<fieldset class="flex-container"{% if field.field.help_text %} aria-describedby="{{ field.field.id_for_label }}_helptext"{% endif %}>{{ field.label_tag }}{% endif %} <div> {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} <div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% endif %}{% if field.is_checkbox %} checkbox-row{% endif %}"> {% if field.is_checkbox %} - {{ field.field }}{{ field.label_tag }} + {{ field.field }}{% if not field.is_fieldset %}{{ field.label_tag }}{% endif %} {% else %} - {{ field.label_tag }} + {% if not field.is_fieldset %}{{ field.label_tag }}{% endif %} {% if field.is_readonly %} <div class="readonly">{{ field.contents }}</div> {% else %} @@ -31,6 +32,7 @@ </div> {% endif %} </div> + {% if field.is_fieldset %}</fieldset>{% endif %} {% endfor %} {% if not line.fields|length == 1 %}</div>{% endif %} </div> diff --git a/django/contrib/admin/templates/admin/widgets/split_datetime.html b/django/contrib/admin/templates/admin/widgets/split_datetime.html index 7fc7bf6833..ac73fa361f 100644 --- a/django/contrib/admin/templates/admin/widgets/split_datetime.html +++ b/django/contrib/admin/templates/admin/widgets/split_datetime.html @@ -1,4 +1,4 @@ <p class="datetime"> - {{ date_label }} {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br> - {{ time_label }} {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %} + <label {% if widget.attrs.id %}for="{{ widget.subwidgets.0.attrs.id }}"{% endif %}>{{ date_label }}</label> {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br> + <label {% if widget.attrs.id %}for="{{ widget.subwidgets.1.attrs.id }}"{% endif %}>{{ time_label }}</label> {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %} </p> diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index a601fc2667..124f3307af 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -49,7 +49,16 @@ class FilteredSelectMultiple(forms.SelectMultiple): return context -class BaseAdminDateWidget(forms.DateInput): +class DateTimeWidgetContextMixin: + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context["widget"]["attrs"][ + "aria-describedby" + ] = f"id_{name}_timezone_warning_helptext" + return context + + +class BaseAdminDateWidget(DateTimeWidgetContextMixin, forms.DateInput): class Media: js = [ "admin/js/calendar.js", @@ -65,7 +74,7 @@ class AdminDateWidget(BaseAdminDateWidget): template_name = "admin/widgets/date.html" -class BaseAdminTimeWidget(forms.TimeInput): +class BaseAdminTimeWidget(DateTimeWidgetContextMixin, forms.TimeInput): class Media: js = [ "admin/js/calendar.js", @@ -98,8 +107,13 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget): context = super().get_context(name, value, attrs) context["date_label"] = _("Date:") context["time_label"] = _("Time:") + for widget in context["widget"]["subwidgets"]: + widget["attrs"]["aria-describedby"] = f"id_{name}_timezone_warning_helptext" return context + def id_for_label(self, id_): + return id_ + class AdminRadioSelect(forms.RadioSelect): template_name = "admin/widgets/radio.html" @@ -282,6 +296,7 @@ class RelatedFieldWidgetWrapper(forms.Widget): self.can_view_related = not multiple and can_view_related # To check if the related object is registered with this AdminSite. self.admin_site = admin_site + self.use_fieldset = True def __deepcopy__(self, memo): obj = copy.copy(self) diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 5a25b66e9a..b77e57abce 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -530,6 +530,7 @@ class ClearableFileInput(FileInput): input_text = _("Change") template_name = "django/forms/widgets/clearable_file_input.html" checked = False + use_fieldset = True def clear_checkbox_name(self, name): """ |
