Skip to content

Commit

Permalink
fix: docs screenreader alerts are no longer screendeader focusable
Browse files Browse the repository at this point in the history
Also some better types in the docs app and the debounce utility
TEST PLAN:
Enter some text in the docs in the top search field with screenreader on. After this focus out, and
find the beginning of the page. It should not read the search results again
  • Loading branch information
matyasf committed Oct 28, 2024
1 parent 82094c3 commit c225853
Show file tree
Hide file tree
Showing 16 changed files with 88 additions and 84 deletions.
19 changes: 9 additions & 10 deletions packages/__docs__/buildScripts/DataTypes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,17 @@ export type ParsedDoc = {
descriptions: Record<string, string>
// Hold minimal information about each document that is needed for search
// functionality
docs: Record<
string,
{
title: string
order?: string
category?: string
isWIP?: boolean
tags?: string
}
>
docs: ParsedDocSummary
}

export type ParsedDocSummary = Record<string,{
title: string
order?: string
category?: string
isWIP?: boolean
tags?: string
}>

type IconGlyph = {
name: string
variant: any
Expand Down
6 changes: 4 additions & 2 deletions packages/__docs__/src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ import type { AppProps, AppState, DocData, LayoutSize } from './props'
import { propTypes, allowedProps } from './props'
import type {
LibraryOptions,
MainDocsData
MainDocsData,
ParsedDocSummary
} from '../../buildScripts/DataTypes.mjs'
import { logError } from '@instructure/console'

Expand Down Expand Up @@ -498,10 +499,11 @@ class App extends Component<AppProps, AppState> {
const { library, docs, themes } = this.state.docsData!
const { layout } = this.state

const themeDocs: Record<string, any> = {}
const themeDocs: ParsedDocSummary = {}

Object.keys(themes).forEach((key) => {
themeDocs[key] = {
title: key,
category: 'themes'
}
})
Expand Down
7 changes: 4 additions & 3 deletions packages/__docs__/src/Hero/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@
* SOFTWARE.
*/
import type { ComponentStyle, WithStyleProps } from '@instructure/emotion'
import type { Colors, PropValidators } from '@instructure/shared-types'
import type { PropValidators } from '@instructure/shared-types'
import PropTypes from 'prop-types'
import type { ParsedDocSummary } from '../../buildScripts/DataTypes.mjs'

type HeroOwnProps = {
name: string
repository: string
version: string
layout: 'small' | 'medium' | 'large' | 'x-large'
docs: any
docs: ParsedDocSummary
}

type PropKeys = keyof HeroOwnProps
Expand Down Expand Up @@ -59,7 +60,7 @@ type HeroStyle = ComponentStyle<
>

type HeroTheme = {
backgroundColor: Colors['backgroundBrand']
backgroundColor: string
}
export type { HeroStyle, HeroTheme }
export type { HeroProps }
Expand Down
45 changes: 20 additions & 25 deletions packages/__docs__/src/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ import { Select } from '@instructure/ui-select'
import { SearchStatus } from '../SearchStatus'

import type { SearchProps, SearchState, OptionType } from './props'
import { debounce } from '@instructure/debounce'

class Search extends Component<SearchProps, SearchState> {
static defaultProps = {
options: undefined
}

_options: OptionType[] = []
_debounced = debounce(this.setState, 1000)

timeoutId?: ReturnType<typeof setTimeout>

constructor(props: SearchProps) {
Expand Down Expand Up @@ -70,18 +73,22 @@ class Search extends Component<SearchProps, SearchState> {
})
})
}
componentDidUpdate(_prevProps: SearchProps, prevState: SearchState) {
if (this.state.announcement != prevState.announcement) {
this._debounced({ announcement: null })
}
}

getOptionById(queryId: string) {
return this.state.filteredOptions.find(({ id }) => id === queryId)
}

filterOptions = (value: string) => {
return this._options.filter((option) => {
// We want to hide WIP components etc.
// We want to hide WIP components
if (option?.isWIP) {
return false
}

return (
option.label.toLowerCase().includes(value.toLowerCase()) ||
(option.tags && option.tags.toString().includes(value.toLowerCase()))
Expand Down Expand Up @@ -201,8 +208,7 @@ class Search extends Component<SearchProps, SearchState> {
renderGroups(options: OptionType[]) {
const { highlightedOptionId, selectedOptionId } = this.state

// TODO fix any
const groups: any = {
const groups: Record<string, OptionType[]> = {
'GETTING STARTED': [],
GUIDES: [],
'CONTRIBUTOR GUIDES': [],
Expand Down Expand Up @@ -233,27 +239,16 @@ class Search extends Component<SearchProps, SearchState> {

return Object.keys(groups).map((group) => (
<Select.Group renderLabel={group} key={group}>
{groups[group].map(
({
id,
label,
disabled
}: {
id: string
label: string
disabled: boolean
}) => (
<Select.Option
id={id}
key={id}
isHighlighted={id === highlightedOptionId}
isSelected={id === selectedOptionId}
isDisabled={disabled}
>
{label}
</Select.Option>
)
)}
{groups[group].map(({ id, label }) => (
<Select.Option
id={id}
key={id}
isHighlighted={id === highlightedOptionId}
isSelected={id === selectedOptionId}
>
{label}
</Select.Option>
))}
</Select.Group>
))
}
Expand Down
16 changes: 4 additions & 12 deletions packages/__docs__/src/Search/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@
* SOFTWARE.
*/
import type { PropValidators } from '@instructure/shared-types'
import type { ParsedDocSummary } from '../../buildScripts/DataTypes.mjs'
import PropTypes from 'prop-types'

type OptionType = {
id: string
value: string
label: string
groupLabel: string
tags: string
tags?: string
isWIP: string | boolean
category?: string
}

type SearchOwnProps = {
options: Record<string, OptionType>
options: ParsedDocSummary
}

type PropKeys = keyof SearchOwnProps
Expand All @@ -55,15 +55,7 @@ type SearchState = {
highlightedOptionId: string | null
selectedOptionId: string | null
selectedOptionLabel: string
filteredOptions: {
id: string
value: string
label: string
groupLabel: string
tags: string
isWIP: string | boolean
category?: string
}[]
filteredOptions: OptionType[]
announcement: string | null
}

Expand Down
26 changes: 14 additions & 12 deletions packages/debounce/src/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ interface DebounceOptions {
trailing?: boolean
}

export type Debounced = {
(...args: unknown[]): unknown
export type Debounced<F extends (...args: any) => any> = F & {
cancel: () => void
flush: () => void
flush: () => ReturnType<F>
}

/**
Expand All @@ -51,6 +50,8 @@ export type Debounced = {
*
* Note: Modified from the original to check for cancelled boolean before invoking func to prevent React setState
* on unmounted components.
* For a cool explanation see https://css-tricks.com/debouncing-throttling-explained-examples/
*
* @module debounce
*
* @param {Function} func The function to debounce.
Expand All @@ -64,15 +65,15 @@ export type Debounced = {
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new debounced function.
*/
function debounce(
func: (...args: any[]) => unknown,
function debounce<F extends (...args: any) => any>(
func: F,
wait = 0,
options: DebounceOptions = {}
): Debounced {
let lastArgs: unknown[] | undefined
) {
let lastArgs: unknown | undefined
let lastThis: unknown
let result: unknown
let lastCallTime: number | undefined // TODO this should never be undefined
let result: ReturnType<F>
let lastCallTime: number
let lastInvokeTime = 0
let timers: ReturnType<typeof setTimeout>[] = []
if (typeof func !== 'function') {
Expand Down Expand Up @@ -155,7 +156,8 @@ function debounce(
function cancel() {
clearAllTimers()
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = undefined
lastCallTime = 0
lastArgs = lastThis = undefined
}

function flush() {
Expand All @@ -167,7 +169,7 @@ function debounce(
timers = []
}

function debounced(...args: unknown[]) {
function debounced(...args: any[]) {
const time = Date.now()
const isInvoking = shouldInvoke(time)

Expand Down Expand Up @@ -197,7 +199,7 @@ function debounce(
debounced.cancel = cancel
debounced.flush = flush

return debounced
return debounced as Debounced<F>
}

export default debounce
Expand Down
20 changes: 9 additions & 11 deletions packages/ui-a11y-content/src/ScreenReaderContent/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,23 @@ import type { ScreenReaderContentStyle } from './props'
* private: true
* ---
* Generates the style object from the theme and provided additional information
* @param {Object} componentTheme The theme variable object.
* @param {Object} props the props of the component, the style is applied to
* @param {Object} state the state of the component, the style is applied to
* @return {Object} The final style object, which will be used in the component
* @return The final style object, which will be used in the component
*/
const generateStyle = (): ScreenReaderContentStyle => {
return {
screenReaderContent: {
label: 'screenReaderContent',
width: '0.0625rem',
height: '0.0625rem',
margin: '-0.0625rem',
padding: 0,
width: '0.0625rem !important',
height: '0.0625rem !important',
margin: '-0.0625rem !important',
padding: '0 !important',
position: 'absolute',
top: 0,
insetInlineStart: 0,
overflow: 'hidden',
clip: 'rect(0 0 0 0)',
border: 0
whiteSpace: 'nowrap',
overflow: 'hidden !important',
clip: 'rect(0 0 0 0) !important',
border: '0 !important'
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/ui-alerts/src/Alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ class Alert extends Component<AlertProps, AlertState> {

if (liveRegion) {
liveRegion.setAttribute('aria-live', this.props.liveRegionPoliteness!)
// indicates what notifications the user agent will trigger when the
// accessibility tree within a live region is modified.
// additions: elements are added, text: Text content is added
liveRegion.setAttribute('aria-relevant', 'additions text')
liveRegion.setAttribute(
'aria-atomic',
Expand Down
16 changes: 14 additions & 2 deletions packages/ui-alerts/src/Alert/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,23 @@ type AlertOwnProps = {
*/
liveRegion?: () => Element
/**
* Choose the politeness level of screenreader alerts.
* Choose the politeness level of screenreader alerts, sets the value of
* `aria-live`.
*
* When regions are specified as `polite`, assistive technologies will notify
* users of updates but generally do not interrupt the current task,
* and updates take low priority.
*
* When regions are specified as `assertive`, assistive technologies will
* immediately notify the user, and could potentially clear the speech queue
* of previous updates.
*/
liveRegionPoliteness?: 'polite' | 'assertive'
/**
* If the screenreader alert should be atomic
* Value for the `aria-atomic` attribute.
* `aria-atomic` controls how much is read when a change happens. Should only
* the specific thing that changed be read or should the entire element be
* read.
*/
isLiveRegionAtomic?: boolean
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class DrawerContent extends Component<DrawerLayoutContentProps> {

private _resizeListener?: ResizeObserver

private _debounced?: Debounced
private _debounced?: Debounced<NonNullable<typeof this.props.onSizeChange>>

componentDidMount() {
const rect = getBoundingClientRect(this.ref)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import type {
} from '@instructure/shared-types'
import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'

type DrawerContentSize = { width: number; height: number }
type DrawerContentSize = { width: number; height?: number }

type DrawerLayoutContentOwnProps = {
label: string
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-position/src/Position/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class Position extends Component<PositionProps, PositionState> {
}

componentWillUnmount() {
;(this.position as Debounced).cancel()
;(this.position as Debounced<typeof this.position>).cancel()
this.stopTracking()
this._timeouts.forEach((timeout) => clearTimeout(timeout))

Expand Down
2 changes: 1 addition & 1 deletion packages/ui-tabs/src/Tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class Tabs extends Component<TabsProps, TabsState> {
private _tabList: Element | null = null
private _focusable: Focusable | null = null
private _tabListPosition?: RectType
private _debounced?: Debounced
private _debounced?: Debounced<typeof this.handleResize>
private _resizeListener?: ResizeObserver

ref: Element | null = null
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-text-area/src/TextArea/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class TextArea extends Component<TextAreaProps> {
private _request?: RequestAnimationFrameType
private _defaultId: string
private _textareaResizeListener?: ResizeObserver
private _debounced?: Debounced
private _debounced?: Debounced<typeof this.grow>
private _textarea: HTMLTextAreaElement | null = null
private _container: HTMLDivElement | null = null
private _height?: string
Expand Down
Loading

0 comments on commit c225853

Please sign in to comment.