Skip to content

Commit 9126b44

Browse files
committed
chore: make side navigation bar trap focus
INSTUI-4266
1 parent 48b5a4a commit 9126b44

File tree

1 file changed

+50
-2
lines changed

1 file changed

+50
-2
lines changed

packages/__docs__/src/App/index.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import type {
7676
} from '../../buildScripts/DataTypes.mjs'
7777
import { logError } from '@instructure/console'
7878
import type { Spacing } from '@instructure/emotion'
79+
import { FocusRegion } from '@instructure/ui-a11y-utils'
7980

8081
type AppContextType = {
8182
themeKey: keyof MainDocsData['themes']
@@ -94,6 +95,9 @@ class App extends Component<AppProps, AppState> {
9495
static propTypes = propTypes
9596
static allowedProps = allowedProps
9697

98+
private navRef = createRef<HTMLElement>()
99+
private navFocusRegion: FocusRegion | null = null
100+
97101
static defaultProps = {
98102
trayWidth: 300
99103
}
@@ -222,10 +226,18 @@ class App extends Component<AppProps, AppState> {
222226
})
223227
})
224228
.catch(errorHandler)
229+
document.addEventListener('focusin', this.handleFocusChange)
225230
}
226231

227-
componentDidUpdate() {
232+
componentDidUpdate(_prevProps: AppProps, prevState: AppState) {
228233
this.props.makeStyles?.()
234+
235+
if (
236+
prevState.showMenu !== this.state.showMenu ||
237+
prevState.layout !== this.state.layout
238+
) {
239+
this.handleNavigationFocusRegion()
240+
}
229241
}
230242

231243
componentWillUnmount() {
@@ -236,6 +248,8 @@ class App extends Component<AppProps, AppState> {
236248
if (this._mediaQueryListener) {
237249
this._mediaQueryListener.remove()
238250
}
251+
document.removeEventListener('focusin', this.handleFocusChange)
252+
this.navFocusRegion?.deactivate()
239253
}
240254

241255
trackPage(page: string) {
@@ -359,6 +373,38 @@ class App extends Component<AppProps, AppState> {
359373
}
360374
}
361375

376+
getFocusRegionConfig() {
377+
return {
378+
shouldContainFocus: true,
379+
shouldCloseOnEscape: true,
380+
shouldReturnFocus: false,
381+
onDismiss: this.handleMenuClose
382+
}
383+
}
384+
385+
handleNavigationFocusRegion() {
386+
if (this.navFocusRegion) {
387+
this.navFocusRegion.activate()
388+
} else if (this.navRef.current) {
389+
this.navFocusRegion = new FocusRegion(
390+
this.navRef.current,
391+
this.getFocusRegionConfig()
392+
)
393+
this.navFocusRegion.activate()
394+
}
395+
}
396+
397+
handleFocusChange = (e: FocusEvent) => {
398+
const isFocusInsideNav =
399+
e.target && this.navRef.current?.contains(e.target as Node)
400+
if (isFocusInsideNav) {
401+
this.handleNavigationFocusRegion()
402+
} else if (this.navFocusRegion) {
403+
this.navFocusRegion.deactivate()
404+
this.navFocusRegion = null
405+
}
406+
}
407+
362408
renderThemeSelect() {
363409
const themeKeys = Object.keys(this.state.docsData!.themes)
364410
const smallScreen = this.state.layout === 'small'
@@ -712,7 +758,9 @@ class App extends Component<AppProps, AppState> {
712758
)
713759

714760
return layout !== 'small' ? (
715-
<nav css={this.props.styles?.inlineNavigation}>{navContent}</nav>
761+
<nav css={this.props.styles?.inlineNavigation} ref={this.navRef}>
762+
{navContent}
763+
</nav>
716764
) : (
717765
<Tray label="Navigation" open={showMenu} onDismiss={this.handleMenuClose}>
718766
{navContent}

0 commit comments

Comments
 (0)