Skip to content

Commit

Permalink
Fixed #34619 -- Associated FilteredSelectMultiple elements to their l…
Browse files Browse the repository at this point in the history
…abel and help text.
  • Loading branch information
GappleBee authored and sarahboyce committed Nov 20, 2024
1 parent f60d5e4 commit 857b104
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 108 deletions.
3 changes: 2 additions & 1 deletion django/contrib/admin/static/admin/css/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ input[type="submit"], button {
background-position: 0 -80px;
}

a.selector-chooseall, a.selector-clearall {
.selector-chooseall, .selector-clearall {
align-self: center;
}

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

.related-widget-wrapper .selector {
order: 1;
flex: 1 0 auto;
}

.related-widget-wrapper > a {
Expand Down
8 changes: 4 additions & 4 deletions django/contrib/admin/static/admin/css/rtl.css
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,19 @@ fieldset .fieldBox {
background-position: 0 -112px;
}

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

a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
background-position: 100% -144px;
}

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

a.active.selector-clearall:focus, a.active.selector-clearall:hover {
.active.selector-clearall:focus, .active.selector-clearall:hover {
background-position: 0 -176px;
}

Expand Down
39 changes: 25 additions & 14 deletions django/contrib/admin/static/admin/css/widgets.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

.selector {
display: flex;
flex-grow: 1;
flex: 1;
gap: 0 10px;
}

Expand All @@ -14,17 +14,20 @@
}

.selector-available, .selector-chosen {
text-align: center;
display: flex;
flex-direction: column;
flex: 1 1;
}

.selector-available h2, .selector-chosen h2 {
.selector-available-title, .selector-chosen-title {
border: 1px solid var(--border-color);
border-radius: 4px 4px 0 0;
}

.selector .helptext {
font-size: 0.6875rem;
}

.selector-chosen .list-footer-display {
border: 1px solid var(--border-color);
border-top: none;
Expand All @@ -40,14 +43,20 @@
color: var(--breadcrumbs-fg);
}

.selector-chosen h2 {
.selector-chosen-title {
background: var(--secondary);
color: var(--header-link-color);
padding: 8px;
}

.selector .selector-available h2 {
.selector-chosen-title label {
color: var(--header-link-color);
}

.selector-available-title {
background: var(--darkened-bg);
color: var(--body-quiet-color);
padding: 8px;
}

.selector .selector-filter {
Expand Down Expand Up @@ -121,6 +130,7 @@
overflow: hidden;
cursor: default;
opacity: 0.55;
border: none;
}

.active.selector-add, .active.selector-remove {
Expand All @@ -147,7 +157,7 @@
background-position: 0 -80px;
}

a.selector-chooseall, a.selector-clearall {
.selector-chooseall, .selector-clearall {
display: inline-block;
height: 16px;
text-align: left;
Expand All @@ -158,38 +168,39 @@ a.selector-chooseall, a.selector-clearall {
color: var(--body-quiet-color);
text-decoration: none;
opacity: 0.55;
border: none;
}

a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
.active.selector-chooseall:focus, .active.selector-clearall:focus,
.active.selector-chooseall:hover, .active.selector-clearall:hover {
color: var(--link-fg);
}

a.active.selector-chooseall, a.active.selector-clearall {
.active.selector-chooseall, .active.selector-clearall {
opacity: 1;
}

a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
.active.selector-chooseall:hover, .active.selector-clearall:hover {
cursor: pointer;
}

a.selector-chooseall {
.selector-chooseall {
padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default;
}

a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
background-position: 100% -176px;
}

a.selector-clearall {
.selector-clearall {
padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default;
}

a.active.selector-clearall:focus, a.active.selector-clearall:hover {
.active.selector-clearall:focus, .active.selector-clearall:hover {
background-position: 0 -144px;
}

Expand Down
106 changes: 64 additions & 42 deletions django/contrib/admin/static/admin/js/SelectFilter2.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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');

for (const p of from_box.parentNode.getElementsByTagName('p')) {
if (p.classList.contains("info")) {
Expand All @@ -38,18 +39,15 @@ Requires core.js and SelectBox.js.
// <div class="selector-available">
const selector_available = quickElement('div', selector_div);
selector_available.className = 'selector-available';
const title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
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(
'span', title_available, '',
'class', 'help help-tooltip help-icon',
'title', interpolate(
gettext(
'This is the list of available %s. You may choose some by ' +
'selecting them in the box below and then clicking the ' +
'"Choose" arrow between the two boxes.'
),
[field_name]
)
'p',
selector_available_title,
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
'class', 'helptext'
);

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

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

selector_available.appendChild(from_box);
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');
choose_all.className = 'selector-chooseall';
const choose_all = quickElement(
'button',
selector_available,
interpolate(gettext('Choose all %s'), [field_name]),
'id', field_id + '_add_all',
'class', 'selector-chooseall'
);

// <ul class="selector-chooser">
const selector_chooser = quickElement('ul', selector_div);
selector_chooser.className = 'selector-chooser';
const add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
add_link.className = 'selector-add';
const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
remove_link.className = 'selector-remove';
const add_button = quickElement(
'button',
quickElement('li', selector_chooser),
interpolate(gettext('Choose selected %s'), [field_name]),
'id', field_id + '_add',
'class', 'selector-add'
);
const remove_button = quickElement(
'button',
quickElement('li', selector_chooser),
interpolate(gettext('Remove selected chosen %s'), [field_name]),
'id', field_id + '_remove',
'class', 'selector-remove'
);

// <div class="selector-chosen">
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
selector_chosen.className = 'selector-chosen';
const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
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(
'span', title_chosen, '',
'class', 'help help-tooltip help-icon',
'title', interpolate(
gettext(
'This is the list of chosen %s. You may remove some by ' +
'selecting them in the box below and then clicking the ' +
'"Remove" arrow between the two boxes.'
),
[field_name]
)
'p',
selector_chosen_title,
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
'class', 'helptext'
);

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

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

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

const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name);
to_box.className = 'filtered';

quickElement(
'select',
selector_chosen,
'',
'id', field_id + '_to',
'multiple', '',
'size', from_box.size,
'name', from_box.name,
'aria-labelledby', field_id + '_to_title',
'class', 'filtered'
);
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');

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');
clear_all.className = 'selector-clearall';
const clear_all = quickElement(
'button',
selector_chosen,
interpolate(gettext('Remove all %s'), [field_name]),
'id', field_id + '_remove_all',
'class', 'selector-clearall'
);

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

Expand All @@ -138,10 +160,10 @@ Requires core.js and SelectBox.js.
choose_all.addEventListener('click', function(e) {
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
});
add_link.addEventListener('click', function(e) {
add_button.addEventListener('click', function(e) {
move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
});
remove_link.addEventListener('click', function(e) {
remove_button.addEventListener('click', function(e) {
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
});
clear_all.addEventListener('click', function(e) {
Expand Down Expand Up @@ -227,11 +249,11 @@ Requires core.js and SelectBox.js.
const from = document.getElementById(field_id + '_from');
const to = document.getElementById(field_id + '_to');
// Active if at least one item is selected
document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from));
document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to));
document.getElementById(field_id + '_add').classList.toggle('active', SelectFilter.any_selected(from));
document.getElementById(field_id + '_remove').classList.toggle('active', SelectFilter.any_selected(to));
// Active if the corresponding box isn't empty
document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option'));
document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option'));
document.getElementById(field_id + '_add_all').classList.toggle('active', from.querySelector('option'));
document.getElementById(field_id + '_remove_all').classList.toggle('active', to.querySelector('option'));
SelectFilter.refresh_filtered_warning(field_id);
},
filter_key_press: function(event, field_id, source, target) {
Expand Down
24 changes: 18 additions & 6 deletions js_tests/admin/SelectFilter2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,25 @@ QUnit.test('init', function(assert) {
SelectFilter.init('id', 'things', 0);
assert.equal($('#test').children().first().prop("tagName"), "DIV");
assert.equal($('#test').children().first().attr("class"), "selector");
assert.equal($('.selector-available h2').text().trim(), "Available things");
assert.equal($('.selector-chosen h2').text().trim(), "Chosen things");
assert.equal($('.selector-available label').text().trim(), "Available things");
assert.equal($('.selector-chosen label').text().trim(), "Chosen things");
assert.equal($('.selector-chosen select')[0].getAttribute('multiple'), '');
assert.equal($('.selector-chooseall').text(), "Choose all");
assert.equal($('.selector-add').text(), "Choose");
assert.equal($('.selector-remove').text(), "Remove");
assert.equal($('.selector-clearall').text(), "Remove all");
assert.equal($('.selector-chooseall').text(), "Choose all things");
assert.equal($('.selector-chooseall').prop("tagName"), "BUTTON");
assert.equal($('.selector-add').text(), "Choose selected things");
assert.equal($('.selector-add').prop("tagName"), "BUTTON");
assert.equal($('.selector-remove').text(), "Remove selected chosen things");
assert.equal($('.selector-remove').prop("tagName"), "BUTTON");
assert.equal($('.selector-clearall').text(), "Remove all things");
assert.equal($('.selector-clearall').prop("tagName"), "BUTTON");
assert.equal($('.selector-available .filtered').attr("aria-labelledby"), "id_from_title");
assert.equal($('.selector-available .selector-available-title label').text(), "Available things ");
assert.equal($('.selector-available .selector-available-title .helptext').text(), 'Choose things by selecting them and then select the "Choose" arrow button.');
assert.equal($('.selector-chosen .filtered').attr("aria-labelledby"), "id_to_title");
assert.equal($('.selector-chosen .selector-chosen-title label').text(), "Chosen things ");
assert.equal($('.selector-chosen .selector-chosen-title .helptext').text(), 'Remove things by selecting them and then select the "Remove" arrow button.');
assert.equal($('.selector-filter label .help-tooltip')[0].getAttribute("aria-label"), "Type into this box to filter down the list of available things.");
assert.equal($('.selector-filter label .help-tooltip')[1].getAttribute("aria-label"), "Type into this box to filter down the list of selected things.");
});

QUnit.test('filtering available options', function(assert) {
Expand Down
Loading

0 comments on commit 857b104

Please sign in to comment.