diff --git a/app/coffeescripts/AssignmentMuter.js b/app/coffeescripts/AssignmentMuter.js index fb6fc2c8009..766cfef6321 100644 --- a/app/coffeescripts/AssignmentMuter.js +++ b/app/coffeescripts/AssignmentMuter.js @@ -16,12 +16,12 @@ export default class AssignmentMuter { this.options = options } - show () { + show (onClose) { if (this.options && this.options.openDialogInstantly) { if (this.assignment.muted) { this.confirmUnmute() } else { - this.showDialog() + this.showDialog(onClose) } } else { this.$link = $(this.$link) @@ -31,7 +31,7 @@ export default class AssignmentMuter { if (this.assignment.muted) { this.confirmUnmute() } else { - this.showDialog() + this.showDialog(onClose) } }) } @@ -41,7 +41,7 @@ export default class AssignmentMuter { this.$link.text(this.assignment.muted ? I18n.t('Unmute Assignment') : I18n.t('Mute Assignment')) } - showDialog () { + showDialog (onClose) { this.$dialog = $(mute_dialog_template()).dialog({ buttons: [{ text: I18n.t('Cancel'), @@ -61,6 +61,7 @@ export default class AssignmentMuter { resizable: false, width: 400 }) + this.$dialog.on('dialogclose', onClose) } afterUpdate (serverResponse) { @@ -92,7 +93,7 @@ export default class AssignmentMuter { click: () => this.$dialog.disableWhileLoading($.ajaxJSON(this.url, 'put', {status: false}, this.afterUpdate)) }], open: () => setTimeout(() => this.$dialog.parent().find('.ui-dialog-titlebar-close').focus(), 100), - close: () => this.$dialog.remove(), + close: () => this.$dialog.dialog('close'), resizable: false, title: I18n.t('unmute_assignment', 'Unmute Assignment'), width: 400, diff --git a/app/coffeescripts/gradezilla/Gradebook.coffee b/app/coffeescripts/gradezilla/Gradebook.coffee index 25b38ad9e2f..43f688b6669 100644 --- a/app/coffeescripts/gradezilla/Gradebook.coffee +++ b/app/coffeescripts/gradezilla/Gradebook.coffee @@ -1420,13 +1420,15 @@ define [ @grid.invalidate() @renderTotalGradeColumnHeader() - togglePointsOrPercentTotals: => + togglePointsOrPercentTotals: (cb) => if UserSettings.contextGet('warned_about_totals_display') @switchTotalDisplay() + cb() if typeof cb == 'function' else dialog_options = showing_points: @options.show_total_grade_as_points save: @switchTotalDisplay + onClose: cb new GradeDisplayWarningDialog(dialog_options) onUserFilterInput: (term) => @@ -2095,6 +2097,13 @@ define [ @renderStudentColumnHeader() @renderTotalGradeColumnHeader() + # Column Header Helpers + handleHeaderKeyDown: (e, columnId) => + @gridSupport.navigation.handleHeaderKeyDown e, + region: 'header' + cell: @grid.getColumnIndex(columnId) + columnId: columnId + # Student Column Header getStudentColumnSortBySetting: => @@ -2129,6 +2138,8 @@ define [ addGradebookElement: @keyboardNav.addGradebookElement removeGradebookElement: @keyboardNav.removeGradebookElement onMenuClose: @handleColumnHeaderMenuClose + onHeaderKeyDown: (e) => + @handleHeaderKeyDown(e, 'student') renderStudentColumnHeader: => mountPoint = @getColumnHeaderNode('student') @@ -2138,6 +2149,7 @@ define [ # Total Grade Column Header freezeTotalGradeColumn: => + @totalColumnPositionChanged = true allColumns = @grid.getColumns() # Remove total_grade column from aggregate section @@ -2159,6 +2171,7 @@ define [ @updateFrozenColumnsAndRenderGrid(allColumns) moveTotalGradeColumnToEnd: => + @totalColumnPositionChanged = true allColumns = @grid.getColumns() # Remove total_grade column from aggregate or frozen section as needed @@ -2222,6 +2235,13 @@ define [ @moveTotalGradeColumnToEnd() , 10) + totalColumnShouldFocus: -> + if @totalColumnPositionChanged + @totalColumnPositionChanged = false + true + else + false + getTotalGradeColumnHeaderProps: -> ref: (ref) => @setHeaderComponentRef('total_grade', ref) @@ -2231,6 +2251,9 @@ define [ addGradebookElement: @keyboardNav.addGradebookElement removeGradebookElement: @keyboardNav.removeGradebookElement onMenuClose: @handleColumnHeaderMenuClose + grabFocus: @totalColumnShouldFocus() + onHeaderKeyDown: (e) => + @handleHeaderKeyDown(e, 'total_grade') renderTotalGradeColumnHeader: => return if @hideAggregateColumns() @@ -2364,6 +2387,8 @@ define [ removeGradebookElement: @keyboardNav.removeGradebookElement onMenuClose: @handleColumnHeaderMenuClose showUnpostedMenuItem: @options.new_gradebook_development_enabled + onHeaderKeyDown: (e) => + @handleHeaderKeyDown(e, @getAssignmentColumnId(assignmentId)) } renderAssignmentColumnHeader: (assignmentId) => @@ -2406,6 +2431,8 @@ define [ addGradebookElement: @keyboardNav.addGradebookElement removeGradebookElement: @keyboardNav.removeGradebookElement onMenuClose: @handleColumnHeaderMenuClose + onHeaderKeyDown: (e) => + @handleHeaderKeyDown(e, @getAssignmentGroupColumnId(assignmentGroupId)) } renderAssignmentGroupColumnHeader: (assignmentGroupId) => diff --git a/app/coffeescripts/gradezilla/SetDefaultGradeDialog.coffee b/app/coffeescripts/gradezilla/SetDefaultGradeDialog.coffee index 3afc37550d0..5a575119460 100644 --- a/app/coffeescripts/gradezilla/SetDefaultGradeDialog.coffee +++ b/app/coffeescripts/gradezilla/SetDefaultGradeDialog.coffee @@ -38,7 +38,7 @@ define [ class SetDefaultGradeDialog constructor: ({@assignment, @students, @context_id, @selected_section}) -> - show: => + show: (onClose) => templateLocals = assignment: @assignment showPointsPossible: (@assignment.points_possible || @assignment.points_possible == '0') && @assignment.grading_type != "gpa_scale" @@ -52,6 +52,7 @@ define [ open: => @$dialog.find(".grading_box").focus() close: => @$dialog.remove() ).fixDialogButtons() + @$dialog.on 'dialogclose', onClose $form = @$dialog $(".ui-dialog-titlebar-close").focus() @@ -85,7 +86,7 @@ define [ count: submissions.length) submittingDfd.resolve() $("#set_default_grade").focus() - @$dialog.remove() + @$dialog.dialog('close') getStudents = => if @selected_section diff --git a/app/coffeescripts/shared/CurveGradesDialog.coffee b/app/coffeescripts/shared/CurveGradesDialog.coffee index 26e70e8837a..62640d008b1 100644 --- a/app/coffeescripts/shared/CurveGradesDialog.coffee +++ b/app/coffeescripts/shared/CurveGradesDialog.coffee @@ -32,7 +32,7 @@ define [ class CurveGradesDialog constructor: ({@assignment, @students, @context_url}) -> - show: => + show: (onClose) => locals = assignment: @assignment action: "#{@context_url}/gradebook/update_submission" @@ -90,6 +90,7 @@ define [ close: => @$dialog.remove() .fixDialogButtons() + @$dialog.on 'dialogclose', onClose @$dialog.parent().find('.ui-dialog-titlebar-close').focus() @$dialog.find("#middle_score").bind "blur change keyup focus", @curve @$dialog.find("#assign_blanks").change @curve diff --git a/app/coffeescripts/shared/GradeDisplayWarningDialog.coffee b/app/coffeescripts/shared/GradeDisplayWarningDialog.coffee index 3dcef137d58..3fbcf091090 100644 --- a/app/coffeescripts/shared/GradeDisplayWarningDialog.coffee +++ b/app/coffeescripts/shared/GradeDisplayWarningDialog.coffee @@ -36,13 +36,16 @@ define [ buttons: [{ text: I18n.t("grade_display_warning.cancel", "Cancel"), click: @cancel}, {text: I18n.t("grade_display_warning.continue", "Continue"), click: @save}] + close: => + @$dialog.remove() + options.onClose() if typeof options.onClose == 'function' save: () => if @$dialog.find('#hide_warning').prop('checked') @options.save({ dontWarnAgain: true }) else @options.save({ dontWarnAgain: false }) - @$dialog.remove() + @$dialog.dialog('close') cancel: () => - @$dialog.remove() + @$dialog.dialog('close') diff --git a/app/jsx/gradezilla/default_gradebook/CurveGradesDialogManager.js b/app/jsx/gradezilla/default_gradebook/CurveGradesDialogManager.js index 17908c34741..1bafa49ba9a 100644 --- a/app/jsx/gradezilla/default_gradebook/CurveGradesDialogManager.js +++ b/app/jsx/gradezilla/default_gradebook/CurveGradesDialogManager.js @@ -27,13 +27,13 @@ import 'compiled/jquery.rails_flash_notifications' return { isDisabled: !submissionsLoaded || gradingType === 'pass_fail' || pointsPossible == null || pointsPossible === 0, - onSelect () { // eslint-disable-line consistent-return + onSelect (onClose) { // eslint-disable-line consistent-return if (!isAdmin && assignment.inClosedGradingPeriod) { return $.flashError(I18n.t('Unable to curve grades because this assignment is due in a closed ' + 'grading period for at least one student')); } const dialog = new CurveGradesDialog({assignment, students, context_url: contextUrl}); - dialog.show(); + dialog.show(onClose); } }; } diff --git a/app/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeader.js b/app/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeader.js index 3bf7e1c32ad..aa51c6bee86 100644 --- a/app/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeader.js +++ b/app/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeader.js @@ -38,6 +38,7 @@ import ColumnHeader from 'jsx/gradezilla/default_gradebook/components/ColumnHead class AssignmentColumnHeader extends ColumnHeader { static propTypes = { + ...ColumnHeader.propTypes, assignment: shape({ courseId: string.isRequired, htmlUrl: string.isRequired, @@ -92,8 +93,7 @@ class AssignmentColumnHeader extends ColumnHeader { onSelect: func.isRequired }).isRequired, onMenuClose: func.isRequired, - showUnpostedMenuItem: bool.isRequired, - ...ColumnHeader.propTypes + showUnpostedMenuItem: bool.isRequired }; static defaultProps = { @@ -117,6 +117,18 @@ class AssignmentColumnHeader extends ColumnHeader { } bindAssignmentLink = (ref) => { this.assignmentLink = ref }; + + curveGrades = () => { this.invokeAndSkipFocus(this.props.curveGradesAction) }; + setDefaultGrades = () => { this.invokeAndSkipFocus(this.props.setDefaultGradeAction) }; + muteAssignment = () => { this.invokeAndSkipFocus(this.props.muteAssignmentAction) }; + downloadSubmissions = () => { this.invokeAndSkipFocus(this.props.downloadSubmissionsAction) }; + reuploadSubmissions = () => { this.invokeAndSkipFocus(this.props.reuploadSubmissionsAction) }; + + invokeAndSkipFocus (action) { + this.setState({ skipFocusOnClose: true }); + action.onSelect(this.focusAtEnd); + } + focusAtStart = () => { this.assignmentLink.focus() }; handleKeyDown = (event) => { @@ -138,7 +150,9 @@ class AssignmentColumnHeader extends ColumnHeader { }; showMessageStudentsWhoDialog = () => { + this.setState({ skipFocusOnClose: true }); const settings = MessageStudentsWhoHelper.settings(this.props.assignment, this.activeStudentDetails()); + settings.onClose = this.focusAtEnd; window.messageStudents(settings); } @@ -266,7 +280,6 @@ class AssignmentColumnHeader extends ColumnHeader { - {I18n.t('Curve Grades')} {I18n.t('Set Default Grade')} {this.props.assignment.muted ? I18n.t('Unmute Assignment') : I18n.t('Mute Assignment')} @@ -306,14 +319,14 @@ class AssignmentColumnHeader extends ColumnHeader { { !this.props.downloadSubmissionsAction.hidden && - + {I18n.t('Download Submissions')} } { !this.props.reuploadSubmissionsAction.hidden && - + {I18n.t('Re-Upload Submissions')} } diff --git a/app/jsx/gradezilla/default_gradebook/components/ColumnHeader.js b/app/jsx/gradezilla/default_gradebook/components/ColumnHeader.js index fab5e686acc..19a3e50480f 100644 --- a/app/jsx/gradezilla/default_gradebook/components/ColumnHeader.js +++ b/app/jsx/gradezilla/default_gradebook/components/ColumnHeader.js @@ -23,12 +23,14 @@ import { func } from 'prop-types'; export default class ColumnHeader extends React.Component { static propTypes = { addGradebookElement: func, - removeGradebookElement: func + removeGradebookElement: func, + onHeaderKeyDown: func }; static defaultProps = { addGradebookElement () {}, - removeGradebookElement () {} + removeGradebookElement () {}, + onHeaderKeyDown () {} }; constructor (props) { @@ -37,33 +39,30 @@ export default class ColumnHeader extends React.Component { this.handleKeyDown = this.handleKeyDown.bind(this); } - state = { menuShown: false }; + state = { menuShown: false, skipFocusOnClose: false }; bindOptionsMenuTrigger = (ref) => { this.optionsMenuTrigger = ref }; - bindSortByMenuContent = (ref) => { + bindFlyoutMenu = (ref, savedRef) => { // instructure-ui components return references to react components. // At this time the only way to get dom node refs is via findDOMNode. if (ref) { // eslint-disable-next-line react/no-find-dom-node - this.props.addGradebookElement(ReactDOM.findDOMNode(ref)); - } else { + const domNode = ReactDOM.findDOMNode(ref); + this.props.addGradebookElement(domNode); + domNode.addEventListener('keydown', this.handleMenuKeyDown); + } else if (savedRef) { // eslint-disable-next-line react/no-find-dom-node - this.props.removeGradebookElement(ReactDOM.findDOMNode(this.sortByMenuContent)); + this.props.removeGradebookElement(ReactDOM.findDOMNode(savedRef)); } + } + bindSortByMenuContent = (ref) => { + this.bindFlyoutMenu(ref, this.sortByMenuContent); this.sortByMenuContent = ref; }; bindOptionsMenuContent = (ref) => { - // Dealing with add/removeGradebookElement in a convoluted combination of - // this method and onToggle rather than the simpler way of calling those - // methods directly (like in bindSortByMenuContent) because this method is - // called by PopoverMenu three times when opening the menu. First with a ref - // to the content, then with null, then again with a ref to the content. - // We MUST get the DOM node here, rather than in onToggle because by the - // time onToggle is called when closing the menu, the component has already - // been unmounted and an error will be thrown if you attempt access. // instructure-ui components return references to react components. // At this time the only way to get dom node refs is via findDOMNode. if (ref) { @@ -71,6 +70,7 @@ export default class ColumnHeader extends React.Component { // eslint-disable-next-line react/no-find-dom-node this.optionsMenuContentDOMNode = ReactDOM.findDOMNode(ref); } + this.bindFlyoutMenu(ref, this.optionsMenuContent); }; focusAtStart = () => { @@ -85,18 +85,43 @@ export default class ColumnHeader extends React.Component { } }; - onToggle = (show) => { - this.setState({ menuShown: show }, () => { - if (show) { - this.props.addGradebookElement(this.optionsMenuContentDOMNode); + onToggle = (menuShown) => { + const newState = { menuShown }; + let callback; + + if (this.state.menuShown && !menuShown) { + if (this.state.skipFocusOnClose) { + newState.skipMenuOnClose = false; } else { - this.props.removeGradebookElement(this.optionsMenuContentDOMNode); + callback = this.focusAtEnd; + } + } + + if (!this.state.menuShown && menuShown) { + newState.skipFocusOnClose = false; + } + + this.setState(newState, () => { + if (typeof callback === 'function') { + callback(); + } + + if (!menuShown) { this.optionsMenuContent = null; this.optionsMenuContentDOMNode = null; } }); }; + handleMenuKeyDown = (event) => { + if (event.which === 9) { // Tab + this.setState({ menuShown: false, skipFocusOnClose: true }); + this.props.onHeaderKeyDown(event); + return false; + } + return true; + }; + handleKeyDown (event) { if (document.activeElement === this.optionsMenuTrigger) { if (event.which === 13) { // Enter diff --git a/app/jsx/gradezilla/default_gradebook/components/StudentColumnHeader.js b/app/jsx/gradezilla/default_gradebook/components/StudentColumnHeader.js index cbe2964cce2..bf11b3d5fd1 100644 --- a/app/jsx/gradezilla/default_gradebook/components/StudentColumnHeader.js +++ b/app/jsx/gradezilla/default_gradebook/components/StudentColumnHeader.js @@ -86,8 +86,15 @@ export default class StudentColumnHeader extends ColumnHeader { this.props.onToggleEnrollmentFilter(enrollmentFilterKey); } - bindDisplayAsMenuContent = (ref) => { this.displayAsMenuContent = ref; }; - bindSecondaryInfoMenuContent = (ref) => { this.secondaryInfoMenuContent = ref; }; + bindDisplayAsMenuContent = (ref) => { + this.displayAsMenuContent = ref; + this.bindFlyoutMenu(ref, this.displayAsMenuContent); + }; + + bindSecondaryInfoMenuContent = (ref) => { + this.secondaryInfoMenuContent = ref; + this.bindFlyoutMenu(ref, this.secondaryInfoMenuContent); + }; render () { const { diff --git a/app/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeader.js b/app/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeader.js index fbc904b9890..8c7c06c6daa 100644 --- a/app/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeader.js +++ b/app/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeader.js @@ -66,13 +66,30 @@ class TotalGradeColumnHeader extends ColumnHeader { onMoveToBack: func.isRequired }).isRequired, onMenuClose: func.isRequired, + grabFocus: bool, ...ColumnHeader.propTypes }; static defaultProps = { + grabFocus: false, ...ColumnHeader.defaultProps }; + state = { menuShown: false, skipFocusOnClose: false }; + + switchGradeDisplay = () => { this.invokeAndSkipFocus(this.props.gradeDisplay) }; + + invokeAndSkipFocus (action) { + this.setState({ skipFocusOnClose: true }); + action.onSelect(this.focusAtEnd); + } + + componentDidMount () { + if (this.props.grabFocus) { + this.focusAtEnd(); + } + } + render () { const { sortBySetting, gradeDisplay, position } = this.props; const selectedSortSetting = sortBySetting.isSortColumn && sortBySetting.settingKey; @@ -127,7 +144,7 @@ class TotalGradeColumnHeader extends ColumnHeader { !gradeDisplay.hidden && {displayAsPoints ? I18n.t('Display as Percentage') : I18n.t('Display as Points')} diff --git a/app/jsx/gradezilla/shared/AssignmentMuterDialogManager.js b/app/jsx/gradezilla/shared/AssignmentMuterDialogManager.js index 738a649ed3a..1565a77037b 100644 --- a/app/jsx/gradezilla/shared/AssignmentMuterDialogManager.js +++ b/app/jsx/gradezilla/shared/AssignmentMuterDialogManager.js @@ -27,11 +27,11 @@ import AssignmentMuter from 'compiled/AssignmentMuter' this.isDialogEnabled = this.isDialogEnabled.bind(this); } - showDialog () { + showDialog (cb) { const assignmentMuter = new AssignmentMuter( null, this.assignment, this.url, null, { openDialogInstantly: true } ); - assignmentMuter.show(); + assignmentMuter.show(cb); } isDialogEnabled () { diff --git a/app/jsx/gradezilla/shared/DownloadSubmissionsDialogManager.js b/app/jsx/gradezilla/shared/DownloadSubmissionsDialogManager.js index 49f98fc1026..bace5d969b0 100644 --- a/app/jsx/gradezilla/shared/DownloadSubmissionsDialogManager.js +++ b/app/jsx/gradezilla/shared/DownloadSubmissionsDialogManager.js @@ -35,9 +35,9 @@ import 'jquery.instructure_misc_helpers' ) && this.assignment.has_submitted_submissions; } - showDialog () { + showDialog (cb) { this.submissionsDownloading(this.assignment.id); - INST.downloadSubmissions(this.downloadUrl); + INST.downloadSubmissions(this.downloadUrl, cb); } } diff --git a/app/jsx/gradezilla/shared/ReuploadSubmissionsDialogManager.js b/app/jsx/gradezilla/shared/ReuploadSubmissionsDialogManager.js index 535ec074267..3e2b869ca78 100644 --- a/app/jsx/gradezilla/shared/ReuploadSubmissionsDialogManager.js +++ b/app/jsx/gradezilla/shared/ReuploadSubmissionsDialogManager.js @@ -33,7 +33,7 @@ import 'jquery.instructure_misc_helpers' return this.assignment.hasDownloadedSubmissions; } - getReuploadForm () { + getReuploadForm (cb) { if (ReuploadSubmissionsDialogManager.reuploadForm) { return ReuploadSubmissionsDialogManager.reuploadForm; } @@ -45,7 +45,12 @@ import 'jquery.instructure_misc_helpers' width: 400, modal: true, resizable: false, - autoOpen: false + autoOpen: false, + close: () => { + if (typeof cb === 'function') { + cb(); + } + } } ).submit(function () { const data = $(this).getFormData(); @@ -64,8 +69,8 @@ import 'jquery.instructure_misc_helpers' return ReuploadSubmissionsDialogManager.reuploadForm; } - showDialog () { - const form = this.getReuploadForm(); + showDialog (cb) { + const form = this.getReuploadForm(cb); form.attr('action', this.reuploadUrl).dialog('open'); } } diff --git a/app/jsx/gradezilla/shared/SetDefaultGradeDialogManager.js b/app/jsx/gradezilla/shared/SetDefaultGradeDialogManager.js index cebdbdce698..47f4e716178 100644 --- a/app/jsx/gradezilla/shared/SetDefaultGradeDialogManager.js +++ b/app/jsx/gradezilla/shared/SetDefaultGradeDialogManager.js @@ -42,11 +42,11 @@ import 'compiled/jquery.rails_flash_notifications' }; } - showDialog () { + showDialog (cb) { if (this.isAdmin || !this.assignment.inClosedGradingPeriod) { const dialog = new SetDefaultGradeDialog(this.getSetDefaultGradeDialogOptions()); - dialog.show(); + dialog.show(cb); } else { $.flashError(I18n.t('Unable to set default grade because this ' + 'assignment is due in a closed grading period for at least one student')); diff --git a/public/javascripts/message_students.js b/public/javascripts/message_students.js index fe9badef1ca..7f387834768 100644 --- a/public/javascripts/message_students.js +++ b/public/javascripts/message_students.js @@ -95,7 +95,10 @@ import './jquery.instructure_misc_plugins' /* showIf */ $message_students_dialog.dialog({ width: 600, modal: true - }).dialog('open').dialog('option', 'title', I18n.t("message_student", "Message Students for %{course_name}", {course_name: title})); + }) + .dialog('open') + .dialog('option', 'title', I18n.t('message_student', 'Message Students for %{course_name}', {course_name: title})) + .on('dialogclose', settings.onClose); }; $(document).ready(function() { diff --git a/public/javascripts/submission_download.js b/public/javascripts/submission_download.js index 09c7e672b0c..816f12e0959 100644 --- a/public/javascripts/submission_download.js +++ b/public/javascripts/submission_download.js @@ -24,57 +24,61 @@ import './jquery.ajaxJSON' import 'jqueryui/dialog' import 'jqueryui/progressbar' - INST.downloadSubmissions = function(url) { - var cancelled = false; - var title = ENV.SUBMISSION_DOWNLOAD_DIALOG_TITLE; - title = title || I18n.t('#submissions.download_submissions', - 'Download Assignment Submissions'); - $("#download_submissions_dialog").dialog({ - title: title, - close: function() { - cancelled = true; - } - }); - $("#download_submissions_dialog .progress").progressbar({value: 0}); - var checkForChange = function() { - if(cancelled || $("#download_submissions_dialog:visible").length == 0) { return; } - $("#download_submissions_dialog .status_loader").css('visibility', 'visible'); - var lastProgress = null; - $.ajaxJSON(url, 'GET', {}, function(data) { - if(data && data.attachment) { - var attachment = data.attachment; - if(attachment.workflow_state == 'zipped') { - $("#download_submissions_dialog .progress").progressbar('value', 100); - var message = I18n.t("#submissions.finished_redirecting", "Finished! Redirecting to File..."); - var link = " " + htmlEscape(I18n.t("#submissions.click_to_download", "Click here to download %{size_of_file}", {size_of_file: attachment.readable_size})) + "" - $("#download_submissions_dialog .status").html(htmlEscape(message) + "
" + $.raw(link)); - $("#download_submissions_dialog .status_loader").css('visibility', 'hidden'); - location.href = url; - return; +INST.downloadSubmissions = function (url, onClose) { + let cancelled = false; + const title = ENV.SUBMISSION_DOWNLOAD_DIALOG_TITLE || I18n.t('Download Assignment Submissions'); + + $('#download_submissions_dialog').dialog({ + title, + close () { cancelled = true; } + }).on('dialogclose', onClose); + $('#download_submissions_dialog .progress').progressbar({value: 0}); + const checkForChange = function () { + if (cancelled || $('#download_submissions_dialog:visible').length === 0) { return; } + $('#download_submissions_dialog .status_loader').css('visibility', 'visible'); + let lastProgress = null; + $.ajaxJSON(url, 'GET', {}, (data) => { + if (data && data.attachment) { + const attachment = data.attachment; + if (attachment.workflow_state === 'zipped') { + $('#download_submissions_dialog .progress').progressbar('value', 100); + const message = I18n.t('#submissions.finished_redirecting', 'Finished! Redirecting to File...'); + const linkText = I18n.t('Click here to download %{size_of_file}', {size_of_file: attachment.readable_size}); + const link = `${htmlEscape(linkText)}`; + + $('#download_submissions_dialog .status').html(`${htmlEscape(message)}
${$.raw(link)}`); + $('#download_submissions_dialog .status_loader').css('visibility', 'hidden'); + + location.href = url; + return; + } else { + let progress = parseInt(attachment.file_state, 10); + if (isNaN(progress)) { progress = 0; } + progress += 5 + $('#download_submissions_dialog .progress').progressbar('value', progress); + let message = null; + if (progress >= 95) { + message = I18n.t('#submissions.creating_zip', 'Creating zip file...'); } else { - var progress = parseInt(attachment.file_state, 10); - if(isNaN(progress)) { progress = 0; } - progress += 5 - $("#download_submissions_dialog .progress").progressbar('value', progress); - var message = null; - if(progress >= 95){ - message = I18n.t("#submissions.creating_zip", "Creating zip file..."); - } else { - message = I18n.t("#submissions.gathering_files_progress", "Gathering Files (%{progress})...", {progress: I18n.toPercentage(progress)}); - } - $("#download_submissions_dialog .status").text(message); - if(progress <= 5 || progress == lastProgress) { - $.ajaxJSON(url + "&compile=1", 'GET', {}, function() {}, function() {}); - } - lastProgress = progress; + message = I18n.t( + '#submissions.gathering_files_progress', + 'Gathering Files (%{progress})...', + { progress: I18n.toPercentage(progress) } + ); } + $('#download_submissions_dialog .status').text(message); + if (progress <= 5 || progress === lastProgress) { + $.ajaxJSON(`${url}&compile=1`, 'GET', {}, () => {}, () => {}); + } + lastProgress = progress; } - $("#download_submissions_dialog .status_loader").css('visibility', 'hidden'); - setTimeout(checkForChange, 3000); - }, function(data) { - $("#download_submissions_dialog .status_loader").css('visibility', 'hidden'); - setTimeout(checkForChange, 1000); - }); - } - checkForChange(); + } + $('#download_submissions_dialog .status_loader').css('visibility', 'hidden'); + setTimeout(checkForChange, 3000); + }, () => { + $('#download_submissions_dialog .status_loader').css('visibility', 'hidden'); + setTimeout(checkForChange, 1000); + }); }; + checkForChange(); +}; diff --git a/spec/coffeescripts/gradezilla/SetDefaultGradeDialogSpec.coffee b/spec/coffeescripts/gradezilla/SetDefaultGradeDialogSpec.coffee index 8bd6aef2bef..b303c13e2b6 100644 --- a/spec/coffeescripts/gradezilla/SetDefaultGradeDialogSpec.coffee +++ b/spec/coffeescripts/gradezilla/SetDefaultGradeDialogSpec.coffee @@ -46,3 +46,10 @@ define [ deepEqual dialog.gradeIsExcused('F'), false #this test documents that we do not consider 'excused' to return true deepEqual dialog.gradeIsExcused('excused'), false + + test 'when given callback for #show, invokes callback upon dialog close', -> + callback = @stub() + dialog = new SetDefaultGradeDialog({ @assignment }) + dialog.show(callback) + $('button.ui-dialog-titlebar-close').click(); + equal(callback.callCount, 1) diff --git a/spec/javascripts/jsx/gradezilla/GradebookSpec.js b/spec/javascripts/jsx/gradezilla/GradebookSpec.js index cc47ce14918..ee45025cfa0 100644 --- a/spec/javascripts/jsx/gradezilla/GradebookSpec.js +++ b/spec/javascripts/jsx/gradezilla/GradebookSpec.js @@ -2124,6 +2124,15 @@ test('when user is ignoring warnings, immediately toggles the total grade displa equal(this.gradebook.switchTotalDisplay.callCount, 1, 'toggles the total grade display'); }); +test('when user is ignoring warnings and a callback is given, immediately invokes callback', function () { + const callback = this.stub(); + UserSettings.contextSet('warned_about_totals_display', true); + + this.gradebook.togglePointsOrPercentTotals(callback); + + equal(callback.callCount, 1); +}); + test('when user is not ignoring warnings, return a dialog', function () { UserSettings.contextSet('warned_about_totals_display', false); @@ -2143,6 +2152,16 @@ test('when user is not ignoring warnings, the dialog has a save property which i dialog.cancel(); }); +test('when user is not ignoring warnings, the dialog has a onClose property which is the callback function', function () { + const callback = this.stub(); + this.stub(UserSettings, 'contextGet').withArgs('warned_about_totals_display').returns(false); + const dialog = this.gradebook.togglePointsOrPercentTotals(callback); + + equal(dialog.options.onClose, callback); + + dialog.cancel(); +}); + QUnit.module('Gradebook#showNotesColumn', { setup () { this.stub(DataLoader, 'getDataForColumn'); @@ -5470,6 +5489,13 @@ test('includes props for the "Position" settings', function () { strictEqual(typeof props.position.onMoveToBack, 'function', 'props include "onMoveToBack"'); }); +test('includes prop for focusing the column header', function () { + const gradebook = this.createGradebook(); + this.stub(gradebook, 'totalColumnShouldFocus').returns(true); + const props = gradebook.getTotalGradeColumnHeaderProps(); + equal(typeof props.grabFocus, 'boolean'); +}); + QUnit.module('Gradebook#setStudentDisplay', { createGradebook (multipleSections = false) { const options = {}; diff --git a/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeaderSpec.js b/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeaderSpec.js index 5de36472dde..003cc4dda58 100644 --- a/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeaderSpec.js +++ b/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentColumnHeaderSpec.js @@ -184,16 +184,20 @@ test('renders a PopoverMenu with a trigger', function () { }); test('calls addGradebookElement prop on open', function () { + notOk(this.props.addGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.addGradebookElement.callCount, 1); + ok(this.props.addGradebookElement.called); }); test('calls removeGradebookElement prop on close', function () { + notOk(this.props.removeGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.removeGradebookElement.callCount, 1); + ok(this.props.removeGradebookElement.called); }); test('calls onMenuClose prop on close', function () { @@ -221,13 +225,6 @@ QUnit.module('AssignmentColumnHeader: Sort by Settings', { this.mountAndOpenOptions = mountAndOpenOptions; }, - openSortByMenu (wrapper) { - const menuContent = new ReactWrapper([wrapper.node.optionsMenuContent], wrapper.node); - const flyout = menuContent.find('MenuItemFlyout'); - flyout.find('button').simulate('mouseOver'); - return new ReactWrapper([wrapper.node.sortByMenuContent], wrapper.node); - }, - teardown () { this.wrapper.unmount(); } @@ -257,6 +254,17 @@ test('clicking "Grade - Low to High" calls onSortByGradeAscending', function () strictEqual(onSortByGradeAscending.callCount, 1); }); +test('clicking "Grade - Low to High" focuses menu trigger', function () { + const onSortByGradeAscending = this.stub(); + const props = defaultProps({ sortBySetting: { onSortByGradeAscending } }); + const menuItem = findMenuItem.call(this, props, 'Sort by', 'Grade - Low to High'); + const focusStub = this.stub(this.wrapper.instance(), 'focusAtEnd') + + menuItem.simulate('click'); + + equal(focusStub.callCount, 1); +}); + test('"Grade - Low to High" is optionally disabled', function () { const props = defaultProps({ sortBySetting: { disabled: true } }); const menuItem = findMenuItem.call(this, props, 'Sort by', 'Grade - Low to High'); @@ -282,6 +290,17 @@ test('clicking "Grade - High to Low" calls onSortByGradeDescending', function () strictEqual(onSortByGradeDescending.callCount, 1); }); +test('clicking "Grade - High to Low" focuses menu trigger', function () { + const onSortByGradeDescending = this.stub(); + const props = defaultProps({ sortBySetting: { onSortByGradeDescending } }); + const menuItem = findMenuItem.call(this, props, 'Sort by', 'Grade - High to Low'); + const focusStub = this.stub(this.wrapper.instance(), 'focusAtEnd') + + menuItem.simulate('click'); + + equal(focusStub.callCount, 1); +}); + test('"Grade - High to Low" is optionally disabled', function () { const props = defaultProps({ sortBySetting: { disabled: true } }); const menuItem = findMenuItem.call(this, props, 'Sort by', 'Grade - High to Low'); @@ -307,6 +326,17 @@ test('clicking "Missing" calls onSortByMissing', function () { strictEqual(onSortByMissing.callCount, 1); }); +test('clicking "Missing" focuses menu trigger', function () { + const onSortByMissing = this.stub(); + const props = defaultProps({ sortBySetting: { onSortByMissing } }); + const menuItem = findMenuItem.call(this, props, 'Sort by', 'Missing'); + const focusStub = this.stub(this.wrapper.instance(), 'focusAtEnd') + + menuItem.simulate('click'); + + equal(focusStub.callCount, 1); +}); + test('"Missing" is optionally disabled', function () { const props = defaultProps({ sortBySetting: { disabled: true } }); const menuItem = findMenuItem.call(this, props, 'Sort by', 'Missing'); @@ -332,6 +362,17 @@ test('clicking "Late" calls onSortByLate', function () { strictEqual(onSortByLate.callCount, 1); }); +test('clicking "Late" focuses menu trigger', function () { + const onSortByLate = this.stub(); + const props = defaultProps({ sortBySetting: { onSortByLate } }); + const menuItem = findMenuItem.call(this, props, 'Sort by', 'Late'); + const focusStub = this.stub(this.wrapper.instance(), 'focusAtEnd') + + menuItem.simulate('click'); + + equal(focusStub.callCount, 1); +}); + test('"Late" is optionally disabled', function () { const props = defaultProps({ sortBySetting: { disabled: true } }); const menuItem = findMenuItem.call(this, props, 'Sort by', 'Late'); @@ -357,6 +398,17 @@ test('clicking "Unposted" calls onSortByUnposted', function () { strictEqual(onSortByUnposted.callCount, 1); }); +test('clicking "Unposted" focuses menu trigger', function () { + const onSortByUnposted = this.stub(); + const props = defaultProps({ sortBySetting: { onSortByUnposted } }); + const menuItem = findMenuItem.call(this, props, 'Sort by', 'Unposted'); + const focusStub = this.stub(this.wrapper.instance(), 'focusAtEnd') + + menuItem.simulate('click'); + + equal(focusStub.callCount, 1); +}); + test('"Unposted" is optionally disabled', function () { const props = defaultProps({ sortBySetting: { disabled: true } }); const menuItem = findMenuItem.call(this, props, 'Sort by', 'Unposted'); @@ -395,13 +447,16 @@ test('Curve Grades menu item is enabled when isDisabled is false', function () { notOk(menuItem.parentElement.parentElement.parentElement.getAttribute('aria-disabled')); }); -test('onSelect is called when menu item is clicked', function () { +test('clicking the menu item invokes onSelect with correct callback', function () { const onSelect = this.stub(); const props = defaultProps({ curveGradesAction: { onSelect } }); this.wrapper = mountAndOpenOptions(props); const menuItem = document.querySelector('[data-menu-item-id="curve-grades"]'); + menuItem.click(); + equal(onSelect.callCount, 1); + equal(onSelect.getCall(0).args[0], this.wrapper.instance().focusAtEnd); }); test('the Curve Grades dialog has focus when it is invoked', function () { @@ -457,14 +512,15 @@ test('disables the menu item when submissions are not loaded', function () { equal(menuItem.parentElement.parentElement.parentElement.getAttribute('aria-disabled'), 'true'); }); -test('clicking the menu item invokes the Message Students Who dialog', function () { +test('clicking the menu item invokes the Message Students Who dialog with correct callback', function () { this.wrapper = mountAndOpenOptions(this.props); - this.stub(window, 'messageStudents'); - + const messageStudents = this.stub(window, 'messageStudents'); const menuItem = document.querySelector('[data-menu-item-id="message-students-who"]'); + menuItem.click(); - equal(window.messageStudents.callCount, 1); + equal(messageStudents.callCount, 1); + equal(messageStudents.getCall(0).args[0].onClose, this.wrapper.instance().focusAtEnd); }); QUnit.module('AssignmentColumnHeader: Mute/Unmute Assignment Action', { @@ -505,7 +561,7 @@ test('disables the option when prop muteAssignmentAction.disabled is truthy', fu equal(specificMenuItem.parentElement.parentElement.parentElement.getAttribute('aria-disabled'), 'true'); }); -test('clicking the option invokes prop muteAssignmentAction.onSelect', function () { +test('clicking the menu item invokes onSelect with correct callback', function () { this.props.muteAssignmentAction.onSelect = this.stub(); this.wrapper = mountAndOpenOptions(this.props); @@ -513,6 +569,7 @@ test('clicking the option invokes prop muteAssignmentAction.onSelect', function specificMenuItem.click(); equal(this.props.muteAssignmentAction.onSelect.callCount, 1); + equal(this.props.muteAssignmentAction.onSelect.getCall(0).args[0], this.wrapper.instance().focusAtEnd); }); test('the Assignment Muting dialog has focus when it is invoked', function () { @@ -618,7 +675,7 @@ test('disables the menu item when the disabled prop is true', function () { equal(specificMenuItem.parentElement.parentElement.parentElement.getAttribute('aria-disabled'), 'true'); }); -test('clicking the menu item invokes the onSelect handler', function () { +test('clicking the menu item invokes onSelect with correct callback', function () { this.props.setDefaultGradeAction.onSelect = this.stub(); this.wrapper = mountAndOpenOptions(this.props); @@ -626,6 +683,7 @@ test('clicking the menu item invokes the onSelect handler', function () { specificMenuItem.click(); equal(this.props.setDefaultGradeAction.onSelect.callCount, 1); + equal(this.props.setDefaultGradeAction.onSelect.getCall(0).args[0], this.wrapper.instance().focusAtEnd); }); test('the Set Default Grade dialog has focus when it is invoked', function () { @@ -674,7 +732,7 @@ test('does not render the menu item when the hidden prop is true', function () { equal(specificMenuItem, null); }); -test('clicking the menu item invokes the onSelect handler', function () { +test('clicking the menu item invokes onSelect with correct callback', function () { this.props.downloadSubmissionsAction.onSelect = this.stub(); this.wrapper = mountAndOpenOptions(this.props); @@ -682,6 +740,7 @@ test('clicking the menu item invokes the onSelect handler', function () { specificMenuItem.click(); equal(this.props.downloadSubmissionsAction.onSelect.callCount, 1); + equal(this.props.downloadSubmissionsAction.onSelect.getCall(0).args[0], this.wrapper.instance().focusAtEnd); }); QUnit.module('AssignmentColumnHeader: Reupload Submissions Action', { @@ -712,7 +771,7 @@ test('does not render the menu item when the hidden prop is true', function () { equal(specificMenuItem, null); }); -test('clicking the menu item invokes the onSelect property', function () { +test('clicking the menu item invokes the onSelect property with correct callback', function () { this.props.reuploadSubmissionsAction.onSelect = this.stub(); this.wrapper = mountAndOpenOptions(this.props); @@ -720,6 +779,7 @@ test('clicking the menu item invokes the onSelect property', function () { specificMenuItem.click(); equal(this.props.reuploadSubmissionsAction.onSelect.callCount, 1); + equal(this.props.reuploadSubmissionsAction.onSelect.getCall(0).args[0], this.wrapper.instance().focusAtEnd); }); QUnit.module('AssignmentColumnHeader#handleKeyDown', function (hooks) { diff --git a/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentGroupColumnHeaderSpec.js b/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentGroupColumnHeaderSpec.js index ca683342a84..0842fe469f0 100644 --- a/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentGroupColumnHeaderSpec.js +++ b/spec/javascripts/jsx/gradezilla/default_gradebook/components/AssignmentGroupColumnHeaderSpec.js @@ -94,16 +94,20 @@ test('adds a class to the trigger when the PopoverMenu is opened', function () { }); test('calls addGradebookElement prop on open', function () { + notOk(this.props.addGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.addGradebookElement.callCount, 1); + ok(this.props.addGradebookElement.called); }); test('calls removeGradebookElement prop on close', function () { + notOk(this.props.removeGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.removeGradebookElement.callCount, 1); + ok(this.props.removeGradebookElement.called); }); test('calls onMenuClose prop on close', function () { diff --git a/spec/javascripts/jsx/gradezilla/default_gradebook/components/StudentColumnHeaderSpec.js b/spec/javascripts/jsx/gradezilla/default_gradebook/components/StudentColumnHeaderSpec.js index bcd33f9b747..2206ab59c87 100644 --- a/spec/javascripts/jsx/gradezilla/default_gradebook/components/StudentColumnHeaderSpec.js +++ b/spec/javascripts/jsx/gradezilla/default_gradebook/components/StudentColumnHeaderSpec.js @@ -111,16 +111,20 @@ test('renders a title for the More icon', function () { }); test('calls addGradebookElement prop on open', function () { + notOk(this.props.addGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.addGradebookElement.callCount, 1); + ok(this.props.addGradebookElement.called); }); test('calls removeGradebookElement prop on close', function () { + notOk(this.props.removeGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.removeGradebookElement.callCount, 1); + ok(this.props.removeGradebookElement.called); }); test('calls onMenuClose prop on close', function () { diff --git a/spec/javascripts/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeaderSpec.js b/spec/javascripts/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeaderSpec.js index 061a0cd633f..e1ac68e08a8 100644 --- a/spec/javascripts/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeaderSpec.js +++ b/spec/javascripts/jsx/gradezilla/default_gradebook/components/TotalGradeColumnHeaderSpec.js @@ -113,16 +113,20 @@ test('renders a title for the More icon based on the assignment name', function }); test('calls addGradebookElement prop on open', function () { + notOk(this.props.addGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.addGradebookElement.callCount, 1); + ok(this.props.addGradebookElement.called); }); test('calls removeGradebookElement prop on close', function () { + notOk(this.props.removeGradebookElement.called); + this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); this.wrapper.find('.Gradebook__ColumnHeaderAction').simulate('click'); - strictEqual(this.props.removeGradebookElement.callCount, 1); + ok(this.props.removeGradebookElement.called); }); test('calls onMenuClose prop on close', function () { diff --git a/spec/selenium/grades/gradezilla/gradebook_a11y_spec.rb b/spec/selenium/grades/gradezilla/gradebook_a11y_spec.rb index 3dea236860f..d53728d0625 100644 --- a/spec/selenium/grades/gradezilla/gradebook_a11y_spec.rb +++ b/spec/selenium/grades/gradezilla/gradebook_a11y_spec.rb @@ -355,4 +355,26 @@ describe "Gradezilla" do end end end + + context "assignment header focus" do + before { Gradezilla.visit(@course)} + let(:assignment) { @course.assignments.first } + + it 'is placed on assignment header trigger upon sort' do + Gradezilla.click_assignment_header_menu(assignment.id) + Gradezilla.click_assignment_popover_sort_by('low-to-high') + + check_element_has_focus Gradezilla.assignment_header_menu_trigger_element(assignment.title) + end + + %w[message-students-who curve-grades set-default-grade assignment-muter download-submissions].each do |dialog| + it "is placed on assignment header trigger upon #{dialog} dialog close" do + Gradezilla.click_assignment_header_menu(assignment.id) + Gradezilla.click_assignment_header_menu_element(dialog) + Gradezilla.close_open_dialog + + check_element_has_focus Gradezilla.assignment_header_menu_trigger_element(assignment.title) + end + end + end end diff --git a/spec/selenium/grades/page_objects/gradezilla_page.rb b/spec/selenium/grades/page_objects/gradezilla_page.rb index 8dc8d886d45..6938df680c9 100644 --- a/spec/selenium/grades/page_objects/gradezilla_page.rb +++ b/spec/selenium/grades/page_objects/gradezilla_page.rb @@ -592,6 +592,10 @@ class Gradezilla assignment_header_menu_element(id) end + def assignment_header_menu_trigger_element(assignment_name) + assignment_header_cell_element(assignment_name).find_element(:css, '.Gradebook__ColumnHeaderAction') + end + def assignment_header_menu_item_selector(item) menu_item_id = "" @@ -608,6 +612,10 @@ class Gradezilla ".container_1 .slick-header-column[id*=assignment_#{assignment_id}] svg[name=IconMutedSolid]" end + def close_open_dialog + fj('.ui-dialog-titlebar-close:visible').click + end + def select_assignment_header_warning_icon assignment_header_warning_icon_element end