diff --git a/packages/react-components/react-tags/etc/react-tags.api.md b/packages/react-components/react-tags/etc/react-tags.api.md index 2cdee8d68e511..b731080fb1171 100644 --- a/packages/react-components/react-tags/etc/react-tags.api.md +++ b/packages/react-components/react-tags/etc/react-tags.api.md @@ -84,7 +84,7 @@ export type TagProps = ComponentProps> & { // @public (undocumented) export type TagSlots = { - root: NonNullable>; + root: NonNullable>; media?: Slot<'span'>; icon?: Slot<'span'>; primaryText: Slot<'span'>; diff --git a/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx b/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx index 174ddfaad88b7..ed002889a23df 100644 --- a/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx +++ b/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx @@ -14,7 +14,7 @@ const requiredProps: TagProps = { }; describe('Tag', () => { - isConformant({ + isConformant({ Component: Tag, displayName: 'Tag', requiredProps, diff --git a/packages/react-components/react-tags/src/components/Tag/Tag.types.ts b/packages/react-components/react-tags/src/components/Tag/Tag.types.ts index 1869fb1368ec9..2dd7710c60367 100644 --- a/packages/react-components/react-tags/src/components/Tag/Tag.types.ts +++ b/packages/react-components/react-tags/src/components/Tag/Tag.types.ts @@ -11,7 +11,7 @@ export type TagContextValues = { }; export type TagSlots = { - root: NonNullable>; + root: NonNullable>; /** * Slot for an icon or other visual element diff --git a/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts b/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts index 16f9f35174451..edc4c3943ba06 100644 --- a/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts +++ b/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts @@ -37,6 +37,7 @@ export const useTagBaseStyles = makeStyles({ ...typographyStyles.body1, paddingLeft: tokens.spacingHorizontalXXS, paddingRight: tokens.spacingHorizontalXXS, + whiteSpace: 'nowrap', }, primaryTextWithSecondaryText: { ...shorthands.gridArea('primary'), @@ -47,6 +48,7 @@ export const useTagBaseStyles = makeStyles({ paddingLeft: tokens.spacingHorizontalXXS, paddingRight: tokens.spacingHorizontalXXS, ...typographyStyles.caption2, + whiteSpace: 'nowrap', }, }); diff --git a/packages/react-components/react-tags/stories/TagGroup/TagGroupOverflow.stories.tsx b/packages/react-components/react-tags/stories/TagGroup/TagGroupOverflow.stories.tsx new file mode 100644 index 0000000000000..8fbe1614c2cae --- /dev/null +++ b/packages/react-components/react-tags/stories/TagGroup/TagGroupOverflow.stories.tsx @@ -0,0 +1,151 @@ +import * as React from 'react'; +import { TagGroup, Tag, TagProps } from '@fluentui/react-tags'; +import { + makeStyles, + shorthands, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + useIsOverflowItemVisible, + useOverflowMenu, + Overflow, + OverflowItem, + Avatar, + tokens, +} from '@fluentui/react-components'; + +const names = [ + 'Johnie McConnell', + 'Allan Munger', + 'Erik Nason', + 'Kristin Patterson', + 'Daisy Phillips', + 'Carole Poland', + 'Carlos Slattery', + 'Robert Tolbert', + 'Kevin Sturgis', + 'Charlotte Waltson', + 'Elliot Woodward', +]; +const defaultItems: TagProps[] = names.map(name => ({ + value: name.replace(' ', '_'), + children: name, + media: ( + + ), + secondaryText: 'Available', +})); + +//----- OverflowMenuItem -----// + +type OverflowMenuItemProps = { + tag: TagProps; +}; + +const useMenuItemStyles = makeStyles({ + menuItem: shorthands.padding(tokens.spacingVerticalSNudge, tokens.spacingHorizontalXS), + tag: { + backgroundColor: 'transparent', + ...shorthands.borderColor('transparent'), + }, +}); + +/** + * A menu item for an overflow menu that only displays when the tab is not visible + */ +const OverflowMenuItem = (props: OverflowMenuItemProps) => { + const { tag } = props; + const isVisible = useIsOverflowItemVisible(tag.value!); + + const styles = useMenuItemStyles(); + + if (isVisible) { + return null; + } + + return ( + + + + ); +}; + +//----- OverflowMenu -----// + +/** + * A menu for viewing tags that have overflowed and are not visible. + */ +const OverflowMenu = () => { + const { ref, isOverflowing, overflowCount } = useOverflowMenu(); + + if (!isOverflowing) { + return null; + } + + return ( + + + {`+${overflowCount}`} + + + + {defaultItems.map(item => ( + + ))} + + + + ); +}; + +//----- Stories -----// + +const useStyles = makeStyles({ + container: { + ...shorthands.overflow('hidden'), + ...shorthands.padding('5px'), + zIndex: 0, // stop the browser resize handle from piercing the overflow menu + height: 'fit-content', + minWidth: '150px', + resize: 'horizontal', + width: '100%', + }, + tagGroup: { + display: 'flex', // TagGroup is inline-flex by default, but we want it to be same width as the container + }, +}); + +export const WithOverflow = () => { + const styles = useStyles(); + + return ( +
+ + + {defaultItems.map(item => ( + + + + ))} + + + +
+ ); +}; + +WithOverflow.storyName = 'With Overflow'; +WithOverflow.parameters = { + docs: { + description: { + story: 'A TagGroup can support overflow by using Overflow and OverflowItem.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/TagGroup/index.stories.tsx b/packages/react-components/react-tags/stories/TagGroup/index.stories.tsx index 09880bb5bd497..a07a090b5f856 100644 --- a/packages/react-components/react-tags/stories/TagGroup/index.stories.tsx +++ b/packages/react-components/react-tags/stories/TagGroup/index.stories.tsx @@ -6,6 +6,7 @@ import bestPracticesMd from './TagGroupBestPractices.md'; export { Default } from './TagGroupDefault.stories'; export { Dismiss } from './TagGroupDismiss.stories'; export { Sizes } from './TagGroupSizes.stories'; +export { WithOverflow } from './TagGroupOverflow.stories'; export default { title: 'Preview Components/Tag/TagGroup',