Restore navigation flow when closing the pace modal

Fix focus loss when closing the pace modal, restoring the focus to
the element that opened the modal

closes LS-3625
flag=course_paces_redesign

Test plan:
- Go to the course pace landing page
- Open the default course pace modal
- Do some stuff and close it
- Expect the focus to come back to the Edit default pace button
- Repeat the process for section and student paces
- Make sure the focus gets restored when closing the modal using the X
, the Close button, or the Discard Changes button when attempting to
close the modal without saving

Change-Id: Ie3ee9374afecf813433495c1ef0fa313f8c4ec96
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/306475
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jackson Howe <jackson.howe@instructure.com>
QA-Review: Jackson Howe <jackson.howe@instructure.com>
Product-Review: Jonathan Guardado <jonathan.guardado@instructure.com>
This commit is contained in:
Jonathan Guardado 2022-12-02 06:43:55 -07:00
parent 613f951bd6
commit 4c02449058
4 changed files with 41 additions and 1 deletions

View File

@ -39,6 +39,7 @@ import {getCoursePace, isNewPace} from '../../reducers/course_paces'
import {PaceContext, CoursePace, StoreState, ResponsiveSizes} from '../../types'
import {actions} from '../../actions/ui'
import {paceContextsActions} from '../../actions/pace_contexts'
import {generateModalLauncherId} from '../../utils/utils'
const I18n = useI18nScope('course_paces_header')
@ -181,6 +182,10 @@ export const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
margin={props.responsiveSize === 'large' ? '0' : 'small 0 0'}
>
<Link
id={generateModalLauncherId({
type: 'Course',
item_id: window.ENV.COURSE_ID,
} as PaceContext)}
isWithinText={false}
data-testid="go-to-default-pace"
onClick={() => {

View File

@ -36,6 +36,7 @@ import {Spinner} from '@instructure/ui-spinner'
import Paginator from '@canvas/instui-bindings/react/Paginator'
import {formatTimeAgoDate} from '../utils/date_stuff/date_helpers'
import {paceContextsActions} from '../actions/pace_contexts'
import {generateModalLauncherId} from '../utils/utils'
const I18n = useI18nScope('course_paces_app')
@ -143,7 +144,11 @@ const PaceContextsTable = ({
}
const renderContextLink = (paceContext: PaceContext) => (
<Link isWithinText={false} onClick={() => handleContextSelect(paceContext)}>
<Link
id={generateModalLauncherId(paceContext)}
isWithinText={false}
onClick={() => handleContextSelect(paceContext)}
>
<TruncateText>{paceContext.name}</TruncateText>
</Link>
)

View File

@ -62,6 +62,7 @@ import PaceModalHeading from './heading'
import {getSelectedPaceContext} from '../../reducers/pace_contexts'
import {getEnrolledSection} from '../../reducers/enrollments'
import PaceModalStats from './stats'
import {generateModalLauncherId} from '../../utils/utils'
const I18n = useI18nScope('course_paces_modal')
@ -116,12 +117,18 @@ export const PaceModal: React.FC<PassedProps & DispatchProps & StoreProps> = pro
return `${title}: ${props.paceName}`
}
const restoreFocus = () => {
const launcherId = generateModalLauncherId(props.selectedPaceContext)
document.getElementById(launcherId)?.focus()
}
const handleClose = () => {
if (props.unappliedChangesExist) {
setPendingContext(props.coursePace.context_type)
} else {
props.clearCategoryError('publish')
props.onClose()
restoreFocus()
}
}
@ -223,6 +230,7 @@ export const PaceModal: React.FC<PassedProps & DispatchProps & StoreProps> = pro
props.onResetPace()
props.clearCategoryError('publish')
props.onClose()
restoreFocus()
}}
contextType={props.coursePace.context_type}
/>

View File

@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {PaceContext} from '../types'
export const generateModalLauncherId = (paceContext: PaceContext) =>
`pace-modal-launcher-${paceContext.type}-${paceContext.item_id}`