From eaaa3a3ef216676d05e1dab8ad491d2fb469aff9 Mon Sep 17 00:00:00 2001 From: Wagner Date: Mon, 11 Nov 2024 08:41:23 -0700 Subject: [PATCH 1/4] LMSA-9483 a11y changes --- .../lms/crosslist/CrosslistConstants.java | 1 + .../controller/DecrosslistController.java | 5 + .../crosslist/service/CrosslistService.java | 10 +- .../resources/static/css/crosslisting.css | 4 + .../resources/static/js/decrosslisting.js | 106 +++++++++++++ .../templates/decrosslistlayout.html | 1 + .../resources/templates/findParentCourse.html | 140 +++++------------- 7 files changed, 151 insertions(+), 116 deletions(-) create mode 100644 src/main/resources/static/js/decrosslisting.js diff --git a/src/main/java/edu/iu/uits/lms/crosslist/CrosslistConstants.java b/src/main/java/edu/iu/uits/lms/crosslist/CrosslistConstants.java index 3ab4b93..0d9efdb 100644 --- a/src/main/java/edu/iu/uits/lms/crosslist/CrosslistConstants.java +++ b/src/main/java/edu/iu/uits/lms/crosslist/CrosslistConstants.java @@ -54,6 +54,7 @@ public interface CrosslistConstants { String LOOKUP_FAILURE_COURSE_NOT_CROSSLISTED_MESSAGE = "This course has not been crosslisted"; String LOOKUP_FAILURE_NOT_FOUND_IN_CANVAS_MESSAGE = "Not found in Canvas"; String LOOKUP_FAILURE_NOT_FOUND_IN_SIS_MESSAGE = "Not found in SIS"; + String LOOKUP_FAILURE_MISSING_SIS_ID = "SIS Section ID is required."; String LOOKUP_SUCCESS_CSS = "rvt-color-green rvt-bg-green-100"; String LOOKUP_FAILURE_CSS = "rvt-orange-green rvt-bg-orange-100"; diff --git a/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java b/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java index 64698d8..fa0abfa 100644 --- a/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java +++ b/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java @@ -106,6 +106,11 @@ public String lookupSearchBySisId(@ModelAttribute FindParentModel findParentMode model.addAttribute("findParentResult", findParentResult); // add canvasCourseId to be used in audit log purposes later model.addAttribute("canvasCourseId", findParentResult.getCanvasCourseId()); + + // If we aren't showing the course info, there must be an error + if (!findParentResult.isShowCourseInfo()){ + model.addAttribute("errorMsg", findParentResult.getStatusMessage()); + } } return "findParentCourse"; diff --git a/src/main/java/edu/iu/uits/lms/crosslist/service/CrosslistService.java b/src/main/java/edu/iu/uits/lms/crosslist/service/CrosslistService.java index 5ae842e..ee14bb6 100644 --- a/src/main/java/edu/iu/uits/lms/crosslist/service/CrosslistService.java +++ b/src/main/java/edu/iu/uits/lms/crosslist/service/CrosslistService.java @@ -395,9 +395,7 @@ public FindParentResult processSisLookup(String sisSectionId) { if (sisSectionId == null || sisSectionId.isEmpty()) { findParentResult.setShowCourseInfo(false); - findParentResult.setStatusMessage(CrosslistConstants.LOOKUP_FAILURE_NOT_FOUND_IN_SIS_MESSAGE); - findParentResult.setStatusIconCssClasses(CrosslistConstants.LOOKUP_FAILURE_CSS); - findParentResult.setStatusIconName(CrosslistConstants.LOOKUP_FAILURE_ICON_NAME); + findParentResult.setStatusMessage(CrosslistConstants.LOOKUP_FAILURE_MISSING_SIS_ID); return findParentResult; } @@ -406,8 +404,6 @@ public FindParentResult processSisLookup(String sisSectionId) { if (section == null) { findParentResult.setShowCourseInfo(false); findParentResult.setStatusMessage(CrosslistConstants.LOOKUP_FAILURE_NOT_FOUND_IN_CANVAS_MESSAGE); - findParentResult.setStatusIconCssClasses(CrosslistConstants.LOOKUP_FAILURE_CSS); - findParentResult.setStatusIconName(CrosslistConstants.LOOKUP_FAILURE_ICON_NAME); return findParentResult; } @@ -416,8 +412,6 @@ public FindParentResult processSisLookup(String sisSectionId) { if (course == null) { findParentResult.setShowCourseInfo(false); findParentResult.setStatusMessage(CrosslistConstants.LOOKUP_FAILURE_NOT_FOUND_IN_CANVAS_MESSAGE); - findParentResult.setStatusIconCssClasses(CrosslistConstants.LOOKUP_FAILURE_CSS); - findParentResult.setStatusIconName(CrosslistConstants.LOOKUP_FAILURE_ICON_NAME); return findParentResult; } @@ -429,8 +423,6 @@ public FindParentResult processSisLookup(String sisSectionId) { if (sectionsList == null) { findParentResult.setShowCourseInfo(false); findParentResult.setStatusMessage(CrosslistConstants.LOOKUP_FAILURE_NOT_FOUND_IN_CANVAS_MESSAGE); - findParentResult.setStatusIconCssClasses(CrosslistConstants.LOOKUP_FAILURE_CSS); - findParentResult.setStatusIconName(CrosslistConstants.LOOKUP_FAILURE_ICON_NAME); return findParentResult; } diff --git a/src/main/resources/static/css/crosslisting.css b/src/main/resources/static/css/crosslisting.css index 09cc97d..9ccdf74 100644 --- a/src/main/resources/static/css/crosslisting.css +++ b/src/main/resources/static/css/crosslisting.css @@ -102,4 +102,8 @@ .checkboxColumn { width: 1%; line-height: inherit; +} + +#results-message:focus { + outline: none; } \ No newline at end of file diff --git a/src/main/resources/static/js/decrosslisting.js b/src/main/resources/static/js/decrosslisting.js new file mode 100644 index 0000000..b237669 --- /dev/null +++ b/src/main/resources/static/js/decrosslisting.js @@ -0,0 +1,106 @@ +/*- + * #%L + * lms-lti-crosslist + * %% + * Copyright (C) 2015 - 2022 Indiana University + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the Indiana University nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +jQuery(document).ready(function() { + var resultsMessage = document.getElementById('results-message'); + + if (resultsMessage != null) { + resultsMessage.focus(); + } + + $("#checkbox-select-all").click(function() { + $('input:checkbox').not(this).prop('checked', this.checked); + }); + + checkboxCounter(); + + $('[aria-invalid="true"]').first().focus(); +}); + +function checkboxCounter() { + // Get all the selected checkboxes, except the "select-all" one up in the table header + var $checkboxes = $('#decrosslist-form input[type="checkbox"]'); + + // update the count + $checkboxes.change(function() { + var countCheckedCheckboxes = $checkboxes.not("#checkbox-select-all").filter(':checked').length; + $('#sections-selected').text(countCheckedCheckboxes + ' selected'); + }); +} + +function submitSisIdForm(button) { + buttonLoading(button); + document.getElementById('sisIdForm').submit(); +} + +function validateCheckboxForm(button) { + // Get all the selected checkboxes, except the "select-all" one up in the table header + var checkboxes = $('#decrosslist-form input[type="checkbox"]').not("#checkbox-select-all").filter(':checked').length; + + if (checkboxes > 0) { + buttonLoading(button); + document.getElementById('decrosslist-form').submit(); + } else { + var checkboxValidationMessage = document.getElementById('checkbox-validation-message'); + checkboxValidationMessage.classList.remove('rvt-display-none'); + + let firstCheckbox = $('#decrosslist-form input[type="checkbox"]').not("#checkbox-select-all").first(); + firstCheckbox.attr('aria-invalid', true); + firstCheckbox.attr('aria-describedby', 'checkbox-validation-message'); + firstCheckbox.focus(); + + return false; + } +} + +function buttonLoading(button) { + if (button.dataset.action != null) { + document.getElementById("submitValue").value = button.dataset.action; + } + button.setAttribute("aria-busy", true); + var buttonsToDisable = document.getElementsByTagName('button'); + for(var i = 0; i < buttonsToDisable.length; i++) { + buttonsToDisable[i].disabled = true; + } + button.classList.add("rvt-button--loading"); + + let spinners = button.getElementsByClassName('rvt-loader'); + if (spinners && spinners.length > 0) { + spinners[0].classList.remove("rvt-display-none"); + } + + let srText = button.getElementsByClassName('sr-loader-text'); + if (srText && srText.length > 0) { + srText[0].classList.remove("rvt-display-none"); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/decrosslistlayout.html b/src/main/resources/templates/decrosslistlayout.html index 66245a5..05dafb3 100644 --- a/src/main/resources/templates/decrosslistlayout.html +++ b/src/main/resources/templates/decrosslistlayout.html @@ -58,6 +58,7 @@ + From 8ab01dc4a310b432e3fbd477d522395fd54906b2 Mon Sep 17 00:00:00 2001 From: Gregory J Thomas Date: Wed, 13 Nov 2024 15:00:13 -0500 Subject: [PATCH 2/4] LMSA-9483 - more changes, mainly separating into two lists and adding buttons instead of a select all checkbox --- .../controller/DecrosslistController.java | 19 ++++ .../resources/static/js/decrosslisting.js | 33 ++++++- .../resources/templates/findParentCourse.html | 94 ++++++++++--------- 3 files changed, 98 insertions(+), 48 deletions(-) diff --git a/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java b/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java index fa0abfa..94b04f8 100644 --- a/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java +++ b/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java @@ -60,6 +60,7 @@ import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.authentication.OidcAuthenticationToken; import javax.servlet.http.HttpSession; +import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -102,7 +103,25 @@ public String lookupSearchBySisId(@ModelAttribute FindParentModel findParentMode String sisIdSearch = findParentModel.getSisIdSearch().trim(); findParentResult = crosslistService.processSisLookup(sisIdSearch); + List
unavailableToDecrosslistSectionsList = new ArrayList<>(); + List
availableToDecrosslistSectionsList = new ArrayList<>(); + if (findParentResult != null) { + List
fullSectionList = findParentResult.getSectionList(); + + if (fullSectionList != null) { + for (Section section : fullSectionList) { + if (section.getSis_section_id() == null || section.getNonxlist_course_id() == null) { + unavailableToDecrosslistSectionsList.add(section); + } else { + availableToDecrosslistSectionsList.add(section); + } + } + } + + model.addAttribute("unavailableToDecrosslistSectionsList", unavailableToDecrosslistSectionsList); + model.addAttribute("availableToDecrosslistSectionsList", availableToDecrosslistSectionsList); + model.addAttribute("findParentResult", findParentResult); // add canvasCourseId to be used in audit log purposes later model.addAttribute("canvasCourseId", findParentResult.getCanvasCourseId()); diff --git a/src/main/resources/static/js/decrosslisting.js b/src/main/resources/static/js/decrosslisting.js index b237669..8fe6401 100644 --- a/src/main/resources/static/js/decrosslisting.js +++ b/src/main/resources/static/js/decrosslisting.js @@ -38,8 +38,14 @@ jQuery(document).ready(function() { resultsMessage.focus(); } - $("#checkbox-select-all").click(function() { - $('input:checkbox').not(this).prop('checked', this.checked); + $("#select-all-button").click(function() { + $('input:checkbox').prop('checked', true); + updateCounter(); + }); + + $("#deselect-all-button").click(function() { + $('input:checkbox').prop('checked', false); + updateCounter(); }); checkboxCounter(); @@ -53,14 +59,31 @@ function checkboxCounter() { // update the count $checkboxes.change(function() { - var countCheckedCheckboxes = $checkboxes.not("#checkbox-select-all").filter(':checked').length; + var countCheckedCheckboxes = $checkboxes.filter(':checked').length; $('#sections-selected').text(countCheckedCheckboxes + ' selected'); }); } +function updateCounter() { + var $checkboxes = $('#decrosslist-form input[type="checkbox"]'); + var countCheckedCheckboxes = $checkboxes.filter(':checked').length; + $('#sections-selected').text(countCheckedCheckboxes + ' selected'); +} + function submitSisIdForm(button) { - buttonLoading(button); - document.getElementById('sisIdForm').submit(); + var inputFieldToSearch = 'sisid-search'; + + if (document.getElementById(inputFieldToSearch).value.trim() === '') { + var validationMessage = document.getElementById('sisid-validation-message'); + + validationMessage.classList.remove('rvt-display-none'); + validationMessage.setAttribute('aria-invalid', true); + validationMessage.setAttribute('aria-describedby', inputFieldToSearch); + return false; + } else { + buttonLoading(button); + document.getElementById('sisIdForm').submit(); + } } function validateCheckboxForm(button) { diff --git a/src/main/resources/templates/findParentCourse.html b/src/main/resources/templates/findParentCourse.html index a7adf94..20458de 100644 --- a/src/main/resources/templates/findParentCourse.html +++ b/src/main/resources/templates/findParentCourse.html @@ -77,14 +77,24 @@

De-cross-listing sections

Example: SP22-BL-FOLK-E295-4441 -
+
- + SIS Section ID is required. +
+ +
+ + + + + + +
@@ -106,51 +116,49 @@

- Sections listed without a checkbox are not available to de-cross-list because they either do not - have a SIS ID or it is the course's original section. + Select sections to remove, then activate De-cross-list selected sections to de-cross-list + them from the course. Unavailable sections are due to either not having a SIS ID or it is + the course's original section. -
- - - - - - - - - - - - -
Table of sections in the course. Select sections to remove, then activate De-cross-list selected sections to de-cross-list them from the course.
-
- - -
-
Section Name
-
- - -
-
- De-crosslisting unavailable -
-
- -
- -
- - - - - - - Select at least one section to de-cross-list. +
+ Unavailable sections +
+

None listed

+
    +
  • + Section Name +
  • +
- +
+
+

None listed

+
+ Available sections + + +
    +
  • +
    + + +
    +
  • +
+
+
+
+ + + + + + + Select at least one section to de-cross-list.
From 6fce57665cfbd00255b882416d3c3d93a8733b31 Mon Sep 17 00:00:00 2001 From: Wagner Date: Thu, 14 Nov 2024 10:59:34 -0700 Subject: [PATCH 3/4] LMSA-9483 more a11y updates --- .../controller/DecrosslistController.java | 17 +++++ .../resources/static/js/decrosslisting.js | 19 +++-- .../resources/templates/findParentCourse.html | 71 +++++++++---------- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java b/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java index 94b04f8..4259cf9 100644 --- a/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java +++ b/src/main/java/edu/iu/uits/lms/crosslist/controller/DecrosslistController.java @@ -205,6 +205,23 @@ public String decrossListSections(@RequestParam("section-checkboxes") List fullSectionList = findParentResult.getSectionList(); + List
unavailableToDecrosslistSectionsList = new ArrayList<>(); + List
availableToDecrosslistSectionsList = new ArrayList<>(); + + if (fullSectionList != null) { + for (Section section : fullSectionList) { + if (section.getSis_section_id() == null || section.getNonxlist_course_id() == null) { + unavailableToDecrosslistSectionsList.add(section); + } else { + availableToDecrosslistSectionsList.add(section); + } + } + } + + model.addAttribute("unavailableToDecrosslistSectionsList", unavailableToDecrosslistSectionsList); + model.addAttribute("availableToDecrosslistSectionsList", availableToDecrosslistSectionsList); } return "findParentCourse"; diff --git a/src/main/resources/static/js/decrosslisting.js b/src/main/resources/static/js/decrosslisting.js index 8fe6401..86deb68 100644 --- a/src/main/resources/static/js/decrosslisting.js +++ b/src/main/resources/static/js/decrosslisting.js @@ -50,7 +50,8 @@ jQuery(document).ready(function() { checkboxCounter(); - $('[aria-invalid="true"]').first().focus(); + // This will move focus to the invalid search if it fails the server-side validation + $('#sisid-search[aria-invalid="true"]').focus(); }); function checkboxCounter() { @@ -71,14 +72,18 @@ function updateCounter() { } function submitSisIdForm(button) { - var inputFieldToSearch = 'sisid-search'; - - if (document.getElementById(inputFieldToSearch).value.trim() === '') { - var validationMessage = document.getElementById('sisid-validation-message'); + let searchInput = document.getElementById('sisid-search'); + if (searchInput.value.trim() === '') { + let validationText = document.getElementById('sisid-error-text'); + validationText.innerText = 'SIS Section ID is required.'; + let validationMessage = document.getElementById('sisid-error-message'); validationMessage.classList.remove('rvt-display-none'); - validationMessage.setAttribute('aria-invalid', true); - validationMessage.setAttribute('aria-describedby', inputFieldToSearch); + + searchInput.setAttribute('aria-invalid', true); + searchInput.setAttribute('aria-describedby', 'sisid-error-text sisid-search-description'); + searchInput.focus(); + return false; } else { buttonLoading(button); diff --git a/src/main/resources/templates/findParentCourse.html b/src/main/resources/templates/findParentCourse.html index 20458de..2a376fb 100644 --- a/src/main/resources/templates/findParentCourse.html +++ b/src/main/resources/templates/findParentCourse.html @@ -66,7 +66,7 @@

De-cross-listing sections

SIS Section ID
-
- - - - - - - SIS Section ID is required. -
- -
+
@@ -123,32 +114,38 @@

- Unavailable sections -
-

None listed

-
    +
    + Unavailable sections +

    None listed

    +
    • Section Name
    -
+

-

None listed

-
- Available sections - - -
    -
  • -
    - - -
    -
  • -
+
+ Available sections + Select at least one course to de-cross-list + + +

No sections available

+ + + + +
    +
  • +
    + + +
    +
  • +
+
@@ -160,14 +157,16 @@

Select at least one section to de-cross-list.

-
- - +
0 selected
+ + + +
From a47d5741a022fcbb220e469d152d831b4d27acf3 Mon Sep 17 00:00:00 2001 From: Gregory J Thomas Date: Mon, 18 Nov 2024 13:44:36 -0500 Subject: [PATCH 4/4] LMSA-9483 - change var to let in decrosslist.js --- src/main/resources/static/js/decrosslisting.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/resources/static/js/decrosslisting.js b/src/main/resources/static/js/decrosslisting.js index 86deb68..ec95dd4 100644 --- a/src/main/resources/static/js/decrosslisting.js +++ b/src/main/resources/static/js/decrosslisting.js @@ -32,7 +32,7 @@ */ jQuery(document).ready(function() { - var resultsMessage = document.getElementById('results-message'); + let resultsMessage = document.getElementById('results-message'); if (resultsMessage != null) { resultsMessage.focus(); @@ -56,18 +56,18 @@ jQuery(document).ready(function() { function checkboxCounter() { // Get all the selected checkboxes, except the "select-all" one up in the table header - var $checkboxes = $('#decrosslist-form input[type="checkbox"]'); + let $checkboxes = $('#decrosslist-form input[type="checkbox"]'); // update the count $checkboxes.change(function() { - var countCheckedCheckboxes = $checkboxes.filter(':checked').length; + let countCheckedCheckboxes = $checkboxes.filter(':checked').length; $('#sections-selected').text(countCheckedCheckboxes + ' selected'); }); } function updateCounter() { - var $checkboxes = $('#decrosslist-form input[type="checkbox"]'); - var countCheckedCheckboxes = $checkboxes.filter(':checked').length; + let $checkboxes = $('#decrosslist-form input[type="checkbox"]'); + let countCheckedCheckboxes = $checkboxes.filter(':checked').length; $('#sections-selected').text(countCheckedCheckboxes + ' selected'); } @@ -93,13 +93,13 @@ function submitSisIdForm(button) { function validateCheckboxForm(button) { // Get all the selected checkboxes, except the "select-all" one up in the table header - var checkboxes = $('#decrosslist-form input[type="checkbox"]').not("#checkbox-select-all").filter(':checked').length; + let checkboxes = $('#decrosslist-form input[type="checkbox"]').not("#checkbox-select-all").filter(':checked').length; if (checkboxes > 0) { buttonLoading(button); document.getElementById('decrosslist-form').submit(); } else { - var checkboxValidationMessage = document.getElementById('checkbox-validation-message'); + let checkboxValidationMessage = document.getElementById('checkbox-validation-message'); checkboxValidationMessage.classList.remove('rvt-display-none'); let firstCheckbox = $('#decrosslist-form input[type="checkbox"]').not("#checkbox-select-all").first(); @@ -116,8 +116,8 @@ function buttonLoading(button) { document.getElementById("submitValue").value = button.dataset.action; } button.setAttribute("aria-busy", true); - var buttonsToDisable = document.getElementsByTagName('button'); - for(var i = 0; i < buttonsToDisable.length; i++) { + let buttonsToDisable = document.getElementsByTagName('button'); + for(let i = 0; i < buttonsToDisable.length; i++) { buttonsToDisable[i].disabled = true; } button.classList.add("rvt-button--loading");