diff --git a/change/@fluentui-react-tags-preview-e419fd65-882d-4f66-b9e6-83fdb69e14f5.json b/change/@fluentui-react-tags-preview-e419fd65-882d-4f66-b9e6-83fdb69e14f5.json new file mode 100644 index 0000000000000..cbe0818048baf --- /dev/null +++ b/change/@fluentui-react-tags-preview-e419fd65-882d-4f66-b9e6-83fdb69e14f5.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: set default aria-labelledby on InteractionTagSecondary", + "packageName": "@fluentui/react-tags-preview", + "email": "yuanboxue@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tags-preview/etc/react-tags-preview.api.md b/packages/react-components/react-tags-preview/etc/react-tags-preview.api.md index 53cda3b5974f1..382d027edf547 100644 --- a/packages/react-components/react-tags-preview/etc/react-tags-preview.api.md +++ b/packages/react-components/react-tags-preview/etc/react-tags-preview.api.md @@ -81,6 +81,7 @@ export type InteractionTagSlots = { // @public export type InteractionTagState = ComponentState & Required> & { handleTagDismiss: TagDismissHandler; + interactionTagPrimaryId: string; }; // @public diff --git a/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.test.tsx b/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.test.tsx index d8f652096ec3a..85bd54ae38822 100644 --- a/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.test.tsx +++ b/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.test.tsx @@ -1,5 +1,9 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; import { InteractionTag } from './InteractionTag'; import { isConformant } from '../../testing/isConformant'; +import { InteractionTagPrimary } from '../InteractionTagPrimary'; +import { InteractionTagSecondary } from '../InteractionTagSecondary'; const requiredProps = { children: 'test', @@ -11,4 +15,16 @@ describe('InteractionTag', () => { displayName: 'InteractionTag', requiredProps, }); + + it('should set aria-labelledby with ids of InteractionTagPrimary and InteractionTagSecondary', () => { + const { getByTestId } = render( + + {'tag'} + + , + ); + expect(getByTestId('secondary').getAttribute('aria-labelledby')).toMatch( + /fui-InteractionTagPrimary-\d+ fui-InteractionTagSecondary-\d+/, + ); + }); }); diff --git a/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.types.ts b/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.types.ts index 92815b84a9fc3..623b46c105e65 100644 --- a/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.types.ts +++ b/packages/react-components/react-tags-preview/src/components/InteractionTag/InteractionTag.types.ts @@ -57,4 +57,9 @@ export type InteractionTagState = ComponentState; + + /** + * id to assign to InteractionTagPrimary + */ + interactionTagPrimaryId: string; }; diff --git a/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTag.tsx b/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTag.tsx index 8144ada3e2fb8..8bd4d7e0ef75f 100644 --- a/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTag.tsx +++ b/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTag.tsx @@ -18,7 +18,9 @@ export const useInteractionTag_unstable = ( ): InteractionTagState => { const { handleTagDismiss, size: contextSize } = useTagGroupContext_unstable(); - const id = useId('fui-Tag', props.id); + const id = useId('fui-InteractionTag-', props.id); + + const interactionTagPrimaryId = useId('fui-InteractionTagPrimary-'); const { appearance = 'filled', disabled = false, shape = 'rounded', size = contextSize, value = id } = props; @@ -26,6 +28,7 @@ export const useInteractionTag_unstable = ( appearance, disabled, handleTagDismiss, + interactionTagPrimaryId, shape, size, value, diff --git a/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTagContextValues.ts b/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTagContextValues.ts index 8b5b4bb6ad8c1..f294dc6654324 100644 --- a/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTagContextValues.ts +++ b/packages/react-components/react-tags-preview/src/components/InteractionTag/useInteractionTagContextValues.ts @@ -2,12 +2,12 @@ import * as React from 'react'; import { InteractionTagState, InteractionTagContextValues } from './InteractionTag.types'; export function useInteractionTagContextValues_unstable(state: InteractionTagState): InteractionTagContextValues { - const { appearance, disabled, handleTagDismiss, shape, size, value } = state; + const { appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value } = state; return { interactionTag: React.useMemo( - () => ({ appearance, disabled, handleTagDismiss, shape, size, value }), - [appearance, disabled, handleTagDismiss, shape, size, value], + () => ({ appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value }), + [appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value], ), }; } diff --git a/packages/react-components/react-tags-preview/src/components/InteractionTagPrimary/useInteractionTagPrimary.ts b/packages/react-components/react-tags-preview/src/components/InteractionTagPrimary/useInteractionTagPrimary.ts index 3def8a0290e47..09aebe9d013eb 100644 --- a/packages/react-components/react-tags-preview/src/components/InteractionTagPrimary/useInteractionTagPrimary.ts +++ b/packages/react-components/react-tags-preview/src/components/InteractionTagPrimary/useInteractionTagPrimary.ts @@ -27,7 +27,7 @@ export const useInteractionTagPrimary_unstable = ( props: InteractionTagPrimaryProps, ref: React.Ref, ): InteractionTagPrimaryState => { - const { appearance, disabled, shape, size } = useInteractionTagContext_unstable(); + const { appearance, disabled, interactionTagPrimaryId, shape, size } = useInteractionTagContext_unstable(); const { hasSecondaryAction = false } = props; return { @@ -51,6 +51,7 @@ export const useInteractionTagPrimary_unstable = ( getNativeElementProps('button', { ref, disabled, + id: interactionTagPrimaryId, ...props, }), { elementType: 'button' }, diff --git a/packages/react-components/react-tags-preview/src/components/InteractionTagSecondary/useInteractionTagSecondary.tsx b/packages/react-components/react-tags-preview/src/components/InteractionTagSecondary/useInteractionTagSecondary.tsx index ae570f88b0e96..e620dd9b5fc3a 100644 --- a/packages/react-components/react-tags-preview/src/components/InteractionTagSecondary/useInteractionTagSecondary.tsx +++ b/packages/react-components/react-tags-preview/src/components/InteractionTagSecondary/useInteractionTagSecondary.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getNativeElementProps, useEventCallback, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, useEventCallback, slot, useId } from '@fluentui/react-utilities'; import { Delete, Backspace } from '@fluentui/keyboard-keys'; import { DismissRegular } from '@fluentui/react-icons'; import type { InteractionTagSecondaryProps, InteractionTagSecondaryState } from './InteractionTagSecondary.types'; @@ -18,7 +18,10 @@ export const useInteractionTagSecondary_unstable = ( props: InteractionTagSecondaryProps, ref: React.Ref, ): InteractionTagSecondaryState => { - const { appearance, disabled, handleTagDismiss, shape, size, value } = useInteractionTagContext_unstable(); + const { appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value } = + useInteractionTagContext_unstable(); + + const id = useId('fui-InteractionTagSecondary-', props.id); const onClick = useEventCallback((ev: React.MouseEvent) => { props?.onClick?.(ev); @@ -49,7 +52,9 @@ export const useInteractionTagSecondary_unstable = ( type: 'button', disabled, ref, + 'aria-labelledby': `${interactionTagPrimaryId} ${id}`, ...props, + id, onClick, onKeyDown, }), diff --git a/packages/react-components/react-tags-preview/src/contexts/interactionTagContext.tsx b/packages/react-components/react-tags-preview/src/contexts/interactionTagContext.tsx index eb5d9cea7f253..8f4a5489422b1 100644 --- a/packages/react-components/react-tags-preview/src/contexts/interactionTagContext.tsx +++ b/packages/react-components/react-tags-preview/src/contexts/interactionTagContext.tsx @@ -8,6 +8,7 @@ const interactionTagContextDefaultValue: InteractionTagContextValue = { appearance: 'filled', disabled: false, handleTagDismiss: () => ({}), + interactionTagPrimaryId: '', shape: 'rounded', size: 'medium', value: '', @@ -19,6 +20,7 @@ const interactionTagContextDefaultValue: InteractionTagContextValue = { export type InteractionTagContextValue = Required< Pick & { handleTagDismiss: TagDismissHandler; + interactionTagPrimaryId: string; value?: Value; } >; diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagAppearance.stories.tsx b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagAppearance.stories.tsx index eba6b5ea55b1c..7cbcc742e0acd 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagAppearance.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagAppearance.stories.tsx @@ -16,34 +16,22 @@ export const Appearance = () => { return (
- } hasSecondaryAction id="filled-primary"> + } hasSecondaryAction> filled - + - } hasSecondaryAction id="outline-primary"> + } hasSecondaryAction> outline - + - } hasSecondaryAction id="brand-primary"> + } hasSecondaryAction> brand - +
); diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagBestPractices.md b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagBestPractices.md index 47c589dd4a18d..337c427c07419 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagBestPractices.md +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagBestPractices.md @@ -4,10 +4,13 @@ - To group multiple tags together, use `TagGroup`. `TagGroup` can handle dismiss of multiple `InteractionTag`. -- `InteractionTagSecondary` should provide information to screen readers about the secondary action using `aria-label` or `aria-labelledby`. To label the`InteractionTagSecondary`component with the added context from`InteractionTagPrimary`, follow these steps: - 1. Apply an `id` attribute to both the InteractionTagPrimary and InteractionTagSecondary components. - 2. Add an `aria-label` attribute to the InteractionTagSecondary component, with a value that describes the secondary action (e.g. "remove"). - 3. Add an `aria-labelledby` attribute to the InteractionTagSecondary component, with the id values of both the InteractionTagPrimary and InteractionTagSecondary components. This will compute the accessible name of the InteractionTagSecondary component. +- `InteractionTagSecondary` should provide information to screen readers about the secondary action using `aria-label` or `aria-labelledby`. + + - Recommended: use a brief `aria-label`, such as 'remove'. By default, the InteractionTagSecondary component includes an `aria-labelledby` attribute. This attribute combines the id values from both the InteractionTagPrimary and InteractionTagSecondary components, allowing for a complete accessible name for InteractionTagSecondary. + + ⚠️ If you assign a custom id to InteractionTagPrimary, you'll need to also specify a custom aria-labelledby for InteractionTagSecondary. + + - Another option: If you want to provide a custom accessible name on InteractionTagSecondary that already contains the necessary information from InteractionTagPrimary, you can use the `aria-label` attribute and set `aria-labelledby` to `null`. ### Don't diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDisabled.stories.tsx b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDisabled.stories.tsx index 8a2dade8dd13a..09fe7b9eecf7f 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDisabled.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDisabled.stories.tsx @@ -14,49 +14,22 @@ export const Disabled = () => { return (
- } - hasSecondaryAction - id="disabled-filled-primary" - > + } hasSecondaryAction> disabled - + - } - hasSecondaryAction - id="disabled-outline-primary" - > + } hasSecondaryAction> disabled - + - } - hasSecondaryAction - id="disabled-brand-primary" - > + } hasSecondaryAction> disabled - +
); diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDismiss.stories.tsx b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDismiss.stories.tsx index 2bb6fb9779057..146783dbca0a5 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDismiss.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagDismiss.stories.tsx @@ -22,22 +22,12 @@ export const Dismiss = () => { return ( - {visibleTags.map(tag => { - const primaryId = `dismiss-primary-${tag.value}`; - const secondaryId = `dismiss-secondary-${tag.value}`; - return ( - - - {tag.children} - - - - ); - })} + {visibleTags.map(tag => ( + + {tag.children} + + + ))} ); }; diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagHasPrimaryAction.stories.tsx b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagHasPrimaryAction.stories.tsx index a623899ab357e..d10e732a3fd2d 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagHasPrimaryAction.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagHasPrimaryAction.stories.tsx @@ -21,9 +21,7 @@ export const HasPrimaryAction = () => { - - golden retriever - + golden retriever Find out more on wiki @@ -36,11 +34,7 @@ export const HasPrimaryAction = () => { - + diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagShape.stories.tsx b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagShape.stories.tsx index ddc25ee33690b..86a52637d7105 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagShape.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagShape.stories.tsx @@ -29,34 +29,16 @@ export const Shape = () => { - } - secondaryText="Secondary text" - hasSecondaryAction - id="rounded-primary" - > + } secondaryText="Secondary text" hasSecondaryAction> Rounded - + - } - secondaryText="Secondary text" - hasSecondaryAction - id="circular-primary" - > + } secondaryText="Secondary text" hasSecondaryAction> Circular - + ); diff --git a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagSize.stories.tsx b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagSize.stories.tsx index ffdd06e28adbd..213b84df4bf80 100644 --- a/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagSize.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/InteractionTag/InteractionTagSize.stories.tsx @@ -27,18 +27,10 @@ export const Size = () => { - } - hasSecondaryAction - id="medium-primary" - > + } hasSecondaryAction> Medium dismissible - + @@ -53,18 +45,10 @@ export const Size = () => { - } - hasSecondaryAction - id="small-primary" - > + } hasSecondaryAction> Small dismissible - + @@ -79,18 +63,10 @@ export const Size = () => { - } - hasSecondaryAction - id="extra-small-primary" - > + } hasSecondaryAction> Extra Small dismissible - + diff --git a/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupDismiss.stories.tsx b/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupDismiss.stories.tsx index e6853df66a88e..26982e94ca6a9 100644 --- a/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupDismiss.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupDismiss.stories.tsx @@ -40,22 +40,12 @@ const DismissWithInteractionTags = () => { return ( - {visibleTags.map(tag => { - const primaryId = `dismiss-primary-${tag.value}`; - const secondaryId = `dismiss-secondary-${tag.value}`; - return ( - - - {tag.children} - - - - ); - })} + {visibleTags.map(tag => ( + + {tag.children} + + + ))} ); }; diff --git a/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupSizes.stories.tsx b/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupSizes.stories.tsx index 93a29cfa3ea25..8d8b922d44e4b 100644 --- a/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupSizes.stories.tsx +++ b/packages/react-components/react-tags-preview/stories/TagGroup/TagGroupSizes.stories.tsx @@ -33,14 +33,10 @@ export const Sizes = () => { }>{size} - } hasSecondaryAction id={`${size}-primary`}> + } hasSecondaryAction> {size} - +