Skip to content

Commit 94257e6

Browse files
ToMESSKamatyasf
authored andcommitted
chore: make side navigation bar trap focus
INSTUI-4266
1 parent 5c02971 commit 94257e6

File tree

1 file changed

+52
-2
lines changed

1 file changed

+52
-2
lines changed

packages/__docs__/src/App/index.tsx

Lines changed: 52 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,40 @@ 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.state.showMenu) {
387+
if (this.navFocusRegion) {
388+
this.navFocusRegion.activate()
389+
} else if (this.navRef.current) {
390+
this.navFocusRegion = new FocusRegion(
391+
this.navRef.current,
392+
this.getFocusRegionConfig()
393+
)
394+
this.navFocusRegion.activate()
395+
}
396+
}
397+
}
398+
399+
handleFocusChange = (e: FocusEvent) => {
400+
const isFocusInsideNav =
401+
e.target && this.navRef.current?.contains(e.target as Node)
402+
if (!isFocusInsideNav && this.navFocusRegion) {
403+
this.navFocusRegion.deactivate()
404+
this.navFocusRegion = null
405+
} else if (isFocusInsideNav && !this.navFocusRegion) {
406+
this.handleNavigationFocusRegion()
407+
}
408+
}
409+
362410
renderThemeSelect() {
363411
const themeKeys = Object.keys(this.state.docsData!.themes)
364412
const smallScreen = this.state.layout === 'small'
@@ -712,7 +760,9 @@ class App extends Component<AppProps, AppState> {
712760
)
713761

714762
return layout !== 'small' ? (
715-
<nav css={this.props.styles?.inlineNavigation}>{navContent}</nav>
763+
<nav css={this.props.styles?.inlineNavigation} ref={this.navRef}>
764+
{navContent}
765+
</nav>
716766
) : (
717767
<Tray label="Navigation" open={showMenu} onDismiss={this.handleMenuClose}>
718768
{navContent}

0 commit comments

Comments
 (0)