Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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.
Comment thread
YuanboXue-Amber marked this conversation as resolved.

### 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' },
Comment thread
YuanboXue-Amber marked this conversation as resolved.
{ 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