diff --git a/CHANGELOG.md b/CHANGELOG.md index a4bc1c2504..a08b4a5801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Fix `input` descenders being cropped in the Teams theme @bcalvery ([#2335](https://github.com/microsoft/fluent-ui-react/pull/2335)) - Use referentially equal objects for `actions` in `useStateManager` @layershifter ([#2347](https://github.com/microsoft/fluent-ui-react/pull/2347)) - Fix `Animation` component not to throw when `children` is not provided @mnajdova ([#2345](https://github.com/microsoft/fluent-ui-react/pull/2345)) +- Fix `loader` - adding labeling when loader get focus @kolaps33 ([#2352](https://github.com/microsoft/fluent-ui-react/pull/2352)) ### Features - Added sourcemaps to the dist output to simplify debugging @miroslavstastny ([#2329](https://github.com/microsoft/fluent-ui-react/pull/2329)) diff --git a/docs/src/examples/components/Loader/BestPractices/LoaderBestPractices.tsx b/docs/src/examples/components/Loader/BestPractices/LoaderBestPractices.tsx new file mode 100644 index 0000000000..f9713d3396 --- /dev/null +++ b/docs/src/examples/components/Loader/BestPractices/LoaderBestPractices.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +import ComponentBestPractices from '../../../../components/ComponentBestPractices' + +const doList = [ + 'Use react-aria-live or similar component to announce the loading state.', + 'If loader is only the element in the screen or region, consider making it focusable by setting `tabIndex` prop to the `Loader`. In most of these cases value of `tabIndex` should be 0.', +] + +const LoaderBestPractices: React.FunctionComponent<{}> = () => { + return +} + +export default LoaderBestPractices diff --git a/packages/accessibility/src/behaviors/Loader/loaderBehavior.ts b/packages/accessibility/src/behaviors/Loader/loaderBehavior.ts index 8ee9c9cea4..600b668e08 100644 --- a/packages/accessibility/src/behaviors/Loader/loaderBehavior.ts +++ b/packages/accessibility/src/behaviors/Loader/loaderBehavior.ts @@ -3,17 +3,37 @@ import { Accessibility } from '../../types' /** * @description * Loader is usually an element that displays the progress status for a task that take a long time or consists of several steps. + * Adds attribute 'aria-labelledby' on 'root' when loader has 'tabIndex' prop. This can be overriden by providing 'aria-labelledby' or 'aria-label' property directly to the component. * * @specification * Adds role 'progressbar' to 'root' slot. */ -const loaderBehavior: Accessibility = () => ({ - attributes: { - root: { - role: 'progressbar', +const loaderBehavior: Accessibility = props => { + return { + attributes: { + root: { + role: 'progressbar', + 'aria-labelledby': getDefaultAriaLabelledBy(props), + }, }, - }, -}) + } +} + +type LoaderBehaviorProps = { + /** id of the loader label element. */ + labelId?: string +} export default loaderBehavior + +/** + * Returns the id of the loader label if user provide tabIndex prop. It is used when user does not provide aria-label or + * aria-labelledby as prop. + */ +const getDefaultAriaLabelledBy = (props: LoaderBehaviorProps) => { + if (props['aria-label'] || props['aria-labelledby']) { + return undefined + } + return props['tabIndex'] === undefined ? undefined : props.labelId +} diff --git a/packages/accessibility/test/behaviors/loaderBehavior-test.tsx b/packages/accessibility/test/behaviors/loaderBehavior-test.tsx new file mode 100644 index 0000000000..9824a4cd6f --- /dev/null +++ b/packages/accessibility/test/behaviors/loaderBehavior-test.tsx @@ -0,0 +1,33 @@ +import { loaderBehavior } from '@fluentui/accessibility' + +describe('LoaderBehavior.ts', () => { + test('do NOT add aria-labelledby, when aria-label was set already', () => { + const props = { labelId: 'label-id', 'aria-label': 'any loading string' } + const expectedResult = loaderBehavior(props) + expect(expectedResult.attributes.root['aria-labelledby']).toEqual(undefined) + }) + + test('do NOT add aria-labelledby, when aria-labelled was set already', () => { + const props = { labelId: 'label-id', 'aria-labelledby': 'id' } + const expectedResult = loaderBehavior(props) + expect(expectedResult.attributes.root['aria-labelledby']).toEqual(undefined) + }) + + test('do NOT add aria-labelledby, when there is no tabIndex specified', () => { + const props = { labelId: 'label-id' } + const expectedResult = loaderBehavior(props) + expect(expectedResult.attributes.root['aria-labelledby']).toEqual(undefined) + }) + + test('add aria-labelledby, when there is tabIndex=0 specified', () => { + const props = { labelId: 'label-id', tabIndex: 0 } + const expectedResult = loaderBehavior(props) + expect(expectedResult.attributes.root['aria-labelledby']).toEqual('label-id') + }) + + test('add aria-labelledby, when there is tabIndex=-1 specified', () => { + const props = { labelId: 'label-id', tabIndex: -1 } + const expectedResult = loaderBehavior(props) + expect(expectedResult.attributes.root['aria-labelledby']).toEqual('label-id') + }) +}) diff --git a/packages/react/src/components/Loader/Loader.tsx b/packages/react/src/components/Loader/Loader.tsx index 127bd04bdb..5ad4513450 100644 --- a/packages/react/src/components/Loader/Loader.tsx +++ b/packages/react/src/components/Loader/Loader.tsx @@ -10,6 +10,7 @@ import { commonPropTypes, SizeValue, ShorthandFactory, + getOrGenerateIdFromShorthand, } from '../../utils' import { WithAsProp, ShorthandValue, withSafeTypeForAs } from '../../types' import Box, { BoxProps } from '../Box/Box' @@ -49,6 +50,7 @@ export interface LoaderProps extends UIComponentProps { export interface LoaderState { visible: boolean + labelId: string } /** @@ -94,6 +96,13 @@ class Loader extends UIComponent, LoaderState> { this.state = { visible: this.props.delay === 0, + labelId: '', + } + } + + static getDerivedStateFromProps(props, state) { + return { + labelId: getOrGenerateIdFromShorthand('loader-label-', props.label, state.labelId), } } @@ -114,7 +123,7 @@ class Loader extends UIComponent, LoaderState> { renderComponent({ ElementType, classes, accessibility, variables, styles, unhandledProps }) { const { indicator, label, svg } = this.props - const { visible } = this.state + const { visible, labelId } = this.state const svgElement = Box.create(svg, { defaultProps: () => ({ className: Loader.slotClassNames.svg, styles: styles.svg }), @@ -135,7 +144,11 @@ class Loader extends UIComponent, LoaderState> { }), })} {Text.create(label, { - defaultProps: () => ({ className: Loader.slotClassNames.label, styles: styles.label }), + defaultProps: () => ({ + className: Loader.slotClassNames.label, + styles: styles.label, + id: labelId, + }), })} )