diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index 80ae6eb2e1..f0fcade41c 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -631,7 +631,7 @@ input[type="submit"], button { background-position: 0 0; } - .active.selector-remove:focus, .active.selector-remove:hover { + :enabled.selector-remove:focus, :enabled.selector-remove:hover { background-position: 0 -24px; } @@ -639,7 +639,7 @@ input[type="submit"], button { background-position: 0 -48px; } - .active.selector-add:focus, .active.selector-add:hover { + :enabled.selector-add:focus, :enabled.selector-add:hover { background-position: 0 -72px; } diff --git a/django/contrib/admin/static/admin/css/responsive_rtl.css b/django/contrib/admin/static/admin/css/responsive_rtl.css index 83380ab69d..5e8f5c5943 100644 --- a/django/contrib/admin/static/admin/css/responsive_rtl.css +++ b/django/contrib/admin/static/admin/css/responsive_rtl.css @@ -75,7 +75,7 @@ background-position: 0 0; } - [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover { + [dir="rtl"] :enabled.selector-remove:focus, :enabled.selector-remove:hover { background-position: 0 -24px; } @@ -83,7 +83,7 @@ background-position: 0 -48px; } - [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover { + [dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover { background-position: 0 -72px; } } diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css index 3ee008939e..a2556d0478 100644 --- a/django/contrib/admin/static/admin/css/rtl.css +++ b/django/contrib/admin/static/admin/css/rtl.css @@ -224,7 +224,7 @@ fieldset .fieldBox { background-size: 24px auto; } -.active.selector-add:focus, .active.selector-add:hover { +:enabled.selector-add:focus, :enabled.selector-add:hover { background-position: 0 -120px; } @@ -233,7 +233,7 @@ fieldset .fieldBox { background-size: 24px auto; } -.active.selector-remove:focus, .active.selector-remove:hover { +:enabled.selector-remove:focus, :enabled.selector-remove:hover { background-position: 0 -168px; } @@ -241,7 +241,7 @@ fieldset .fieldBox { background: url(../img/selector-icons.svg) right -128px no-repeat; } -.active.selector-chooseall:focus, .active.selector-chooseall:hover { +:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover { background-position: 100% -144px; } @@ -249,7 +249,7 @@ fieldset .fieldBox { background: url(../img/selector-icons.svg) 0 -160px no-repeat; } -.active.selector-clearall:focus, .active.selector-clearall:hover { +:enabled.selector-clearall:focus, :enabled.selector-clearall:hover { background-position: 0 -176px; } diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 2784cb4b82..eb555c8baa 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -129,11 +129,11 @@ border: none; } -.active.selector-add, .active.selector-remove { +:enabled.selector-add, :enabled.selector-remove { opacity: 1; } -.active.selector-add:hover, .active.selector-remove:hover { +:enabled.selector-add:hover, :enabled.selector-remove:hover { cursor: pointer; } @@ -142,7 +142,7 @@ background-size: 24px auto; } -.active.selector-add:focus, .active.selector-add:hover { +:enabled.selector-add:focus, :enabled.selector-add:hover { background-position: 0 -168px; } @@ -151,7 +151,7 @@ background-size: 24px auto; } -.active.selector-remove:focus, .active.selector-remove:hover { +:enabled.selector-remove:focus, :enabled.selector-remove:hover { background-position: 0 -120px; } @@ -169,16 +169,16 @@ border: none; } -.active.selector-chooseall:focus, .active.selector-clearall:focus, -.active.selector-chooseall:hover, .active.selector-clearall:hover { +:enabled.selector-chooseall:focus, :enabled.selector-clearall:focus, +:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover { color: var(--link-fg); } -.active.selector-chooseall, .active.selector-clearall { +:enabled.selector-chooseall, :enabled.selector-clearall { opacity: 1; } -.active.selector-chooseall:hover, .active.selector-clearall:hover { +:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover { cursor: pointer; } @@ -188,7 +188,7 @@ cursor: default; } -.active.selector-chooseall:focus, .active.selector-chooseall:hover { +:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover { background-position: 100% -176px; } @@ -198,7 +198,7 @@ cursor: default; } -.active.selector-clearall:focus, .active.selector-clearall:hover { +:enabled.selector-clearall:focus, :enabled.selector-clearall:hover { background-position: 0 -144px; } @@ -252,12 +252,12 @@ cursor: default; } -.stacked .active.selector-add { +.stacked :enabled.selector-add { background-position: 0 -48px; cursor: pointer; } -.stacked .active.selector-add:focus, .stacked .active.selector-add:hover { +.stacked :enabled.selector-add:focus, .stacked :enabled.selector-add:hover { background-position: 0 -72px; cursor: pointer; } @@ -268,12 +268,12 @@ cursor: default; } -.stacked .active.selector-remove { +.stacked :enabled.selector-remove { background-position: 0 0px; cursor: pointer; } -.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover { +.stacked :enabled.selector-remove:focus, .stacked :enabled.selector-remove:hover { background-position: 0 -24px; cursor: pointer; } diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js index 7f0cfef8c9..addb7ec728 100644 --- a/django/contrib/admin/static/admin/js/SelectFilter2.js +++ b/django/contrib/admin/static/admin/js/SelectFilter2.js @@ -149,7 +149,7 @@ Requires core.js and SelectBox.js. // Set up the JavaScript event handlers for the select box filter interface const move_selection = function(e, elem, move_func, from, to) { - if (elem.classList.contains('active')) { + if (!elem.hasAttribute('disabled')) { move_func(from, to); SelectFilter.refresh_icons(field_id); SelectFilter.refresh_filtered_selects(field_id); @@ -248,13 +248,12 @@ Requires core.js and SelectBox.js. refresh_icons: function(field_id) { 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').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').classList.toggle('active', from.querySelector('option')); - document.getElementById(field_id + '_remove_all').classList.toggle('active', to.querySelector('option')); - SelectFilter.refresh_filtered_warning(field_id); + // Disabled if no items are selected. + document.getElementById(field_id + '_add').disabled = !SelectFilter.any_selected(from); + document.getElementById(field_id + '_remove').disabled = !SelectFilter.any_selected(to); + // Disabled if the corresponding box is empty. + document.getElementById(field_id + '_add_all').disabled = !from.querySelector('option'); + document.getElementById(field_id + '_remove_all').disabled = !to.querySelector('option'); }, filter_key_press: function(event, field_id, source, target) { const source_box = document.getElementById(field_id + source); diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index 3810c359e5..636a6ffdf2 100644 --- a/django/contrib/admin/tests.py +++ b/django/contrib/admin/tests.py @@ -218,19 +218,16 @@ class AdminSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase): """ self._assertOptionsValues("%s > option:checked" % selector, values) - def has_css_class(self, selector, klass): + def is_disabled(self, selector): """ - Return True if the element identified by `selector` has the CSS class - `klass`. + Return True if the element identified by `selector` has the `disabled` + attribute. """ from selenium.webdriver.common.by import By return ( - self.selenium.find_element( - By.CSS_SELECTOR, - selector, + self.selenium.find_element(By.CSS_SELECTOR, selector).get_attribute( + "disabled" ) - .get_attribute("class") - .find(klass) - != -1 + == "true" ) diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 15e11a6d8f..0cf1324623 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1254,21 +1254,27 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase): self.arthur = Student.objects.create(name="Arthur") self.school = School.objects.create(name="School of Awesome") - def assertActiveButtons( - self, mode, field_name, choose, remove, choose_all=None, remove_all=None + def assertButtonsDisabled( + self, + mode, + field_name, + choose_btn_disabled=False, + remove_btn_disabled=False, + choose_all_btn_disabled=False, + remove_all_btn_disabled=False, ): choose_button = "#id_%s_add" % field_name choose_all_button = "#id_%s_add_all" % field_name remove_button = "#id_%s_remove" % field_name remove_all_button = "#id_%s_remove_all" % field_name - self.assertEqual(self.has_css_class(choose_button, "active"), choose) - self.assertEqual(self.has_css_class(remove_button, "active"), remove) + self.assertEqual(self.is_disabled(choose_button), choose_btn_disabled) + self.assertEqual(self.is_disabled(remove_button), remove_btn_disabled) if mode == "horizontal": self.assertEqual( - self.has_css_class(choose_all_button, "active"), choose_all + self.is_disabled(choose_all_button), choose_all_btn_disabled ) self.assertEqual( - self.has_css_class(remove_all_button, "active"), remove_all + self.is_disabled(remove_all_button), remove_all_btn_disabled ) def execute_basic_operations(self, mode, field_name): @@ -1296,7 +1302,14 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase): ], ) self.assertSelectOptions(to_box, [str(self.lisa.id), str(self.peter.id)]) - self.assertActiveButtons(mode, field_name, False, False, True, True) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=True, + remove_btn_disabled=True, + choose_all_btn_disabled=False, + remove_all_btn_disabled=False, + ) # Click 'Choose all' -------------------------------------------------- if mode == "horizontal": @@ -1323,7 +1336,14 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase): str(self.john.id), ], ) - self.assertActiveButtons(mode, field_name, False, False, False, True) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=True, + remove_btn_disabled=True, + choose_all_btn_disabled=True, + remove_all_btn_disabled=False, + ) # Click 'Remove all' -------------------------------------------------- if mode == "horizontal": @@ -1350,7 +1370,14 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase): ], ) self.assertSelectOptions(to_box, []) - self.assertActiveButtons(mode, field_name, False, False, True, False) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=True, + remove_btn_disabled=True, + choose_all_btn_disabled=False, + remove_all_btn_disabled=True, + ) # Choose some options ------------------------------------------------ from_lisa_select_option = self.selenium.find_element( @@ -1367,9 +1394,23 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase): self.select_option(from_box, str(self.jason.id)) self.select_option(from_box, str(self.bob.id)) self.select_option(from_box, str(self.john.id)) - self.assertActiveButtons(mode, field_name, True, False, True, False) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=False, + remove_btn_disabled=True, + choose_all_btn_disabled=False, + remove_all_btn_disabled=True, + ) self.selenium.find_element(By.ID, choose_button).click() - self.assertActiveButtons(mode, field_name, False, False, True, True) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=True, + remove_btn_disabled=True, + choose_all_btn_disabled=False, + remove_all_btn_disabled=False, + ) self.assertSelectOptions( from_box, @@ -1402,9 +1443,23 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase): # Remove some options ------------------------------------------------- self.select_option(to_box, str(self.lisa.id)) self.select_option(to_box, str(self.bob.id)) - self.assertActiveButtons(mode, field_name, False, True, True, True) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=True, + remove_btn_disabled=False, + choose_all_btn_disabled=False, + remove_all_btn_disabled=False, + ) self.selenium.find_element(By.ID, remove_button).click() - self.assertActiveButtons(mode, field_name, False, False, True, True) + self.assertButtonsDisabled( + mode, + field_name, + choose_btn_disabled=True, + remove_btn_disabled=True, + choose_all_btn_disabled=False, + remove_all_btn_disabled=False, + ) self.assertSelectOptions( from_box,