Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-tags-preview): add default aria-labelledby on InteractionTagSecondary #29234

Merged
merged 6 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
YuanboXue-Amber marked this conversation as resolved.
Show resolved Hide resolved
}),
{ 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,9 @@

- 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: You can use a short `aria-label`, for example 'remove'. Because by default `InteractionTagSecondary` has an `aria-labelledby` attribute with the id values of both the InteractionTagPrimary and InteractionTagSecondary components. This will compute the full accessible name 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
Loading