Skip to content

Commit

Permalink
feat(react-tags-preview): add default aria-labelledby on InteractionT…
Browse files Browse the repository at this point in the history
…agSecondary (#29234)

* fix

* chg

* api

* document custom id

* format
  • Loading branch information
YuanboXue-Amber authored Sep 25, 2023
1 parent afa2b8e commit c9b7dde
Show file tree
Hide file tree
Showing 18 changed files with 92 additions and 160 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: set default aria-labelledby on InteractionTagSecondary",
"packageName": "@fluentui/react-tags-preview",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export type InteractionTagSlots = {
// @public
export type InteractionTagState<Value = TagValue> = ComponentState<InteractionTagSlots> & Required<Pick<InteractionTagProps, 'appearance' | 'disabled' | 'shape' | 'size' | 'value'>> & {
handleTagDismiss: TagDismissHandler<Value>;
interactionTagPrimaryId: string;
};

// @public
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -11,4 +15,16 @@ describe('InteractionTag', () => {
displayName: 'InteractionTag',
requiredProps,
});

it('should set aria-labelledby with ids of InteractionTagPrimary and InteractionTagSecondary', () => {
const { getByTestId } = render(
<InteractionTag>
<InteractionTagPrimary>{'tag'}</InteractionTagPrimary>
<InteractionTagSecondary data-testid="secondary" aria-label="remove" />
</InteractionTag>,
);
expect(getByTestId('secondary').getAttribute('aria-labelledby')).toMatch(
/fui-InteractionTagPrimary-\d+ fui-InteractionTagSecondary-\d+/,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ export type InteractionTagState<Value = TagValue> = ComponentState<InteractionTa
* Event handler from TagGroup context that allows TagGroup to dismiss the tag
*/
handleTagDismiss: TagDismissHandler<Value>;

/**
* id to assign to InteractionTagPrimary
*/
interactionTagPrimaryId: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ 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;

return {
appearance,
disabled,
handleTagDismiss,
interactionTagPrimaryId,
shape,
size,
value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const useInteractionTagPrimary_unstable = (
props: InteractionTagPrimaryProps,
ref: React.Ref<HTMLElement>,
): InteractionTagPrimaryState => {
const { appearance, disabled, shape, size } = useInteractionTagContext_unstable();
const { appearance, disabled, interactionTagPrimaryId, shape, size } = useInteractionTagContext_unstable();
const { hasSecondaryAction = false } = props;

return {
Expand All @@ -51,6 +51,7 @@ export const useInteractionTagPrimary_unstable = (
getNativeElementProps('button', {
ref,
disabled,
id: interactionTagPrimaryId,
...props,
}),
{ elementType: 'button' },
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,7 +18,10 @@ export const useInteractionTagSecondary_unstable = (
props: InteractionTagSecondaryProps,
ref: React.Ref<HTMLElement>,
): 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<HTMLButtonElement>) => {
props?.onClick?.(ev);
Expand Down Expand Up @@ -49,7 +52,9 @@ export const useInteractionTagSecondary_unstable = (
type: 'button',
disabled,
ref,
'aria-labelledby': `${interactionTagPrimaryId} ${id}`,
...props,
id,
onClick,
onKeyDown,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const interactionTagContextDefaultValue: InteractionTagContextValue = {
appearance: 'filled',
disabled: false,
handleTagDismiss: () => ({}),
interactionTagPrimaryId: '',
shape: 'rounded',
size: 'medium',
value: '',
Expand All @@ -19,6 +20,7 @@ const interactionTagContextDefaultValue: InteractionTagContextValue = {
export type InteractionTagContextValue<Value = string> = Required<
Pick<InteractionTagState, 'appearance' | 'disabled' | 'shape' | 'size'> & {
handleTagDismiss: TagDismissHandler<Value>;
interactionTagPrimaryId: string;
value?: Value;
}
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,22 @@ export const Appearance = () => {
return (
<div className={styles.container}>
<InteractionTag>
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction id="filled-primary">
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction>
filled
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="filled-primary filled-secondary"
id="filled-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
<InteractionTag appearance="outline">
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction id="outline-primary">
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction>
outline
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="outline-primary outline-secondary"
id="outline-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
<InteractionTag appearance="brand">
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction id="brand-primary">
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction>
brand
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="brand-primary brand-secondary"
id="brand-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,22 @@ export const Disabled = () => {
return (
<div className={styles.container}>
<InteractionTag disabled>
<InteractionTagPrimary
secondaryText="appearance=filled"
icon={<CalendarMonthRegular />}
hasSecondaryAction
id="disabled-filled-primary"
>
<InteractionTagPrimary secondaryText="appearance=filled" icon={<CalendarMonthRegular />} hasSecondaryAction>
disabled
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="disabled-filled-primary disabled-filled-secondary"
id="disabled-filled-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
<InteractionTag disabled appearance="outline">
<InteractionTagPrimary
secondaryText="appearance=outline"
icon={<CalendarMonthRegular />}
hasSecondaryAction
id="disabled-outline-primary"
>
<InteractionTagPrimary secondaryText="appearance=outline" icon={<CalendarMonthRegular />} hasSecondaryAction>
disabled
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="disabled-outline-primary disabled-outline-secondary"
id="disabled-outline-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
<InteractionTag disabled appearance="brand">
<InteractionTagPrimary
secondaryText="appearance=brand"
icon={<CalendarMonthRegular />}
hasSecondaryAction
id="disabled-brand-primary"
>
<InteractionTagPrimary secondaryText="appearance=brand" icon={<CalendarMonthRegular />} hasSecondaryAction>
disabled
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="disabled-brand-primary disabled-brand-secondary"
id="disabled-brand-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,12 @@ export const Dismiss = () => {

return (
<TagGroup onDismiss={removeItem} aria-label="Dismiss example">
{visibleTags.map(tag => {
const primaryId = `dismiss-primary-${tag.value}`;
const secondaryId = `dismiss-secondary-${tag.value}`;
return (
<InteractionTag value={tag.value} key={tag.value}>
<InteractionTagPrimary id={primaryId} hasSecondaryAction>
{tag.children}
</InteractionTagPrimary>
<InteractionTagSecondary
id={secondaryId}
aria-label="remove"
aria-labelledby={`${primaryId} ${secondaryId}`}
/>
</InteractionTag>
);
})}
{visibleTags.map(tag => (
<InteractionTag value={tag.value} key={tag.value}>
<InteractionTagPrimary hasSecondaryAction>{tag.children}</InteractionTagPrimary>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
))}
</TagGroup>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ export const HasPrimaryAction = () => {
<InteractionTag>
<Popover trapFocus>
<PopoverTrigger>
<InteractionTagPrimary hasSecondaryAction id="golden-retriever-primary">
golden retriever
</InteractionTagPrimary>
<InteractionTagPrimary hasSecondaryAction>golden retriever</InteractionTagPrimary>
</PopoverTrigger>
<PopoverSurface className={styles.popover}>
<Link href="https://en.wikipedia.org/wiki/Golden_Retriever">Find out more on wiki</Link>
Expand All @@ -36,11 +34,7 @@ export const HasPrimaryAction = () => {
</PopoverSurface>
</Popover>
<Tooltip content={liked ? 'unlike' : 'like'} relationship="label">
<InteractionTagSecondary
onClick={toggleSecondary}
aria-labelledby="golden-retriever-primary golden-retriever-secondary"
id="golden-retriever-secondary"
>
<InteractionTagSecondary onClick={toggleSecondary}>
<HeartIcon filled={liked} />
</InteractionTagSecondary>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,16 @@ export const Shape = () => {
</InteractionTag>

<InteractionTag>
<InteractionTagPrimary
icon={<CalendarMonthRegular />}
secondaryText="Secondary text"
hasSecondaryAction
id="rounded-primary"
>
<InteractionTagPrimary icon={<CalendarMonthRegular />} secondaryText="Secondary text" hasSecondaryAction>
Rounded
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="rounded-primary rounded-secondary"
id="rounded-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
<InteractionTag shape="circular">
<InteractionTagPrimary
icon={<CalendarMonthRegular />}
secondaryText="Secondary text"
hasSecondaryAction
id="circular-primary"
>
<InteractionTagPrimary icon={<CalendarMonthRegular />} secondaryText="Secondary text" hasSecondaryAction>
Circular
</InteractionTagPrimary>
<InteractionTagSecondary
aria-label="remove"
aria-labelledby="circular-primary circular-secondary"
id="circular-secondary"
/>
<InteractionTagSecondary aria-label="remove" />
</InteractionTag>
</div>
);
Expand Down
Loading

0 comments on commit c9b7dde

Please sign in to comment.