Skip to content

Commit

Permalink
chore: add 'Skip to main content' button to the main page
Browse files Browse the repository at this point in the history
Closes: INSTUI-4240
chore: add 'Skip to main content' button to the main page
  • Loading branch information
ToMESSKa committed Jan 28, 2025
1 parent 6c85b31 commit 87a856f
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 35 deletions.
152 changes: 127 additions & 25 deletions packages/__docs__/src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
createContext,
LegacyRef,
ReactElement,
SyntheticEvent
SyntheticEvent,
createRef
} from 'react'

import { Alert } from '@instructure/ui-alerts'
Expand Down Expand Up @@ -106,6 +107,10 @@ class App extends Component<AppProps, AppState> {
_mediaQueryListener?: ReturnType<typeof addMediaQueryMatchListener>
_defaultDocumentTitle?: string
_controller?: AbortController
_heroRef: React.RefObject<Hero>
_navRef: React.RefObject<Nav>
_skipToMainButtonRef?: HTMLElement
_mainContentRef?: HTMLElement

constructor(props: AppProps) {
super(props)
Expand All @@ -127,6 +132,9 @@ class App extends Component<AppProps, AppState> {
versionsData: undefined,
iconsData: null
}

this._heroRef = createRef()
this._navRef = createRef()
}

fetchDocumentData = async (docId: string) => {
Expand All @@ -151,6 +159,16 @@ class App extends Component<AppProps, AppState> {
return this.setState({ versionsData })
}

mainContentRef = (el: Element | null) => {
this._mainContentRef = el as HTMLElement
}

focusContent = () => {
if (this._mainContentRef) {
this._mainContentRef.focus()
}
}

scrollToElement() {
const [_page, id] = this.getPathInfo()

Expand Down Expand Up @@ -192,6 +210,7 @@ class App extends Component<AppProps, AppState> {
const errorHandler = (error: Error) => {
logError(error.name === 'AbortError', error.message)
}
document.addEventListener('keydown', this.handleTabKey)

fetch('icons-data.json', { signal })
.then((response) => response.json())
Expand Down Expand Up @@ -285,7 +304,9 @@ class App extends Component<AppProps, AppState> {
}

handleMenuOpen = () => {
this.setState({ showMenu: true })
this.setState({ showMenu: true }, () => {
this._navRef.current?.focusTextInput()
})
}

handleMenuClose = () => {
Expand Down Expand Up @@ -316,6 +337,33 @@ class App extends Component<AppProps, AppState> {
}
}

focusMainContent = () => {
if (this._heroRef?.current?.focusMainContent) {
this._heroRef.current.focusMainContent()
} else if (this._mainContentRef?.focus) {
this._mainContentRef.focus()
}
}

handleTabKey = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
if (document.activeElement === document.body) {
event.preventDefault()
this.focusSkipToMainButton()
}
}
}

skipToMainButtonRef = (el: Element | null) => {
this._skipToMainButtonRef = el as HTMLElement
}

focusSkipToMainButton = () => {
if (this._skipToMainButtonRef) {
this._skipToMainButtonRef.focus()
}
}

renderThemeSelect() {
const themeKeys = Object.keys(this.state.docsData!.themes)
const smallScreen = this.state.layout === 'small'
Expand Down Expand Up @@ -443,18 +491,24 @@ class App extends Component<AppProps, AppState> {
}
>
{this.renderThemeSelect()}
<Section id={currentData.id} heading={heading}>
<Document
doc={{
...currentData,
legacyGitBranch
}}
description={currentData.description}
themeVariables={themeVariables}
repository={repository}
layout={layout}
/>
</Section>
<View
elementRef={this.mainContentRef}
tabIndex={0}
aria-label="main content"
>
<Section id={currentData.id} heading={heading}>
<Document
doc={{
...currentData,
legacyGitBranch
}}
description={currentData.description}
themeVariables={themeVariables}
repository={repository}
layout={layout}
/>
</Section>
</View>
</View>
)
return this.renderWrappedContent(documentContent)
Expand Down Expand Up @@ -487,6 +541,7 @@ class App extends Component<AppProps, AppState> {
repository={library.repository}
version={library.version}
layout={layout}
ref={this._heroRef}
/>
</InstUISettingsProvider>
)
Expand Down Expand Up @@ -552,15 +607,48 @@ class App extends Component<AppProps, AppState> {
return this.renderHero()
}
if (key === 'CHANGELOG') {
return this.renderChangeLog()
return (
<View
elementRef={this.mainContentRef}
tabIndex={0}
aria-label="changelog page main content"
>
{this.renderChangeLog()}
</View>
)
} else if (key === 'icons') {
return this.renderIcons(key)
return (
<View
elementRef={this.mainContentRef}
tabIndex={0}
aria-label="icons page main content"
as={'div'}
>
{this.renderIcons(key)}
</View>
)
} else if (theme) {
return this.renderTheme(key)
return (
<View
elementRef={this.mainContentRef}
tabIndex={0}
aria-label="theme page main content"
>
{this.renderTheme(key)}
</View>
)
} else if (doc) {
return this.renderDocument(key!, repository)
} else {
return this.renderError()
return (
<View
elementRef={this.mainContentRef}
tabIndex={0}
aria-label="error page main content"
>
{this.renderError()}
</View>
)
}
}

Expand Down Expand Up @@ -613,13 +701,7 @@ class App extends Component<AppProps, AppState> {
shape="circle"
color="secondary"
size="medium"
aria-expanded={true}
ref={(button) => {
if (button) {
button.focus()
}
}}
/>
aria-expanded={true} />
</InstUISettingsProvider>
</View>

Expand All @@ -634,6 +716,7 @@ class App extends Component<AppProps, AppState> {
sections={this.state.docsData!.sections}
docs={this.state.docsData!.docs}
themes={this.state.docsData!.themes}
ref={this._navRef}
/>
</View>
)
Expand Down Expand Up @@ -692,6 +775,24 @@ class App extends Component<AppProps, AppState> {
) : null
}

renderSkipToMainButton = () => {
return (
<View
as={'button'}
onClick={this.focusMainContent}
tabIndex={0}
css={this.props.styles?.skipToMainButton}
borderRadius="small"
display="inline-block"
padding="small"
background="primary"
elementRef={this.skipToMainButtonRef}
>
Skip to main content
</View>
)
}

render() {
const key = this.state.key
const { showMenu, layout, docsData, iconsData } = this.state
Expand All @@ -712,6 +813,7 @@ class App extends Component<AppProps, AppState> {
{showMenu && layout === 'small' && (
<Mask onClick={this.handleMenuClose} />
)}
{this.renderSkipToMainButton()}
{this.renderNavigation()}
<div
css={this.props.styles?.content}
Expand Down
1 change: 1 addition & 0 deletions packages/__docs__/src/App/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type AppStyle = ComponentStyle<
| 'hamburger'
| 'inlineNavigation'
| 'globalStyles'
| 'skipToMainButton'
>

type AppTheme = {
Expand Down
15 changes: 15 additions & 0 deletions packages/__docs__/src/App/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ const generateStyle = (componentTheme: AppTheme): AppStyle => {
borderInlineEndWidth: componentTheme.navBorderWidth,
borderInlineEndStyle: 'solid'
},
skipToMainButton: {
label: 'skipToMainButton',
position: 'absolute',
left: '-9999px',
zIndex: 999,
marginTop: '6px',
opacity: 0,
height: '60px',
fontSize: '150%',
'&:focus': {
left: '11.5rem',
transform: 'translateX(-50%)',
opacity: 1
}
},
globalStyles: {
html: {
height: '100%',
Expand Down
20 changes: 19 additions & 1 deletion packages/__docs__/src/Hero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { Heading } from '../Heading'

import type { HeroProps } from './props'
import { propTypes, allowedProps } from './props'
import React from 'react'

@withStyle(generateStyle, generateComponentTheme)
class Hero extends Component<HeroProps> {
Expand All @@ -61,6 +62,8 @@ class Hero extends Component<HeroProps> {
docs: null
}

_mainContent?: HTMLElement

componentDidMount() {
this.props.makeStyles?.()
}
Expand All @@ -69,6 +72,16 @@ class Hero extends Component<HeroProps> {
this.props.makeStyles?.()
}

mainContentRef = (el: Element | null) => {
this._mainContent = el as HTMLElement
}

focusMainContent = () => {
if (this._mainContent) {
this._mainContent.focus()
}
}

render() {
const { version, layout, styles } = this.props

Expand Down Expand Up @@ -191,7 +204,12 @@ class Hero extends Component<HeroProps> {
const checkmark = <IconCheckMarkSolid inline={false} color="success" />

const heroBodyContent = (
<View as="div">
<View
as="div"
elementRef={this.mainContentRef}
tabIndex={0}
aria-label="main content"
>
<Heading as="h3" level="h2" margin="none none medium">
Components everyone can count on
</Heading>
Expand Down
Loading

0 comments on commit 87a856f

Please sign in to comment.