From c2258531aa377b698fe932012112704f1879b501 Mon Sep 17 00:00:00 2001 From: Matyas Szabo Date: Sat, 26 Oct 2024 00:10:13 +0200 Subject: [PATCH] fix: docs screenreader alerts are no longer screendeader focusable 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 --- packages/__docs__/buildScripts/DataTypes.mts | 19 ++++---- packages/__docs__/src/App/index.tsx | 6 ++- packages/__docs__/src/Hero/props.ts | 7 +-- packages/__docs__/src/Search/index.tsx | 45 +++++++++---------- packages/__docs__/src/Search/props.ts | 16 ++----- packages/debounce/src/debounce.ts | 26 ++++++----- .../src/ScreenReaderContent/styles.ts | 20 ++++----- packages/ui-alerts/src/Alert/index.tsx | 3 ++ packages/ui-alerts/src/Alert/props.ts | 16 ++++++- .../src/DrawerLayout/DrawerContent/index.tsx | 2 +- .../src/DrawerLayout/DrawerContent/props.ts | 2 +- packages/ui-position/src/Position/index.tsx | 2 +- packages/ui-tabs/src/Tabs/index.tsx | 2 +- packages/ui-text-area/src/TextArea/index.tsx | 2 +- .../src/TruncateList/index.tsx | 2 +- .../src/TruncateText/index.tsx | 2 +- 16 files changed, 88 insertions(+), 84 deletions(-) diff --git a/packages/__docs__/buildScripts/DataTypes.mts b/packages/__docs__/buildScripts/DataTypes.mts index 6ee798a842..b2aa756227 100644 --- a/packages/__docs__/buildScripts/DataTypes.mts +++ b/packages/__docs__/buildScripts/DataTypes.mts @@ -213,18 +213,17 @@ export type ParsedDoc = { descriptions: Record // 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 + type IconGlyph = { name: string variant: any diff --git a/packages/__docs__/src/App/index.tsx b/packages/__docs__/src/App/index.tsx index 66179f13d0..07308b0958 100644 --- a/packages/__docs__/src/App/index.tsx +++ b/packages/__docs__/src/App/index.tsx @@ -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' @@ -498,10 +499,11 @@ class App extends Component { const { library, docs, themes } = this.state.docsData! const { layout } = this.state - const themeDocs: Record = {} + const themeDocs: ParsedDocSummary = {} Object.keys(themes).forEach((key) => { themeDocs[key] = { + title: key, category: 'themes' } }) diff --git a/packages/__docs__/src/Hero/props.ts b/packages/__docs__/src/Hero/props.ts index 7d5e760737..f2ca809fe1 100644 --- a/packages/__docs__/src/Hero/props.ts +++ b/packages/__docs__/src/Hero/props.ts @@ -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 @@ -59,7 +60,7 @@ type HeroStyle = ComponentStyle< > type HeroTheme = { - backgroundColor: Colors['backgroundBrand'] + backgroundColor: string } export type { HeroStyle, HeroTheme } export type { HeroProps } diff --git a/packages/__docs__/src/Search/index.tsx b/packages/__docs__/src/Search/index.tsx index 38994ec7e1..cda94f7dee 100644 --- a/packages/__docs__/src/Search/index.tsx +++ b/packages/__docs__/src/Search/index.tsx @@ -33,6 +33,7 @@ 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 { static defaultProps = { @@ -40,6 +41,8 @@ class Search extends Component { } _options: OptionType[] = [] + _debounced = debounce(this.setState, 1000) + timeoutId?: ReturnType constructor(props: SearchProps) { @@ -70,6 +73,11 @@ class Search extends Component { }) }) } + 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) @@ -77,11 +85,10 @@ class Search extends Component { 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())) @@ -201,8 +208,7 @@ class Search extends Component { renderGroups(options: OptionType[]) { const { highlightedOptionId, selectedOptionId } = this.state - // TODO fix any - const groups: any = { + const groups: Record = { 'GETTING STARTED': [], GUIDES: [], 'CONTRIBUTOR GUIDES': [], @@ -233,27 +239,16 @@ class Search extends Component { return Object.keys(groups).map((group) => ( - {groups[group].map( - ({ - id, - label, - disabled - }: { - id: string - label: string - disabled: boolean - }) => ( - - {label} - - ) - )} + {groups[group].map(({ id, label }) => ( + + {label} + + ))} )) } diff --git a/packages/__docs__/src/Search/props.ts b/packages/__docs__/src/Search/props.ts index 7b7725e8e7..0a23308476 100644 --- a/packages/__docs__/src/Search/props.ts +++ b/packages/__docs__/src/Search/props.ts @@ -22,6 +22,7 @@ * SOFTWARE. */ import type { PropValidators } from '@instructure/shared-types' +import type { ParsedDocSummary } from '../../buildScripts/DataTypes.mjs' import PropTypes from 'prop-types' type OptionType = { @@ -29,13 +30,12 @@ type OptionType = { value: string label: string groupLabel: string - tags: string + tags?: string isWIP: string | boolean - category?: string } type SearchOwnProps = { - options: Record + options: ParsedDocSummary } type PropKeys = keyof SearchOwnProps @@ -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 } diff --git a/packages/debounce/src/debounce.ts b/packages/debounce/src/debounce.ts index 9054af05be..058127a523 100644 --- a/packages/debounce/src/debounce.ts +++ b/packages/debounce/src/debounce.ts @@ -28,10 +28,9 @@ interface DebounceOptions { trailing?: boolean } -export type Debounced = { - (...args: unknown[]): unknown +export type Debounced any> = F & { cancel: () => void - flush: () => void + flush: () => ReturnType } /** @@ -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. @@ -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 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 + let lastCallTime: number let lastInvokeTime = 0 let timers: ReturnType[] = [] if (typeof func !== 'function') { @@ -155,7 +156,8 @@ function debounce( function cancel() { clearAllTimers() lastInvokeTime = 0 - lastArgs = lastCallTime = lastThis = undefined + lastCallTime = 0 + lastArgs = lastThis = undefined } function flush() { @@ -167,7 +169,7 @@ function debounce( timers = [] } - function debounced(...args: unknown[]) { + function debounced(...args: any[]) { const time = Date.now() const isInvoking = shouldInvoke(time) @@ -197,7 +199,7 @@ function debounce( debounced.cancel = cancel debounced.flush = flush - return debounced + return debounced as Debounced } export default debounce diff --git a/packages/ui-a11y-content/src/ScreenReaderContent/styles.ts b/packages/ui-a11y-content/src/ScreenReaderContent/styles.ts index 509442631b..90950b7067 100644 --- a/packages/ui-a11y-content/src/ScreenReaderContent/styles.ts +++ b/packages/ui-a11y-content/src/ScreenReaderContent/styles.ts @@ -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' } } } diff --git a/packages/ui-alerts/src/Alert/index.tsx b/packages/ui-alerts/src/Alert/index.tsx index 13aebd3a8b..80ab2d4cda 100644 --- a/packages/ui-alerts/src/Alert/index.tsx +++ b/packages/ui-alerts/src/Alert/index.tsx @@ -153,6 +153,9 @@ class Alert extends Component { 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', diff --git a/packages/ui-alerts/src/Alert/props.ts b/packages/ui-alerts/src/Alert/props.ts index f1147a5f30..c1d14784e5 100644 --- a/packages/ui-alerts/src/Alert/props.ts +++ b/packages/ui-alerts/src/Alert/props.ts @@ -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 /** diff --git a/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/index.tsx b/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/index.tsx index d746268dc6..f7c057917a 100644 --- a/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/index.tsx +++ b/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/index.tsx @@ -71,7 +71,7 @@ class DrawerContent extends Component { private _resizeListener?: ResizeObserver - private _debounced?: Debounced + private _debounced?: Debounced> componentDidMount() { const rect = getBoundingClientRect(this.ref) diff --git a/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/props.ts b/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/props.ts index 8189763b98..194aa1032b 100644 --- a/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/props.ts +++ b/packages/ui-drawer-layout/src/DrawerLayout/DrawerContent/props.ts @@ -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 diff --git a/packages/ui-position/src/Position/index.tsx b/packages/ui-position/src/Position/index.tsx index b555f164fb..d898edcf88 100644 --- a/packages/ui-position/src/Position/index.tsx +++ b/packages/ui-position/src/Position/index.tsx @@ -161,7 +161,7 @@ class Position extends Component { } componentWillUnmount() { - ;(this.position as Debounced).cancel() + ;(this.position as Debounced).cancel() this.stopTracking() this._timeouts.forEach((timeout) => clearTimeout(timeout)) diff --git a/packages/ui-tabs/src/Tabs/index.tsx b/packages/ui-tabs/src/Tabs/index.tsx index 894e9f3c17..fa4d69bce1 100644 --- a/packages/ui-tabs/src/Tabs/index.tsx +++ b/packages/ui-tabs/src/Tabs/index.tsx @@ -86,7 +86,7 @@ class Tabs extends Component { private _tabList: Element | null = null private _focusable: Focusable | null = null private _tabListPosition?: RectType - private _debounced?: Debounced + private _debounced?: Debounced private _resizeListener?: ResizeObserver ref: Element | null = null diff --git a/packages/ui-text-area/src/TextArea/index.tsx b/packages/ui-text-area/src/TextArea/index.tsx index 119063a21c..960934775e 100644 --- a/packages/ui-text-area/src/TextArea/index.tsx +++ b/packages/ui-text-area/src/TextArea/index.tsx @@ -78,7 +78,7 @@ class TextArea extends Component { private _request?: RequestAnimationFrameType private _defaultId: string private _textareaResizeListener?: ResizeObserver - private _debounced?: Debounced + private _debounced?: Debounced private _textarea: HTMLTextAreaElement | null = null private _container: HTMLDivElement | null = null private _height?: string diff --git a/packages/ui-truncate-list/src/TruncateList/index.tsx b/packages/ui-truncate-list/src/TruncateList/index.tsx index 3d0098fdbd..4b3f8144de 100644 --- a/packages/ui-truncate-list/src/TruncateList/index.tsx +++ b/packages/ui-truncate-list/src/TruncateList/index.tsx @@ -63,7 +63,7 @@ class TruncateList extends Component { private _menuTriggerRef: HTMLLIElement | null = null - private _debouncedHandleResize?: Debounced + private _debouncedHandleResize?: Debounced private _resizeListener?: ResizeObserver handleRef = (el: HTMLUListElement | null) => { diff --git a/packages/ui-truncate-text/src/TruncateText/index.tsx b/packages/ui-truncate-text/src/TruncateText/index.tsx index e37d1606fe..4d928b81c2 100644 --- a/packages/ui-truncate-text/src/TruncateText/index.tsx +++ b/packages/ui-truncate-text/src/TruncateText/index.tsx @@ -68,7 +68,7 @@ class TruncateText extends Component { ref: Element | null = null private _text?: JSX.Element - private _debounced?: Debounced + private _debounced?: Debounced private _stage: HTMLSpanElement | null = null private _wasTruncated?: boolean private _resizeListener?: ResizeObserver