Adds K-5 schedule tab jump to navigation button
Replaces the hidden duplicate weekly nav toolbar with a separate "Jump to weekly navigation" button that returns focus to the lone navigation toolbar (for better keyboard-only/screenreader usability). fixes LS-2131 flag = canvas_for_elementary Test plan: - As a student enrolled in a K-5 course, go to the dashboard - Switch to the schedule tab - Tab to the bottom of the planner - Expect a hidden button to appear reading "Jump to navigation toolbar" - Press enter, and expect the button to take you back to the "Today" button - Press left or right to switch to one of the next/previous buttons - Tab back to the "Jump to navigation toolbar" button - Press enter, expect focus to jump to whatever button was last active on the toolbar (next/previous) Change-Id: Ic715d64603253412d2b97e961988ff9fcd6e92a5 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/263277 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Robin Kuss <rkuss@instructure.com> QA-Review: Robin Kuss <rkuss@instructure.com> Product-Review: Jeff Largent <jeff.largent@instructure.com>
This commit is contained in:
parent
947836e149
commit
d659447da7
|
@ -135,3 +135,7 @@ if (!('matchMedia' in window)) {
|
|||
})
|
||||
window.matchMedia._mocked = true
|
||||
}
|
||||
|
||||
if (!('scrollIntoView' in window.HTMLElement.prototype)) {
|
||||
window.HTMLElement.prototype.scrollIntoView = () => {}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - 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 React, {PureComponent} from 'react'
|
||||
import {Button} from '@instructure/ui-buttons'
|
||||
import formatMessage from '../../format-message'
|
||||
|
||||
import {WEEKLY_PLANNER_ACTIVE_BTN_ID} from '../WeeklyPlannerHeader'
|
||||
|
||||
export const WEEKLY_PLANNER_JUMP_TO_NAV_BUTTON = 'jump-to-weekly-nav-button'
|
||||
|
||||
export default class JumpToHeaderButton extends PureComponent {
|
||||
buttonRef = null
|
||||
|
||||
state = {focused: false}
|
||||
|
||||
setFocused = focused => () => {
|
||||
this.setState({focused}, () => this.buttonRef.scrollIntoView(false))
|
||||
}
|
||||
|
||||
focusHeader = () => {
|
||||
document.getElementById(WEEKLY_PLANNER_ACTIVE_BTN_ID)?.focus()
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
opacity: this.state.focused ? '1' : '0',
|
||||
position: this.state.focused ? 'static' : 'absolute'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={WEEKLY_PLANNER_JUMP_TO_NAV_BUTTON}
|
||||
data-testid={WEEKLY_PLANNER_JUMP_TO_NAV_BUTTON}
|
||||
onClick={this.focusHeader}
|
||||
onBlur={this.setFocused(false)}
|
||||
onFocus={this.setFocused(true)}
|
||||
elementRef={e => {
|
||||
this.buttonRef = e
|
||||
}}
|
||||
>
|
||||
{formatMessage('Jump to navigation toolbar')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -35,6 +35,8 @@ import {isInMomentRange} from '../../utilities/dateUtils'
|
|||
import theme from './theme'
|
||||
import styles from './styles.css'
|
||||
|
||||
export const WEEKLY_PLANNER_ACTIVE_BTN_ID = 'weekly-header-active-button'
|
||||
|
||||
// Breaking our encapsulation by reaching outside our dom sub-tree
|
||||
// I suppose we could wire up the event handlers in K5Dashboard.js
|
||||
// and pass the height as a prop to all the pages. Maybe it will be
|
||||
|
@ -69,7 +71,6 @@ export class WeeklyPlannerHeader extends Component {
|
|||
loadingError: PropTypes.string
|
||||
}).isRequired,
|
||||
visible: PropTypes.bool,
|
||||
isFooter: PropTypes.bool,
|
||||
todayMoment: momentObj,
|
||||
weekStartMoment: momentObj,
|
||||
weekEndMoment: momentObj,
|
||||
|
@ -88,9 +89,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
prevEnabled: true,
|
||||
nextEnabled: true,
|
||||
focusedButtonIndex: 1, // start with the today button
|
||||
buttons: [this.prevButtonRef, this.todayButtonRef, this.nextButtonRef],
|
||||
focused: false,
|
||||
activeButton: 0 // -1 for prev, 0 for today, 1 for next
|
||||
buttons: [this.prevButtonRef, this.todayButtonRef, this.nextButtonRef]
|
||||
}
|
||||
|
||||
handleStickyOffset = () => {
|
||||
|
@ -100,14 +99,14 @@ export class WeeklyPlannerHeader extends Component {
|
|||
handlePrev = () => {
|
||||
this.prevButtonRef.current.focus()
|
||||
this.props.loadPastWeekItems()
|
||||
this.setState({focusedButtonIndex: 0, activeButton: -1})
|
||||
this.setState({focusedButtonIndex: 0})
|
||||
}
|
||||
|
||||
handleToday = () => {
|
||||
this.todayButtonRef.current.focus()
|
||||
this.props.loadThisWeekItems()
|
||||
this.setState((state, _props) => {
|
||||
return {focusedButtonIndex: state.prevEnabled ? 1 : 0, activeButton: 0}
|
||||
return {focusedButtonIndex: state.prevEnabled ? 1 : 0}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -115,7 +114,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
this.nextButtonRef.current.focus()
|
||||
this.props.loadNextWeekItems({loadMoreButtonClicked: true})
|
||||
this.setState((state, _props) => {
|
||||
return {focusedButtonIndex: state.prevEnabled ? 2 : 1, activeButton: 1}
|
||||
return {focusedButtonIndex: state.prevEnabled ? 2 : 1}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -133,14 +132,6 @@ export class WeeklyPlannerHeader extends Component {
|
|||
this.setState({focusedButtonIndex: newFocusedIndex})
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({focused: true})
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
this.setState({focused: false})
|
||||
}
|
||||
|
||||
updateButtons() {
|
||||
const buttons = []
|
||||
|
||||
|
@ -187,7 +178,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
// 2. the window becomes narrow enough for the tabs to wrap.
|
||||
// We need to relocate the WeeklyPlannerHeader so it sticks
|
||||
// to the bottom of the tabs panel.
|
||||
if (!this.props.isFooter && this.props.visible !== prevProps.visible) {
|
||||
if (this.props.visible !== prevProps.visible) {
|
||||
if (this.props.visible) {
|
||||
const focusTarget = processFocusTarget()
|
||||
this.handleStickyOffset()
|
||||
|
@ -215,7 +206,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
) {
|
||||
const buttons = this.updateButtons()
|
||||
|
||||
if (!this.props.isFooter && prevState.buttons.length === 3 && buttons.length === 2) {
|
||||
if (prevState.buttons.length === 3 && buttons.length === 2) {
|
||||
// when prev or next buttons go away, move focus to Today
|
||||
this.todayButtonRef.current.focus()
|
||||
}
|
||||
|
@ -237,30 +228,21 @@ export class WeeklyPlannerHeader extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let prevButtonId, todayButtonId, nextButtonId
|
||||
if (!this.props.isFooter) {
|
||||
prevButtonId = this.state.activeButton === -1 ? 'weekly-header-active-button' : undefined
|
||||
todayButtonId = this.state.activeButton === 0 ? 'weekly-header-active-button' : undefined
|
||||
nextButtonId = this.state.activeButton === 1 ? 'weekly-header-active-button' : undefined
|
||||
getButtonId(which) {
|
||||
return this.getButtonTabIndex(which) === 0 ? WEEKLY_PLANNER_ACTIVE_BTN_ID : undefined
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
id={this.props.isFooter ? 'weekly_planner_footer' : 'weekly_planner_header'}
|
||||
data-testid={this.props.isFooter ? 'WeeklyPlannerFooter' : 'WeeklyPlannerHeader'}
|
||||
className={`${styles.root} ${
|
||||
this.props.isFooter ? 'WeeklyPlannerFooter' : 'WeeklyPlannerHeader'
|
||||
}`}
|
||||
style={{
|
||||
top: `${this.state.stickyOffset}px`,
|
||||
opacity: `${this.props.isFooter && !this.state.focused ? 0 : 1}`
|
||||
}}
|
||||
id="weekly_planner_header"
|
||||
data-testid="WeeklyPlannerHeader"
|
||||
className={`${styles.root} WeeklyPlannerHeader`}
|
||||
style={{top: `${this.state.stickyOffset}px`}}
|
||||
role="toolbar"
|
||||
aria-label={formatMessage('Weekly schedule navigation')}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
{this.props.loading.loadingError && !this.props.isFooter && (
|
||||
{this.props.loading.loadingError && (
|
||||
<div className={styles.errorbox}>
|
||||
<ErrorAlert error={this.props.loading.loadingError} margin="xx-small">
|
||||
{formatMessage('Error loading items')}
|
||||
|
@ -275,7 +257,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
onKeyDown={this.handleKey}
|
||||
>
|
||||
<IconButton
|
||||
id={prevButtonId}
|
||||
id={this.getButtonId('prev')}
|
||||
onClick={this.handlePrev}
|
||||
screenReaderLabel={formatMessage('View previous week')}
|
||||
interaction={this.state.prevEnabled ? 'enabled' : 'disabled'}
|
||||
|
@ -285,7 +267,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
<IconArrowOpenStartLine />
|
||||
</IconButton>
|
||||
<Button
|
||||
id={todayButtonId}
|
||||
id={this.getButtonId('today')}
|
||||
margin="0 xx-small"
|
||||
onClick={this.handleToday}
|
||||
ref={this.todayButtonRef}
|
||||
|
@ -296,7 +278,7 @@ export class WeeklyPlannerHeader extends Component {
|
|||
</AccessibleContent>
|
||||
</Button>
|
||||
<IconButton
|
||||
id={nextButtonId}
|
||||
id={this.getButtonId('next')}
|
||||
onClick={this.handleNext}
|
||||
screenReaderLabel={formatMessage('View next week')}
|
||||
interaction={this.state.nextEnabled ? 'enabled' : 'disabled'}
|
||||
|
|
|
@ -33,10 +33,6 @@
|
|||
background-image: linear-gradient(to top, rgba(255, 255, 255, 0), #fafafa);
|
||||
}
|
||||
|
||||
.WeeklyPlannerFooter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.errorbox {
|
||||
background: var(--backgroundPrimary);
|
||||
z-index: 2;
|
||||
|
|
|
@ -20,4 +20,5 @@
|
|||
// to make them themeable by consumers.
|
||||
export {default as Day} from './Day'
|
||||
export {default as Grouping} from './Grouping'
|
||||
export {default as JumpToHeaderButton} from './JumpToHeaderButton'
|
||||
export {default as PlannerItem} from './PlannerItem'
|
||||
|
|
|
@ -394,14 +394,27 @@ describe('K-5 Dashboard', () => {
|
|||
|
||||
const header = await findByTestId('WeeklyPlannerHeader')
|
||||
expect(header).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const footer = await findByTestId('WeeklyPlannerFooter')
|
||||
expect(footer).toBeInTheDocument()
|
||||
it('renders an "jump to navigation" button at the bottom of the schedule tab', async () => {
|
||||
const {findByRole} = render(
|
||||
<K5Dashboard {...defaultProps} defaultTab="tab-schedule" plannerEnabled />
|
||||
)
|
||||
|
||||
const jumpToNavButton = await findByRole('button', {name: 'Jump to navigation toolbar'})
|
||||
expect(jumpToNavButton).not.toBeVisible()
|
||||
|
||||
act(() => jumpToNavButton.focus())
|
||||
expect(jumpToNavButton).toBeVisible()
|
||||
|
||||
act(() => jumpToNavButton.click())
|
||||
expect(document.activeElement.id).toBe('weekly-header-active-button')
|
||||
expect(jumpToNavButton).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('displays a teacher preview if the user has no student enrollments', async () => {
|
||||
const {findByTestId, getByText} = render(
|
||||
<K5Dashboard {...defaultProps} defaultTab="tab-schedule" plannerEnable={false} />
|
||||
<K5Dashboard {...defaultProps} defaultTab="tab-schedule" plannerEnabled={false} />
|
||||
)
|
||||
|
||||
expect(await findByTestId('kinder-panda')).toBeInTheDocument()
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {createPlannerApp, renderWeeklyPlannerHeader} from '@instructure/canvas-planner'
|
||||
import {
|
||||
createPlannerApp,
|
||||
renderWeeklyPlannerHeader,
|
||||
JumpToHeaderButton
|
||||
} from '@instructure/canvas-planner'
|
||||
|
||||
const SchedulePage = ({visible = false}) => {
|
||||
const [isPlannerCreated, setPlannerCreated] = useState(false)
|
||||
|
@ -41,7 +45,7 @@ const SchedulePage = ({visible = false}) => {
|
|||
>
|
||||
{renderWeeklyPlannerHeader({visible})}
|
||||
{isPlannerCreated && plannerApp.current}
|
||||
{isPlannerCreated && renderWeeklyPlannerHeader({visible, isFooter: true})}
|
||||
{isPlannerCreated && <JumpToHeaderButton />}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue