Skip to content

Commit

Permalink
Accessibility ergonomy filtering (DefectDojo#11634)
Browse files Browse the repository at this point in the history
* Added vertical alignment for filter and clear filter logic

* Apply clear filter logic

* Fix new line at the end of file

* Updated unit tests

* Removed forgotten console log

* Updated selector for integration test
  • Loading branch information
littlesvensson authored Jan 29, 2025
1 parent 9e8111a commit 0e91248
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 59 deletions.
2 changes: 1 addition & 1 deletion dojo/static/dojo/css/dojo.css
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,7 @@ div.custom-search-form {
}

.dojo-filter-set.form-inline .filter-form-control {
width: auto!important;
width: 100%!important;
vertical-align: middle;
}

Expand Down
11 changes: 8 additions & 3 deletions dojo/static/dojo/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,13 @@ function clear_form(form){
case 'radio':
this.checked = false;
break;
case 'select-multiple':
$(this).val(null).trigger('change');
case 'select-multiple':
// Clear all types of multiple select versions
if ($(this).hasClass('select2-hidden-accessible')) {
$(this).data('select2').$container.find(".select2-selection__choice").remove();
}
$(this).val(null).trigger('change');
break;
}
});
}
}
3 changes: 3 additions & 0 deletions dojo/templates/dojo/filter_js_snippet.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

$(function () {
$('.similar-filters select[multiple]').not('[data-tagulous]').each(function () {
// Removing bootstrap-select class and replacing with select2 to avoid library conflicts
// leading to visually unappealing dropdowns
$(this).removeClass('selectpicker').closest('.bootstrap-select').replaceWith($(this));
$(this).select2(
)
})
Expand Down
164 changes: 118 additions & 46 deletions dojo/templates/dojo/filter_snippet.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,54 +15,67 @@
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="filter-form-group">
{% for field in form.visible_fields %}
<div class="filter-form-input">
{{ field.errors }}
<label for="{{ field.auto_id }}" style="display: block;">
{{ field.label }}
{% if field.help_text %}
<i class="fa-solid fa-circle-question has-popover" data-trigger="hover" data-content="{{ field.help_text }}" data-placement="right" data-container="body">
</i>
<div class="container-fluid filter-form-group">
{% for field in form.visible_fields %}
<div class="col-lg-3 col-md-4 col-sm-6 col-12">
<div class="filter-form-input" style="min-width: 0;">
<label for="{{ field.auto_id }}" class="form-label mb-1" style="display: block;">
{{ field.label }}
{% if field.help_text %}
<i class="fa-solid fa-circle-question has-popover"
data-trigger="hover"
data-content="{{ field.help_text }}"
data-placement="right"
data-container="body">
</i>
{% endif %}
</label>
{% with placeholder="placeholder:"|add:field.label %}
{{ field|addcss:"class: form-control filter-form-control"|addcss:placeholder }}
{% endwith %}
</div>
</div>
{% endfor %}
</div>
<div class="container-fluid">
<div class="row mt-3">
<div class="col-12">
<div class="d-flex justify-content-end align-items-center">
{% if submit == 'report' %}
{% query_string_as_hidden %}
<button class="btn btn-secondary" name="_generate" type="submit">
<i class="fa-solid fa-file-lines"></i> Generate Report
</button>
{% else %}
<button id="apply" class="btn btn-secondary me-2">
<i class="fa-solid fa-filter"></i> Apply Filters
</button>
{% if clear_js %}
<a class="btn btn-outline-secondary me-2" href="#{{form_id}}" id="clear_js" role="button">
<i class="fa-solid fa-remove"></i> Clear Filters
</a>
{% else %}
<a class="btn btn-outline-secondary me-2" href="{{ clear_link|default:request.path }}" id="clear" role="button" >
<i class="fa-solid fa-remove"></i> Clear Filters
</a>
{% endif %}
{% if restart_link %}
<a href="{{ restart_link }}" id="restart" class="btn btn-secondary">
<i class="fa-solid fa-remove"></i> Restart
</a>
{% endif %}
</label>
{% with placeholder="placeholder:"|add:field.label %}
{{ field|addcss:"class: form-control filter-form-control"|addcss:placeholder }}
{% endwith %}
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% if submit == 'report' %}
{% query_string_as_hidden %}
<div class="inline-block" style="vertical-align: text-top">
<button class="btn btn-secondary" name="_generate" type="submit">
<i class="fa-solid fa-file-lines"></i> Generate Report
</button>
</div>
{% else %}
<div class="inline-block" style="vertical-align: text-top">
<button id="apply" class="btn btn-sm btn-secondary">
<i class="fa-solid fa-filter"></i> Apply Filters
</button>
&nbsp;
{% if clear_js %}
<a href="#{{form_id}}" id="clear_js" class="clear centered"> [Clear Filters] </a>
{% elif clear_link %}
<a href="{{ clear_link }}" id="clear" class="clear centered"> [Clear Filters] </a>
{% else %}
<a href="{{ request.path }}" id="clear" class="clear centered"> [Clear Filters] </a>
{% endif %}
{% if restart_link %}
<a href="{{ restart_link }}" id="clear" class="clear centered"> [Restart] </a>
{% endif %}
</div>
{% endif %}
</div>

</form>

</div>
<script>
$(document).ready(function() {
$(".filter-set>form").first().submit(function(event) {
const form = $(".filter-set>form");
$(form).first().submit(function(event) {
var formData = $(".filter-set>form").first().serializeArray();
var filteredFormData = formData.filter(function(item) {
// Remove null or empty values
Expand All @@ -81,11 +94,70 @@

// Append the new query parameters to the base URL
var newAction = baseUrl + "?" + queryParams.join("&");

// Append the query parameters to the action URL
var newAction = baseUrl + "?" + queryParams.join("&");
window.location.href = newAction;
event.preventDefault();
});
});
</script>

// Clear filter logic below should clear filters without reload and
// have the button disabled if no filters are active

const clearFilterLink = $("#clear, #clear_js");

const hasActiveFilters = () => {
return $(form)
.find(":input:not([type='hidden'])")
.filter(function () {
const value = $(this).val();

if ($(this).is("select[multiple]")) {
// Checking if value is an array and has non-empty elements at the same time
return Array.isArray(value) && value.some(v => v && v.trim() !== "");
} else if ($(this).is(":checkbox, :radio")) {
// Checkboxes and radio buttons
return $(this).prop("checked");
} else if (typeof value === "string") {
// Text inputs, textareas
return value.trim() !== "" && value !== "unknown" && value !== null;
} else {
// Other input types
return value !== null && value !== undefined;
}
}).length > 0;
};

const updateClearFiltersState = () => {
const filtersActive = hasActiveFilters();

if (clearFilterLink.length) {
clearFilterLink
.toggleClass("disabled", !filtersActive)
.toggleClass("btn-outline-secondary", !filtersActive)
.toggleClass("btn-secondary", filtersActive)
.attr("aria-disabled", !filtersActive)
.css("pointer-events", filtersActive ? "auto" : "none");
}
};

$(form).on("input change", updateClearFiltersState);

$(document).on('click', '#clear, #clear_js', function (event) {
event.preventDefault();
if ($(this).attr("aria-disabled") === "true") {
return;
}
const form = $(this).closest(".filter-set").find("form");
if (form.length) {
clear_form(form);
// Refresh some UI components to work have cleared form from all respective libraries
form.find("select.selectpicker").selectpicker("refresh");
form.find(".multi-tag-input").val('').trigger('change');
form.find("select.select2-hidden-accessible").val(null).trigger('change');
form.find(".select2-selection__choice").remove();
form.find(".select2-search__field").val('');
// Update the state for the clear filters button
updateClearFiltersState();

}
});
});
</script>
4 changes: 2 additions & 2 deletions tests/group_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_group_edit_name_and_global_role(self):
driver.find_element(By.ID, "id_name").clear()
driver.find_element(By.ID, "id_name").send_keys("Group Name")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed group is now available, proceed with opening the context menu and clicking 'Edit' button
driver.find_element(By.ID, "dropdownMenuGroup").click()
driver.find_element(By.ID, "editGroup").click()
Expand Down Expand Up @@ -139,7 +139,7 @@ def test_group_delete(self):
driver.find_element(By.ID, "id_name").clear()
driver.find_element(By.ID, "id_name").send_keys("Another Name")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed group is now available, proceed with clicking 'Delete' button
driver.find_element(By.ID, "dropdownMenuGroup").click()
driver.find_element(By.ID, "deleteGroup").click()
Expand Down
2 changes: 1 addition & 1 deletion tests/product_group_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def navigate_to_group_view(self):
driver.find_element(By.ID, "id_name").clear()
driver.find_element(By.ID, "id_name").send_keys("Group Name")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed group is now available, proceed with opening the context menu and clicking 'Edit' button
driver.find_element(By.ID, "dropdownMenuGroup").click()
driver.find_element(By.ID, "viewGroup").click()
Expand Down
2 changes: 1 addition & 1 deletion tests/product_type_group_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def navigate_to_group_view(self):
driver.find_element(By.ID, "id_name").clear()
driver.find_element(By.ID, "id_name").send_keys("Group Name")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed group is now available, proceed with opening the context menu and clicking 'Edit' button
driver.find_element(By.ID, "dropdownMenuGroup").click()
driver.find_element(By.ID, "viewGroup").click()
Expand Down
10 changes: 5 additions & 5 deletions tests/user_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_user_edit_permissions(self):
driver.find_element(By.ID, "id_username").clear()
driver.find_element(By.ID, "id_username").send_keys("propersahm")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed user is now available, proceed with opening the context menu and clicking 'Edit' button
driver.find_element(By.ID, "dropdownMenuUser").click()
driver.find_element(By.ID, "editUser").click()
Expand All @@ -141,7 +141,7 @@ def test_user_delete(self):
driver.find_element(By.ID, "id_username").clear()
driver.find_element(By.ID, "id_username").send_keys("propersahm")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed user is now available, proceed with clicking 'View' button
driver.find_element(By.ID, "dropdownMenuUser").click()
driver.find_element(By.ID, "viewUser").click()
Expand Down Expand Up @@ -169,7 +169,7 @@ def test_user_with_writer_role_delete(self):
driver.find_element(By.ID, "id_username").clear()
driver.find_element(By.ID, "id_username").send_keys("userWriter")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed user is now available, proceed with clicking 'View' button
driver.find_element(By.ID, "dropdownMenuUser").click()
driver.find_element(By.ID, "viewUser").click()
Expand Down Expand Up @@ -235,7 +235,7 @@ def test_user_edit_configuration(self):
driver.find_element(By.ID, "id_username").clear()
driver.find_element(By.ID, "id_username").send_keys("propersahm")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed user is now available, proceed with opening the context menu and clicking 'Edit' button
driver.find_element(By.ID, "dropdownMenuUser").click()
driver.find_element(By.ID, "viewUser").click()
Expand All @@ -256,7 +256,7 @@ def test_user_edit_configuration(self):
driver.find_element(By.ID, "id_username").clear()
driver.find_element(By.ID, "id_username").send_keys("propersahm")
# click on 'apply filter' button
driver.find_element(By.CSS_SELECTOR, "button.btn.btn-sm.btn-secondary").click()
driver.find_element(By.ID, "apply").click()
# only the needed user is now available, proceed with opening the context menu and clicking 'Edit' button
driver.find_element(By.ID, "dropdownMenuUser").click()
driver.find_element(By.ID, "viewUser").click()
Expand Down

0 comments on commit 0e91248

Please sign in to comment.