kopia lustrzana https://github.com/wagtail/wagtail
Replace ButtonSelect widgets with radio buttons
- Instead of a complex and non-accessible JS solution for filter selects, replace with simple radio select fields - Fixes #9838pull/9821/head
rodzic
38e39271ee
commit
ff7494bf79
|
@ -39,6 +39,7 @@ Changelog
|
|||
* Fix: Prevent matches from unrelated models from leaking into SQLite FTS searches (Matt Westcott)
|
||||
* Fix: Prevent duplicate addition of StreamField blocks with the new block picker (Deepam Priyadarshi)
|
||||
* Fix: Enable partial search on images and documents index view where available (Mng)
|
||||
* Fix: Adopt a no-JavaScript and more accessible solution for option selection in reporting, using HTML only `radio` input fields (Mehul Aggarwal)
|
||||
* Docs: Add code block to make it easier to understand contribution docs (Suyash Singh)
|
||||
* Docs: Add new "Icons" page for icons customisation and reuse across the admin interface (Coen van der Kamp)
|
||||
* Docs: Fix broken formatting for MultiFieldPanel / FieldRowPanel permission kwarg docs (Matt Westcott)
|
||||
|
|
|
@ -701,6 +701,7 @@ Contributors
|
|||
* Deepam Priyadarshi
|
||||
* Mng
|
||||
* George Sakkis
|
||||
* Mehul Aggarwal
|
||||
|
||||
Translators
|
||||
===========
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
.button-select {
|
||||
&__option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
|
||||
background-color: $color-white;
|
||||
border-color: $color-teal;
|
||||
color: $color-grey-1;
|
||||
|
||||
&--selected {
|
||||
background-color: $color-teal;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
background-color: SelectedItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-select .button-select__option {
|
||||
/* override default margin from horizontally-aligned buttons */
|
||||
margin-inline-start: 0;
|
||||
}
|
|
@ -155,7 +155,6 @@ These are classes for components.
|
|||
@import 'components/link.legacy';
|
||||
@import 'components/indicator';
|
||||
@import 'components/status-tag';
|
||||
@import 'components/button-select';
|
||||
@import 'components/skiplink';
|
||||
@import 'components/workflow-tasks';
|
||||
@import 'components/workflow-timeline';
|
||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'jquery';
|
|||
|
||||
import { coreControllerDefinitions } from '../../controllers';
|
||||
import { escapeHtml } from '../../utils/text';
|
||||
import { initButtonSelects } from '../../includes/initButtonSelects';
|
||||
import { initStimulus } from '../../includes/initStimulus';
|
||||
import { initTagField } from '../../includes/initTagField';
|
||||
import { initTooltips } from '../../includes/initTooltips';
|
||||
|
@ -487,6 +486,4 @@ $(document).ready(initDropDowns);
|
|||
wagtail.ui.initDropDowns = initDropDowns;
|
||||
wagtail.ui.DropDownController = DropDownController;
|
||||
|
||||
$(document).ready(initButtonSelects);
|
||||
|
||||
window.wagtail = wagtail;
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import { initButtonSelects } from './initButtonSelects';
|
||||
|
||||
// save our DOM elements to a variable
|
||||
const testElements = `
|
||||
<div class="button-select">
|
||||
<input type="hidden"/>
|
||||
<button class="button-select__option">
|
||||
All
|
||||
</button>
|
||||
<button class="button-select__option" value="in_progress">
|
||||
In Progress
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
describe('initButtonSelects', () => {
|
||||
const spy = jest.spyOn(document, 'addEventListener');
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing if there is no button-select container', () => {
|
||||
// Set up our document body
|
||||
document.body.innerHTML = `
|
||||
<div>
|
||||
<input type="hidden" />
|
||||
<button class="button-select__option" />
|
||||
</div>`;
|
||||
initButtonSelects();
|
||||
// no event listeners registered
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('there is a button-select container present', () => {
|
||||
it('should add class of button-select__option--selected to button-select__option when clicked', () => {
|
||||
document.body.innerHTML = testElements;
|
||||
initButtonSelects();
|
||||
document.querySelectorAll('.button-select__option').forEach((button) => {
|
||||
button.click();
|
||||
expect(button.classList.value).toContain(
|
||||
'button-select__option--selected',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove the class button-select__option--selected when button is not clicked', () => {
|
||||
document.body.innerHTML = testElements;
|
||||
initButtonSelects();
|
||||
document.querySelectorAll('.button-select__option').forEach((button) => {
|
||||
button.click();
|
||||
document
|
||||
.querySelector('.button-select')
|
||||
.querySelectorAll('.button-select__option--selected')
|
||||
.forEach((selectedButtonElement) => {
|
||||
selectedButtonElement.classList.remove(
|
||||
'button-select__option--selected',
|
||||
);
|
||||
});
|
||||
expect(button.classList.value).not.toContain(
|
||||
'button-select__option--selected',
|
||||
);
|
||||
});
|
||||
});
|
||||
it('add the value of the button clicked to the input value', () => {
|
||||
document.body.innerHTML = testElements;
|
||||
initButtonSelects();
|
||||
const inputElement = document.querySelector('input[type="hidden"]');
|
||||
// Checking that the input ellement has no value
|
||||
expect(inputElement.value).toBeFalsy();
|
||||
document.querySelectorAll('.button-select__option').forEach((button) => {
|
||||
button.click();
|
||||
expect(inputElement.value).toEqual(button.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
/**
|
||||
* Initialise button selectors
|
||||
*/
|
||||
const initButtonSelects = () => {
|
||||
document.querySelectorAll('.button-select').forEach((element) => {
|
||||
const inputElement = element.querySelector(
|
||||
'input[type="hidden"]',
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (!inputElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
element
|
||||
.querySelectorAll('.button-select__option')
|
||||
.forEach((buttonElement) => {
|
||||
buttonElement.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
inputElement.value = (buttonElement as HTMLButtonElement).value;
|
||||
|
||||
element
|
||||
.querySelectorAll('.button-select__option--selected')
|
||||
.forEach((selectedButtonElement) => {
|
||||
selectedButtonElement.classList.remove(
|
||||
'button-select__option--selected',
|
||||
);
|
||||
});
|
||||
|
||||
buttonElement.classList.add('button-select__option--selected');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export { initButtonSelects };
|
|
@ -53,6 +53,7 @@ Support for adding custom validation logic to StreamField blocks has been formal
|
|||
* Prevent matches from unrelated models from leaking into SQLite FTS searches (Matt Westcott)
|
||||
* Prevent duplicate addition of StreamField blocks with the new block picker (Deepam Priyadarshi)
|
||||
* Enable partial search on images and documents index view where available (Mng)
|
||||
* Adopt a no-JavaScript and more accessible solution for option selection in reporting, using HTML only `radio` input fields (Mehul Aggarwal)
|
||||
|
||||
### Documentation
|
||||
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import django_filters
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.widgets import SuffixedMultiWidget
|
||||
|
||||
from wagtail.admin.widgets import (
|
||||
AdminDateInput,
|
||||
BooleanButtonSelect,
|
||||
ButtonSelect,
|
||||
FilteredSelect,
|
||||
)
|
||||
from wagtail.admin.widgets import AdminDateInput, BooleanRadioSelect, FilteredSelect
|
||||
from wagtail.coreutils import get_content_type_label
|
||||
|
||||
|
||||
|
@ -96,7 +92,7 @@ class WagtailFilterSet(django_filters.FilterSet):
|
|||
filter_class, params = super().filter_for_lookup(field, lookup_type)
|
||||
|
||||
if filter_class == django_filters.ChoiceFilter:
|
||||
params.setdefault("widget", ButtonSelect)
|
||||
params.setdefault("widget", forms.RadioSelect)
|
||||
params.setdefault("empty_label", _("All"))
|
||||
|
||||
elif filter_class in [django_filters.DateFilter, django_filters.DateTimeFilter]:
|
||||
|
@ -106,7 +102,7 @@ class WagtailFilterSet(django_filters.FilterSet):
|
|||
params.setdefault("widget", DateRangePickerWidget)
|
||||
|
||||
elif filter_class == django_filters.BooleanFilter:
|
||||
params.setdefault("widget", BooleanButtonSelect)
|
||||
params.setdefault("widget", BooleanRadioSelect)
|
||||
|
||||
return filter_class, params
|
||||
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<div class="button-select">
|
||||
<input type="hidden" name="{{ widget.name }}" value="{{ widget.value.0 }}" {% include "django/forms/widgets/attrs.html" %}>
|
||||
|
||||
{% for group_name, group_choices, group_index in widget.optgroups %}
|
||||
{% if group_name %}
|
||||
<h4>{{ group_name }}</h4>
|
||||
{% endif %}
|
||||
{% for option in group_choices %}
|
||||
{% include option.template_name with widget=option %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<button class="button button-select__option{% if widget.attrs.selected %} button-select__option--selected{% endif %}" value="{{ widget.value|stringformat:'s' }}">{{ widget.label }}</button>
|
|
@ -1,6 +1,7 @@
|
|||
import datetime
|
||||
|
||||
import django_filters
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import CharField, Q
|
||||
|
@ -13,7 +14,6 @@ from wagtail.admin.filters import (
|
|||
WagtailFilterSet,
|
||||
)
|
||||
from wagtail.admin.utils import get_latest_str
|
||||
from wagtail.admin.widgets import ButtonSelect
|
||||
from wagtail.coreutils import get_content_type_label
|
||||
from wagtail.models import (
|
||||
Task,
|
||||
|
@ -61,7 +61,7 @@ class WorkflowReportFilterSet(WagtailFilterSet):
|
|||
method="filter_reviewable",
|
||||
choices=(("true", _("Awaiting my review")),),
|
||||
empty_label=_("All"),
|
||||
widget=ButtonSelect,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
requested_by = django_filters.ModelChoiceFilter(
|
||||
field_name="requested_by", queryset=get_requested_by_queryset
|
||||
|
@ -107,7 +107,7 @@ class WorkflowTasksReportFilterSet(WagtailFilterSet):
|
|||
method="filter_reviewable",
|
||||
choices=(("true", _("Awaiting my review")),),
|
||||
empty_label=_("All"),
|
||||
widget=ButtonSelect,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
def filter_reviewable(self, queryset, name, value):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from wagtail.admin.widgets.auto_height_text import * # NOQA
|
||||
from wagtail.admin.widgets.boolean_radio_select import * # NOQA
|
||||
from wagtail.admin.widgets.button import * # NOQA
|
||||
from wagtail.admin.widgets.button_select import * # NOQA
|
||||
from wagtail.admin.widgets.chooser import * # NOQA
|
||||
from wagtail.admin.widgets.datetime import * # NOQA
|
||||
from wagtail.admin.widgets.filtered_select import * # NOQA
|
||||
|
|
|
@ -2,20 +2,12 @@ from django import forms
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ButtonSelect(forms.Select):
|
||||
class BooleanRadioSelect(forms.RadioSelect):
|
||||
"""
|
||||
A select widget for fields with choices. Displays as a list of buttons.
|
||||
A radio select widget for boolean fields. Displays as three options; "All", "Yes" and "No".
|
||||
"""
|
||||
|
||||
input_type = "hidden"
|
||||
template_name = "wagtailadmin/widgets/button_select.html"
|
||||
option_template_name = "wagtailadmin/widgets/button_select_option.html"
|
||||
|
||||
|
||||
class BooleanButtonSelect(ButtonSelect):
|
||||
"""
|
||||
A select widget for boolean fields. Displays as three buttons. "All", "Yes" and "No".
|
||||
"""
|
||||
input_type = "radio"
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
choices = (
|
|
@ -1,8 +1,8 @@
|
|||
import django_filters
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin.filters import WagtailFilterSet
|
||||
from wagtail.admin.widgets import ButtonSelect
|
||||
from wagtail.contrib.redirects.models import Redirect
|
||||
from wagtail.models import Site
|
||||
|
||||
|
@ -16,7 +16,7 @@ class RedirectsReportFilterSet(WagtailFilterSet):
|
|||
(False, _("Temporary")),
|
||||
),
|
||||
empty_label=_("All"),
|
||||
widget=ButtonSelect,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
site = django_filters.ModelChoiceFilter(
|
||||
|
|
|
@ -539,7 +539,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="">All</button>',
|
||||
'<label for="id_country_code_0"><input type="radio" name="country_code" value="" id="id_country_code_0" checked>All</label>',
|
||||
html=True,
|
||||
)
|
||||
self.assertTemplateUsed(response, "wagtailadmin/shared/filters.html")
|
||||
|
@ -553,7 +553,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
self.assertNotContains(response, "There are 2 matches")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="">All</button>',
|
||||
'<label for="id_country_code_0"><input type="radio" name="country_code" value="" id="id_country_code_0" checked>All</label>',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -566,7 +566,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
self.assertNotContains(response, "There are 2 matches")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="">All</button>',
|
||||
'<label for="id_country_code_0"><input type="radio" name="country_code" value="" id="id_country_code_0" checked>All</label>',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -577,7 +577,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
self.assertContains(response, "Sorry, no filterable snippets match your query")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="PH">Philippines</button>',
|
||||
'<label for="id_country_code_2"><input type="radio" name="country_code" value="PH" id="id_country_code_2" checked>Philippines</label>',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -589,7 +589,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
self.assertContains(response, "There is 1 match")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="ID">Indonesia</button>',
|
||||
'<label for="id_country_code_1"><input type="radio" name="country_code" value="ID" id="id_country_code_1" checked>Indonesia</label>',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -600,7 +600,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
self.assertContains(response, "Sorry, no filterable snippets match your query")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="ID">Indonesia</button>',
|
||||
'<label for="id_country_code_1"><input type="radio" name="country_code" value="ID" id="id_country_code_1" checked>Indonesia</label>',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -612,7 +612,7 @@ class TestSnippetListViewWithFilterSet(WagtailTestUtils, TestCase):
|
|||
self.assertContains(response, "There is 1 match")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button class="button button-select__option button-select__option--selected" value="UK">United Kingdom</button>',
|
||||
'<label for="id_country_code_3"><input type="radio" name="country_code" value="UK" id="id_country_code_3" checked>United Kingdom</label>',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue