Skip to content

Commit

Permalink
feat(react-tags): add a11y role and best practices guide (#28075)
Browse files Browse the repository at this point in the history
* changes

* mention picker

* focus on prev tag when remove last tag in group

* fix
  • Loading branch information
YuanboXue-Amber authored Jun 15, 2023
1 parent a7f7914 commit 02c7384
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/react-components/react-tags/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@fluentui/react-avatar": "^9.5.5",
"@fluentui/react-icons": "^2.0.203",
"@fluentui/react-jsx-runtime": "9.0.0-alpha.6",
"@fluentui/react-shared-contexts": "^9.5.0",
"@fluentui/react-tabster": "^9.7.5",
"@fluentui/react-theme": "^9.1.8",
"@fluentui/react-utilities": "^9.9.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as React from 'react';
import { getNativeElementProps, useEventCallback } from '@fluentui/react-utilities';
import { getNativeElementProps, useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import type { TagGroupProps, TagGroupState } from './TagGroup.types';
import { useArrowNavigationGroup, useFocusFinders } from '@fluentui/react-tabster';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { tagButtonClassNames } from '../TagButton/useTagButtonStyles.styles';

/**
* Create the state required to render TagGroup.
Expand All @@ -14,10 +17,37 @@ import type { TagGroupProps, TagGroupState } from './TagGroup.types';
export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref<HTMLElement>): TagGroupState => {
const { onDismiss, size = 'medium' } = props;

const innerRef = React.useRef<HTMLElement>();
const { targetDocument } = useFluent();
const { findNextFocusable, findPrevFocusable } = useFocusFinders();

const handleTagDismiss = useEventCallback((e: React.MouseEvent | React.KeyboardEvent, id: string) => {
onDismiss?.(e, { dismissedTagValue: id });

// TODO set focus after tag dismiss
// set focus after tag dismiss
const activeElement = targetDocument?.activeElement;
if (innerRef.current?.contains(activeElement as HTMLElement)) {
// focus on next tag only if the active element is within the current tag group
const next = findNextFocusable(activeElement as HTMLElement, { container: innerRef.current });
if (next) {
next.focus();
return;
}

// if there is no next focusable, focus on the previous focusable
if (activeElement?.className.includes(tagButtonClassNames.dismissButton)) {
const prev = findPrevFocusable(activeElement.parentElement as HTMLElement, { container: innerRef.current });
prev?.focus();
} else {
const prev = findPrevFocusable(activeElement as HTMLElement, { container: innerRef.current });
prev?.focus();
}
}
});

const arrowNavigationProps = useArrowNavigationGroup({
circular: true,
axis: 'both',
});

return {
Expand All @@ -30,9 +60,10 @@ export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref<HTMLEl
},

root: getNativeElementProps('div', {
ref,
ref: useMergedRefs(ref, innerRef),
role: 'toolbar',
...arrowNavigationProps,
...props,
// TODO aria attributes
}),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A Tag provides a visual representation of an attribute, person, or asset.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

### Do

- Use `Tag` instead of `TagButton` for tags without a primary action.

### Don't
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A TagButton follows the same characteristics as a Tag, but with the added functionality of having a primary and secondary action.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@

### Do

- A Picker component is planned for displaying multiple selected values using `TagGroup` with `Combobox`, and will be the recommended approach once it's available. But for now, when using `TagGroup` with `Combobox`:
- Set the `listbox` role for `TagGroup` and the `option` role for each `Tag`.
- If using `TagButton`, set the `option` role for the content and make the dismiss button not focusable. When content is focused, Enter/Space should invoke the primary action, and Backspace/Delete remove the tag.

### Don't
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A TagGroup is a container for multiple controls that are Tag or TagButton.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { TagGroup, Tag, TagButton, TagButtonProps, TagProps, TagGroupProps } fro

export const Dismiss = () => {
const defaultItems = [
{ value: '1', children: 'Tag 1' },
{ value: '2', children: 'Tag 2' },
{ value: 'tagButton-foo', children: 'Foo' },
{ value: 'tagButton-bar', children: 'Bar' },
{ value: '1', children: 'Tag 1', 'aria-label': 'Tag1, remove' },
{ value: '2', children: 'Tag 2', 'aria-label': 'Tag2, remove' },
{ value: 'tagButton-foo', children: 'Foo', dismissButton: { 'aria-label': 'Foo, remove' } },
{ value: 'tagButton-bar', children: 'Bar', dismissButton: { 'aria-label': 'Bar, remove' } },
];

const [items, setItems] = React.useState<Array<TagProps | TagButtonProps>>(defaultItems);
Expand Down

0 comments on commit 02c7384

Please sign in to comment.