restore column header focus after dialog close

fixes: CNVS-37602

Test Plan:
1. Navigate to GradeZilla
2. Open an assignment column header menu
3. Press keys while the menu is open
  - When you press TAB:
     Observe that the menu closes and the next item
     in the normal grid navigation flow is focused
  - When you press SHIFT+TAB:
     Observe that the menu closes and the previous item
     in the normal grid navigation flow is focused
  - When you press ESC:
     Observe that the menu closes and the menu trigger
     is focused
  - Other keys should behave as before
     a. up/down arrow keys focus menu items
     b. left/right arrow keys expand/collapse flyouts
  - Nested flyouts should also trigger navigation upon
    these key presses
4. Select a menu item
 - If a modal appears:
  a. Observe that the focus is placed within the modal
  b. Observe the header menu trigger is refocused
     when the modal is closed
 - If a modal doesn't appear:
  a. Observe the header menu trigger is refocused
5. Repeat 2-4 with all column header types
 - Assignment Group column headers
 - Student column headers
 - Total Grade column header

Change-Id: I7cd50a5bc2c598b5bf899f7a5d17998fc4f4ec04
Reviewed-on: https://gerrit.instructure.com/115864
Reviewed-by: Jeremy Neander <jneander@instructure.com>
Tested-by: Jenkins
Reviewed-by: Sheldon Leibole <sheldon@siimpl.io>
QA-Review: Anju Reddy <areddy@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Brian Park 2017-06-15 16:56:32 -07:00 committed by Keith T. Garner
parent 070f643531
commit a05e102e68
24 changed files with 371 additions and 129 deletions

View File

@ -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,

View File

@ -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) =>

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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);
}
};
}

View File

@ -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 {
</MenuItemGroup>
</MenuItemFlyout>
<MenuItem
disabled={!this.props.submissionsLoaded}
onSelect={this.showMessageStudentsWhoDialog}
@ -276,21 +289,21 @@ class AssignmentColumnHeader extends ColumnHeader {
<MenuItem
disabled={this.props.curveGradesAction.isDisabled}
onSelect={this.props.curveGradesAction.onSelect}
onSelect={this.curveGrades}
>
<span data-menu-item-id="curve-grades">{I18n.t('Curve Grades')}</span>
</MenuItem>
<MenuItem
disabled={this.props.setDefaultGradeAction.disabled}
onSelect={this.props.setDefaultGradeAction.onSelect}
onSelect={this.setDefaultGrades}
>
<span data-menu-item-id="set-default-grade">{I18n.t('Set Default Grade')}</span>
</MenuItem>
<MenuItem
disabled={this.props.muteAssignmentAction.disabled}
onSelect={this.props.muteAssignmentAction.onSelect}
onSelect={this.muteAssignment}
>
<span data-menu-item-id="assignment-muter">
{this.props.assignment.muted ? I18n.t('Unmute Assignment') : I18n.t('Mute Assignment')}
@ -306,14 +319,14 @@ class AssignmentColumnHeader extends ColumnHeader {
{
!this.props.downloadSubmissionsAction.hidden &&
<MenuItem onSelect={this.props.downloadSubmissionsAction.onSelect}>
<MenuItem onSelect={this.downloadSubmissions}>
<span data-menu-item-id="download-submissions">{I18n.t('Download Submissions')}</span>
</MenuItem>
}
{
!this.props.reuploadSubmissionsAction.hidden &&
<MenuItem onSelect={this.props.reuploadSubmissionsAction.onSelect}>
<MenuItem onSelect={this.reuploadSubmissions}>
<span data-menu-item-id="reupload-submissions">{I18n.t('Re-Upload Submissions')}</span>
</MenuItem>
}

View File

@ -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

View File

@ -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 {

View File

@ -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 &&
<MenuItem
disabled={this.props.gradeDisplay.disabled}
onSelect={this.props.gradeDisplay.onSelect}
onSelect={this.switchGradeDisplay}
>
<span data-menu-item-id="grade-display-switcher" style={nowrapStyle}>
{displayAsPoints ? I18n.t('Display as Percentage') : I18n.t('Display as Points')}

View File

@ -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 () {

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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'));

View File

@ -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() {

View File

@ -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 = "<a href=\"" + htmlEscape(url) + "\"><b> " + htmlEscape(I18n.t("#submissions.click_to_download", "Click here to download %{size_of_file}", {size_of_file: attachment.readable_size})) + "</b></a>"
$("#download_submissions_dialog .status").html(htmlEscape(message) + "<br>" + $.raw(link));
$("#download_submissions_dialog .status_loader").css('visibility', 'hidden');
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 = `<a href="${htmlEscape(url)}"><b>${htmlEscape(linkText)}</b></a>`;
$('#download_submissions_dialog .status').html(`${htmlEscape(message)}<br>${$.raw(link)}`);
$('#download_submissions_dialog .status_loader').css('visibility', 'hidden');
location.href = url;
return;
} else {
var progress = parseInt(attachment.file_state, 10);
if(isNaN(progress)) { progress = 0; }
let 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...");
$('#download_submissions_dialog .progress').progressbar('value', progress);
let 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)});
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() {});
$('#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');
$('#download_submissions_dialog .status_loader').css('visibility', 'hidden');
setTimeout(checkForChange, 3000);
}, function(data) {
$("#download_submissions_dialog .status_loader").css('visibility', 'hidden');
}, () => {
$('#download_submissions_dialog .status_loader').css('visibility', 'hidden');
setTimeout(checkForChange, 1000);
});
}
checkForChange();
};
checkForChange();
};

View File

@ -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)

View File

@ -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 = {};

View File

@ -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) {

View File

@ -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 () {

View File

@ -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 () {

View File

@ -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 () {

View File

@ -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

View File

@ -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