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,
+ }),
})}
)