Skip to content

Commit 857b104

Browse files
GappleBeesarahboyce
authored andcommitted
Fixed #34619 -- Associated FilteredSelectMultiple elements to their label and help text.
1 parent f60d5e4 commit 857b104

File tree

9 files changed

+168
-108
lines changed

9 files changed

+168
-108
lines changed

django/contrib/admin/static/admin/css/responsive.css

+2-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ input[type="submit"], button {
299299
background-position: 0 -80px;
300300
}
301301

302-
a.selector-chooseall, a.selector-clearall {
302+
.selector-chooseall, .selector-clearall {
303303
align-self: center;
304304
}
305305

@@ -649,6 +649,7 @@ input[type="submit"], button {
649649

650650
.related-widget-wrapper .selector {
651651
order: 1;
652+
flex: 1 0 auto;
652653
}
653654

654655
.related-widget-wrapper > a {

django/contrib/admin/static/admin/css/rtl.css

+4-4
Original file line numberDiff line numberDiff line change
@@ -235,19 +235,19 @@ fieldset .fieldBox {
235235
background-position: 0 -112px;
236236
}
237237

238-
a.selector-chooseall {
238+
.selector-chooseall {
239239
background: url(../img/selector-icons.svg) right -128px no-repeat;
240240
}
241241

242-
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
242+
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
243243
background-position: 100% -144px;
244244
}
245245

246-
a.selector-clearall {
246+
.selector-clearall {
247247
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
248248
}
249249

250-
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
250+
.active.selector-clearall:focus, .active.selector-clearall:hover {
251251
background-position: 0 -176px;
252252
}
253253

django/contrib/admin/static/admin/css/widgets.css

+25-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
.selector {
44
display: flex;
5-
flex-grow: 1;
5+
flex: 1;
66
gap: 0 10px;
77
}
88

@@ -14,17 +14,20 @@
1414
}
1515

1616
.selector-available, .selector-chosen {
17-
text-align: center;
1817
display: flex;
1918
flex-direction: column;
2019
flex: 1 1;
2120
}
2221

23-
.selector-available h2, .selector-chosen h2 {
22+
.selector-available-title, .selector-chosen-title {
2423
border: 1px solid var(--border-color);
2524
border-radius: 4px 4px 0 0;
2625
}
2726

27+
.selector .helptext {
28+
font-size: 0.6875rem;
29+
}
30+
2831
.selector-chosen .list-footer-display {
2932
border: 1px solid var(--border-color);
3033
border-top: none;
@@ -40,14 +43,20 @@
4043
color: var(--breadcrumbs-fg);
4144
}
4245

43-
.selector-chosen h2 {
46+
.selector-chosen-title {
4447
background: var(--secondary);
4548
color: var(--header-link-color);
49+
padding: 8px;
4650
}
4751

48-
.selector .selector-available h2 {
52+
.selector-chosen-title label {
53+
color: var(--header-link-color);
54+
}
55+
56+
.selector-available-title {
4957
background: var(--darkened-bg);
5058
color: var(--body-quiet-color);
59+
padding: 8px;
5160
}
5261

5362
.selector .selector-filter {
@@ -121,6 +130,7 @@
121130
overflow: hidden;
122131
cursor: default;
123132
opacity: 0.55;
133+
border: none;
124134
}
125135

126136
.active.selector-add, .active.selector-remove {
@@ -147,7 +157,7 @@
147157
background-position: 0 -80px;
148158
}
149159

150-
a.selector-chooseall, a.selector-clearall {
160+
.selector-chooseall, .selector-clearall {
151161
display: inline-block;
152162
height: 16px;
153163
text-align: left;
@@ -158,38 +168,39 @@ a.selector-chooseall, a.selector-clearall {
158168
color: var(--body-quiet-color);
159169
text-decoration: none;
160170
opacity: 0.55;
171+
border: none;
161172
}
162173

163-
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
164-
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
174+
.active.selector-chooseall:focus, .active.selector-clearall:focus,
175+
.active.selector-chooseall:hover, .active.selector-clearall:hover {
165176
color: var(--link-fg);
166177
}
167178

168-
a.active.selector-chooseall, a.active.selector-clearall {
179+
.active.selector-chooseall, .active.selector-clearall {
169180
opacity: 1;
170181
}
171182

172-
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
183+
.active.selector-chooseall:hover, .active.selector-clearall:hover {
173184
cursor: pointer;
174185
}
175186

176-
a.selector-chooseall {
187+
.selector-chooseall {
177188
padding: 0 18px 0 0;
178189
background: url(../img/selector-icons.svg) right -160px no-repeat;
179190
cursor: default;
180191
}
181192

182-
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
193+
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
183194
background-position: 100% -176px;
184195
}
185196

186-
a.selector-clearall {
197+
.selector-clearall {
187198
padding: 0 0 0 18px;
188199
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
189200
cursor: default;
190201
}
191202

192-
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
203+
.active.selector-clearall:focus, .active.selector-clearall:hover {
193204
background-position: 0 -144px;
194205
}
195206

django/contrib/admin/static/admin/js/SelectFilter2.js

+64-42
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Requires core.js and SelectBox.js.
1515
const from_box = document.getElementById(field_id);
1616
from_box.id += '_from'; // change its ID
1717
from_box.className = 'filtered';
18+
from_box.setAttribute('aria-labelledby', field_id + '_from_title');
1819

1920
for (const p of from_box.parentNode.getElementsByTagName('p')) {
2021
if (p.classList.contains("info")) {
@@ -38,18 +39,15 @@ Requires core.js and SelectBox.js.
3839
// <div class="selector-available">
3940
const selector_available = quickElement('div', selector_div);
4041
selector_available.className = 'selector-available';
41-
const title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
42+
const selector_available_title = quickElement('div', selector_available);
43+
selector_available_title.id = field_id + '_from_title';
44+
selector_available_title.className = 'selector-available-title';
45+
quickElement('label', selector_available_title, interpolate(gettext('Available %s') + ' ', [field_name]), 'for', field_id + '_from');
4246
quickElement(
43-
'span', title_available, '',
44-
'class', 'help help-tooltip help-icon',
45-
'title', interpolate(
46-
gettext(
47-
'This is the list of available %s. You may choose some by ' +
48-
'selecting them in the box below and then clicking the ' +
49-
'"Choose" arrow between the two boxes.'
50-
),
51-
[field_name]
52-
)
47+
'p',
48+
selector_available_title,
49+
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
50+
'class', 'helptext'
5351
);
5452

5553
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
@@ -60,7 +58,7 @@ Requires core.js and SelectBox.js.
6058
quickElement(
6159
'span', search_filter_label, '',
6260
'class', 'help-tooltip search-label-icon',
63-
'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
61+
'aria-label', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
6462
);
6563

6664
filter_p.appendChild(document.createTextNode(' '));
@@ -69,32 +67,44 @@ Requires core.js and SelectBox.js.
6967
filter_input.id = field_id + '_input';
7068

7169
selector_available.appendChild(from_box);
72-
const choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link');
73-
choose_all.className = 'selector-chooseall';
70+
const choose_all = quickElement(
71+
'button',
72+
selector_available,
73+
interpolate(gettext('Choose all %s'), [field_name]),
74+
'id', field_id + '_add_all',
75+
'class', 'selector-chooseall'
76+
);
7477

7578
// <ul class="selector-chooser">
7679
const selector_chooser = quickElement('ul', selector_div);
7780
selector_chooser.className = 'selector-chooser';
78-
const add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
79-
add_link.className = 'selector-add';
80-
const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
81-
remove_link.className = 'selector-remove';
81+
const add_button = quickElement(
82+
'button',
83+
quickElement('li', selector_chooser),
84+
interpolate(gettext('Choose selected %s'), [field_name]),
85+
'id', field_id + '_add',
86+
'class', 'selector-add'
87+
);
88+
const remove_button = quickElement(
89+
'button',
90+
quickElement('li', selector_chooser),
91+
interpolate(gettext('Remove selected chosen %s'), [field_name]),
92+
'id', field_id + '_remove',
93+
'class', 'selector-remove'
94+
);
8295

8396
// <div class="selector-chosen">
8497
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
8598
selector_chosen.className = 'selector-chosen';
86-
const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
99+
const selector_chosen_title = quickElement('div', selector_chosen);
100+
selector_chosen_title.className = 'selector-chosen-title';
101+
selector_chosen_title.id = field_id + '_to_title';
102+
quickElement('label', selector_chosen_title, interpolate(gettext('Chosen %s') + ' ', [field_name]), 'for', field_id + '_to');
87103
quickElement(
88-
'span', title_chosen, '',
89-
'class', 'help help-tooltip help-icon',
90-
'title', interpolate(
91-
gettext(
92-
'This is the list of chosen %s. You may remove some by ' +
93-
'selecting them in the box below and then clicking the ' +
94-
'"Remove" arrow between the two boxes.'
95-
),
96-
[field_name]
97-
)
104+
'p',
105+
selector_chosen_title,
106+
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
107+
'class', 'helptext'
98108
);
99109

100110
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
@@ -105,23 +115,35 @@ Requires core.js and SelectBox.js.
105115
quickElement(
106116
'span', search_filter_selected_label, '',
107117
'class', 'help-tooltip search-label-icon',
108-
'title', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
118+
'aria-label', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
109119
);
110120

111121
filter_selected_p.appendChild(document.createTextNode(' '));
112122

113123
const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
114124
filter_selected_input.id = field_id + '_selected_input';
115125

116-
const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name);
117-
to_box.className = 'filtered';
118-
126+
quickElement(
127+
'select',
128+
selector_chosen,
129+
'',
130+
'id', field_id + '_to',
131+
'multiple', '',
132+
'size', from_box.size,
133+
'name', from_box.name,
134+
'aria-labelledby', field_id + '_to_title',
135+
'class', 'filtered'
136+
);
119137
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
120138
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
121139
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
122-
123-
const clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
124-
clear_all.className = 'selector-clearall';
140+
const clear_all = quickElement(
141+
'button',
142+
selector_chosen,
143+
interpolate(gettext('Remove all %s'), [field_name]),
144+
'id', field_id + '_remove_all',
145+
'class', 'selector-clearall'
146+
);
125147

126148
from_box.name = from_box.name + '_old';
127149

@@ -138,10 +160,10 @@ Requires core.js and SelectBox.js.
138160
choose_all.addEventListener('click', function(e) {
139161
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
140162
});
141-
add_link.addEventListener('click', function(e) {
163+
add_button.addEventListener('click', function(e) {
142164
move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
143165
});
144-
remove_link.addEventListener('click', function(e) {
166+
remove_button.addEventListener('click', function(e) {
145167
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
146168
});
147169
clear_all.addEventListener('click', function(e) {
@@ -227,11 +249,11 @@ Requires core.js and SelectBox.js.
227249
const from = document.getElementById(field_id + '_from');
228250
const to = document.getElementById(field_id + '_to');
229251
// Active if at least one item is selected
230-
document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from));
231-
document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to));
252+
document.getElementById(field_id + '_add').classList.toggle('active', SelectFilter.any_selected(from));
253+
document.getElementById(field_id + '_remove').classList.toggle('active', SelectFilter.any_selected(to));
232254
// Active if the corresponding box isn't empty
233-
document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option'));
234-
document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option'));
255+
document.getElementById(field_id + '_add_all').classList.toggle('active', from.querySelector('option'));
256+
document.getElementById(field_id + '_remove_all').classList.toggle('active', to.querySelector('option'));
235257
SelectFilter.refresh_filtered_warning(field_id);
236258
},
237259
filter_key_press: function(event, field_id, source, target) {

js_tests/admin/SelectFilter2.test.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,25 @@ QUnit.test('init', function(assert) {
1212
SelectFilter.init('id', 'things', 0);
1313
assert.equal($('#test').children().first().prop("tagName"), "DIV");
1414
assert.equal($('#test').children().first().attr("class"), "selector");
15-
assert.equal($('.selector-available h2').text().trim(), "Available things");
16-
assert.equal($('.selector-chosen h2').text().trim(), "Chosen things");
15+
assert.equal($('.selector-available label').text().trim(), "Available things");
16+
assert.equal($('.selector-chosen label').text().trim(), "Chosen things");
1717
assert.equal($('.selector-chosen select')[0].getAttribute('multiple'), '');
18-
assert.equal($('.selector-chooseall').text(), "Choose all");
19-
assert.equal($('.selector-add').text(), "Choose");
20-
assert.equal($('.selector-remove').text(), "Remove");
21-
assert.equal($('.selector-clearall').text(), "Remove all");
18+
assert.equal($('.selector-chooseall').text(), "Choose all things");
19+
assert.equal($('.selector-chooseall').prop("tagName"), "BUTTON");
20+
assert.equal($('.selector-add').text(), "Choose selected things");
21+
assert.equal($('.selector-add').prop("tagName"), "BUTTON");
22+
assert.equal($('.selector-remove').text(), "Remove selected chosen things");
23+
assert.equal($('.selector-remove').prop("tagName"), "BUTTON");
24+
assert.equal($('.selector-clearall').text(), "Remove all things");
25+
assert.equal($('.selector-clearall').prop("tagName"), "BUTTON");
26+
assert.equal($('.selector-available .filtered').attr("aria-labelledby"), "id_from_title");
27+
assert.equal($('.selector-available .selector-available-title label').text(), "Available things ");
28+
assert.equal($('.selector-available .selector-available-title .helptext').text(), 'Choose things by selecting them and then select the "Choose" arrow button.');
29+
assert.equal($('.selector-chosen .filtered').attr("aria-labelledby"), "id_to_title");
30+
assert.equal($('.selector-chosen .selector-chosen-title label').text(), "Chosen things ");
31+
assert.equal($('.selector-chosen .selector-chosen-title .helptext').text(), 'Remove things by selecting them and then select the "Remove" arrow button.');
32+
assert.equal($('.selector-filter label .help-tooltip')[0].getAttribute("aria-label"), "Type into this box to filter down the list of available things.");
33+
assert.equal($('.selector-filter label .help-tooltip')[1].getAttribute("aria-label"), "Type into this box to filter down the list of selected things.");
2234
});
2335

2436
QUnit.test('filtering available options', function(assert) {

0 commit comments

Comments
 (0)