# TagGroup

Container for multiple Tag or InteractionTag controls.

## Default

Source: CoreAI.

Compares a group of static tags with a group of interactive tags.

[Download source](/coreai-design-system/source/examples/components-tag-taggroup--default.tsx)

```tsx
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { TagGroup } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";

export const Default = () => {
    const styles = /* css */ `
      .tag-group-default-example {
        display: flex;
        flex-direction: column;
        row-gap: 10px;
      }
    `;
    return (<>
        <style>{styles}</style>
        <div className="tag-group-default-example">
          Example with Tag:
          <TagGroup aria-label="Simple tag group with Tag" role="list">
            <Tag role="listitem">Tag 1</Tag>
            <Tag role="listitem">Tag 2</Tag>
            <Tag role="listitem">Tag 3</Tag>
          </TagGroup>
          Example with InteractionTag:
          <TagGroup aria-label="Simple tag group with InteractionTag">
            <InteractionTag>
              <InteractionTagPrimary>Tag 1</InteractionTagPrimary>
            </InteractionTag>
            <InteractionTag>
              <InteractionTagPrimary>Tag 2</InteractionTagPrimary>
            </InteractionTag>
            <InteractionTag>
              <InteractionTagPrimary>Tag 3</InteractionTagPrimary>
            </InteractionTag>
          </TagGroup>
        </div>
      </>);
};

export default Default;
```

## API

Generated CoreAI prop summaries, not replacement type declarations. Most native DOM/event and ARIA props are omitted. `?` means optional; JSDoc shows defaults, deprecations, and conditional requirements. Defaults assume the standard CoreAI provider.

### TagGroup API

TagGroup component - a container for multiple tags.
Provides context to size or dismiss children tags.

```ts
interface TagGroupProps {
  appearance?: "brand" | "filled" | "outline";

  as?: "div";

  children?: any;

  className?: string;

  /**
   * Sets selected values for an uncontrolled component.
   */
  defaultSelectedValues?: string[];

  /**
   * A TagGroup can show that it cannot be interacted with.
   * @default false
   */
  disabled?: boolean;

  dismissible?: boolean;

  /**
   * Callback for when a tag is dismissed
   */
  onDismiss?: TagDismissHandler<string>;

  /**
   * Callback for when a tag is selected
   */
  onTagSelect?: EventHandler<TagSelectData<string>>;

  ref?: Ref<HTMLDivElement>;

  /**
   * Values of the selected tags
   */
  selectedValues?: string[];

  size?: "small" | "medium" | "extra-small";

  style?: CSSProperties;
}
```

## Implementation

CoreAI 0.12.0; Fluent 9.74.9.

[Setup and dependencies](/coreai-design-system/guides/Package.md).

Import components from `@coreai-ds/react-components`. CoreAI guidance above takes precedence over API demonstration examples.

## Disabled

Source: Fluent, adapted for CoreAI.

A TagGroup can be disabled. The collection of Tag/InteractionTag will also be disabled.

[Download source](/coreai-design-system/source/examples/components-tag-taggroup--disabled.tsx)

```tsx
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import * as React from 'react';

const useStyles = makeStyles({
    container: {
        display: 'flex',
        flexDirection: 'column',
        rowGap: '10px',
    },
});

const WithTags = () => (<TagGroup disabled aria-label="Disabled tag group with Tag" role="list">
    <Tag role="listitem">Tag 1</Tag>
    <Tag role="listitem">Tag 2</Tag>
    <Tag role="listitem">Tag 3</Tag>
  </TagGroup>);

const WithInteractionTags = () => (<TagGroup disabled aria-label="Disabled tag group with InteractionTag">
    <InteractionTag>
      <InteractionTagPrimary>Tag 1</InteractionTagPrimary>
    </InteractionTag>
    <InteractionTag>
      <InteractionTagPrimary>Tag 2</InteractionTagPrimary>
    </InteractionTag>
    <InteractionTag>
      <InteractionTagPrimary>Tag 3</InteractionTagPrimary>
    </InteractionTag>
  </TagGroup>);

export const Disabled = (): JSXElement => {
    const styles = useStyles();
    return (<div className={styles.container}>
      Disabled example with Tag:
      <WithTags />
      Disabled example with InteractionTag:
      <WithInteractionTags />
    </div>);
};

export default Disabled;
```

## Dismiss

Source: Fluent, adapted for CoreAI.

A TagGroup contains a collection of Tag/InteractionTag that can be dismissed. Ensure that focus is properly managed when all tags have been dismissed.

[Download source](/coreai-design-system/source/examples/components-tag-taggroup--dismiss.tsx)

```tsx
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TagGroupProps } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { InteractionTagSecondary } from "@coreai-ds/react-components";

const useStyles = makeStyles({
    container: {
        display: 'flex',
        flexDirection: 'column',
        rowGap: '10px',
    },
    tagGroup: {
        flexWrap: 'wrap',
    },
    resetButton: {
        width: 'fit-content',
    },
});

const initialTags = [
    { value: '1', children: 'Tag 1' },
    { value: '2', children: 'Tag 2' },
    { value: '3', children: 'Tag 3' },
];

/**
 * focus management for the reset button
 */
const useResetExample = (visibleTagsLength: number) => {
    const resetButtonRef = React.useRef<HTMLButtonElement>(null);
    const firstTagRef = React.useRef<HTMLButtonElement>(null);
    const prevVisibleTagsLengthRef = React.useRef<number>(visibleTagsLength);
    React.useEffect(() => {
        if (visibleTagsLength === 0) {
            resetButtonRef.current?.focus();
        }
        else if (prevVisibleTagsLengthRef.current === 0) {
            firstTagRef.current?.focus();
        }
        prevVisibleTagsLengthRef.current = visibleTagsLength;
    }, [visibleTagsLength]);
    return { firstTagRef, resetButtonRef };
};

const DismissWithTags = () => {
    const [visibleTags, setVisibleTags] = React.useState(initialTags);
    const removeItem: TagGroupProps['onDismiss'] = (_e, { value }) => {
        setVisibleTags([...visibleTags].filter(tag => tag.value !== value));
    };
    const resetItems = () => setVisibleTags(initialTags);
    const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
    const styles = useStyles();
    return (<>
      {visibleTags.length !== 0 && (<TagGroup className={styles.tagGroup} onDismiss={removeItem} aria-label="TagGroup example with dismissible Tags">
          {visibleTags.map((tag, index) => (<Tag dismissible dismissIcon={{ 'aria-label': 'remove' }} value={tag.value} key={tag.value} ref={index === 0 ? firstTagRef : null}>
              {tag.children}
            </Tag>))}
        </TagGroup>)}
      <Button onClick={resetItems} ref={resetButtonRef} disabled={visibleTags.length === initialTags.length} className={styles.resetButton} size="small">
        Reset the example
      </Button>
    </>);
};

const DismissWithInteractionTags = () => {
    const [visibleTags, setVisibleTags] = React.useState(initialTags);
    const removeItem: TagGroupProps['onDismiss'] = (_e, { value }) => {
        setVisibleTags([...visibleTags].filter(tag => tag.value !== value));
    };
    const resetItems = () => setVisibleTags(initialTags);
    const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
    const styles = useStyles();
    return (<>
      {visibleTags.length !== 0 && (<TagGroup className={styles.tagGroup} onDismiss={removeItem} aria-label="Dismiss example">
          {visibleTags.map((tag, index) => (<InteractionTag value={tag.value} key={tag.value}>
              <InteractionTagPrimary hasSecondaryAction ref={index === 0 ? firstTagRef : null}>
                {tag.children}
              </InteractionTagPrimary>
              <InteractionTagSecondary aria-label="remove"/>
            </InteractionTag>))}
        </TagGroup>)}
      <Button onClick={resetItems} ref={resetButtonRef} disabled={visibleTags.length !== 0} className={styles.resetButton} size="small">
        Reset the example
      </Button>
    </>);
};

export const Dismiss = (): JSXElement => {
    const styles = useStyles();
    return (<div className={styles.container}>
      Example with Tag:
      <DismissWithTags />
      Example with InteractionTag:
      <DismissWithInteractionTags />
    </div>);
};

export default Dismiss;
```

## Select

Source: Fluent, adapted for CoreAI.

A TagGroup contains a collection of InteractionTag that can be selected. Note: This prop only changes the appearance of the tag at the moment. A future PR will add the integration with TagGroup.

[Download source](/coreai-design-system/source/examples/components-tag-taggroup--select.tsx)

```tsx
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import type { JSXElement } from "@coreai-ds/react-components";
import { makeResetStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TagValue } from "@coreai-ds/react-components";
import type { TagGroupProps } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { InteractionTagSecondary } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";

const useContainerStyles = makeResetStyles({
    display: 'flex',
    flexDirection: 'column',
    rowGap: '10px',
});

const WithMultiselectTags = () => {
    const [seletedTags, setSelectedTags] = React.useState<Array<TagValue> | undefined>([]);
    const selectItem: TagGroupProps['onTagSelect'] = (_e, { value }) => {
        if (!seletedTags) {
            return;
        }
        if (seletedTags.includes(value)) {
            setSelectedTags(seletedTags.filter(tag => tag !== value));
        }
        else {
            setSelectedTags([...seletedTags, value]);
        }
    };
    return (<>
      <p>Selected values: {seletedTags?.join(', ')}</p>
      <TagGroup onTagSelect={selectItem} aria-label="Tag group with Multiselect Tag">
        <InteractionTag>
          <InteractionTagPrimary>Tag 1</InteractionTagPrimary>
        </InteractionTag>
        <InteractionTag>
          <InteractionTagPrimary>Tag 2</InteractionTagPrimary>
        </InteractionTag>
        <InteractionTag>
          <InteractionTagPrimary>Tag 3</InteractionTagPrimary>
        </InteractionTag>
      </TagGroup>
    </>);
};

const initialTags = [
    { value: '1', children: 'Tag 1' },
    { value: '2', children: 'Tag 2' },
    { value: '3', children: 'Tag 3' },
];

/**
 * focus management for the reset button
 */
const useResetExample = (visibleTagsLength: number) => {
    const resetButtonRef = React.useRef<HTMLButtonElement>(null);
    const firstTagRef = React.useRef<HTMLButtonElement>(null);
    const prevVisibleTagsLengthRef = React.useRef<number>(visibleTagsLength);
    React.useEffect(() => {
        if (visibleTagsLength === 0) {
            resetButtonRef.current?.focus();
        }
        else if (prevVisibleTagsLengthRef.current === 0) {
            firstTagRef.current?.focus();
        }
        prevVisibleTagsLengthRef.current = visibleTagsLength;
    }, [visibleTagsLength]);
    return { firstTagRef, resetButtonRef };
};

const useStyles = makeStyles({
    resetButton: {
        width: 'fit-content',
    },
});

const DismissWithInteractionTags = () => {
    const [visibleTags, setVisibleTags] = React.useState(initialTags);
    const [seletedTags, setSelectedTags] = React.useState<Array<TagValue> | undefined>([]);
    const removeItem: TagGroupProps['onDismiss'] = (_e, { value }) => {
        if (seletedTags?.includes(value)) {
            setSelectedTags(seletedTags?.filter(tag => tag !== value));
        }
        setVisibleTags([...visibleTags].filter(tag => tag.value !== value));
    };
    const resetItems = () => setVisibleTags(initialTags);
    const selectItem: TagGroupProps['onTagSelect'] = (_e, { value }) => {
        if (!seletedTags) {
            return;
        }
        if (seletedTags.includes(value)) {
            setSelectedTags(seletedTags.filter(tag => tag !== value));
        }
        else {
            setSelectedTags([...seletedTags, value]);
        }
    };
    const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
    const styles = useStyles();
    return (<>
      <p>Selected values: {seletedTags?.join(', ')}</p>
      {visibleTags.length !== 0 && (<TagGroup onTagSelect={selectItem} onDismiss={removeItem} aria-label="Tag group with Dismissable Multiselect Tag">
          {visibleTags.map((tag, index) => (<InteractionTag value={tag.value} key={tag.value}>
              <InteractionTagPrimary hasSecondaryAction ref={index === 0 ? firstTagRef : null}>
                {tag.children}
              </InteractionTagPrimary>
              <InteractionTagSecondary aria-label="remove"/>
            </InteractionTag>))}
        </TagGroup>)}
      <Button onClick={resetItems} ref={resetButtonRef} disabled={visibleTags.length !== 0} className={styles.resetButton} size="small">
        Reset the example
      </Button>
    </>);
};

export const Select = (): JSXElement => {
    const containerStyles = useContainerStyles();
    return (<div className={containerStyles}>
      Example with InteractionTag:
      <WithMultiselectTags />
      Example with Dismissable InteractionTag:
      <DismissWithInteractionTags />
    </div>);
};

export default Select;
```

## Sizes

Source: Fluent, adapted for CoreAI.

A TagGroup can set default size for all its tags. It supports `medium`, `small` and `extra-small` size. Default value is `medium`.

[Download source](/coreai-design-system/source/examples/components-tag-taggroup--sizes.tsx)

```tsx
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import type { TagSize } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { Avatar } from "@coreai-ds/react-components";
import { CalendarMonthRegular } from "@fluentui/react-icons";
import { InteractionTagSecondary } from "@coreai-ds/react-components";
import * as React from 'react';

const useContainerStyles = makeStyles({
    root: {
        display: 'flex',
        flexDirection: 'column',
        rowGap: '10px',
    },
    tagGroup: {
        flexWrap: 'wrap',
    },
});

export const Sizes = (): JSXElement => {
    const containerStyles = useContainerStyles();
    const sizes: TagSize[] = ['medium', 'small', 'extra-small'];
    return (<div className={containerStyles.root}>
      {sizes.map(size => (<div key={size}>
          {`${size}: `}
          <TagGroup className={containerStyles.tagGroup} size={size} aria-label={`${size} tag group example`}>
            <InteractionTag>
              <InteractionTagPrimary media={<Avatar name="Katri Athokas"/>}>{size}</InteractionTagPrimary>
            </InteractionTag>
            <InteractionTag shape="circular">
              <InteractionTagPrimary icon={<CalendarMonthRegular />}>{size}</InteractionTagPrimary>
            </InteractionTag>
            <InteractionTag>
              <InteractionTagPrimary icon={<CalendarMonthRegular />} hasSecondaryAction>
                {size}
              </InteractionTagPrimary>
              <InteractionTagSecondary aria-label="remove"/>
            </InteractionTag>
          </TagGroup>
        </div>))}
    </div>);
};

export default Sizes;
```

## With Overflow

Source: Fluent, adapted for CoreAI.

A TagGroup can support overflow by using Overflow and OverflowItem.

[Download source](/coreai-design-system/source/examples/components-tag-taggroup--with-overflow.tsx)

```tsx
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Overflow } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import type { InteractionTagPrimaryProps } from "@coreai-ds/react-components";
import { Avatar } from "@coreai-ds/react-components";
import { OverflowItem } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { useOverflowMenu } from "@coreai-ds/react-components";
import { Menu } from "@coreai-ds/react-components";
import { MenuTrigger } from "@coreai-ds/react-components";
import { MenuPopover } from "@coreai-ds/react-components";
import { MenuList } from "@coreai-ds/react-components";
import type { TagProps } from "@coreai-ds/react-components";
import { useIsOverflowItemVisible } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { tagClassNames } from "@coreai-ds/react-components";
import { shorthands } from "@coreai-ds/react-components";
import { MenuItem } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import * as React from 'react';

//----- Stories -----//
const useStyles = makeStyles({
    container: {
        overflow: 'hidden',
        padding: '5px',
        zIndex: 0, // stop the browser resize handle from piercing the overflow menu
        height: 'fit-content',
        minWidth: '150px',
        resize: 'horizontal',
        width: '100%',
        boxSizing: 'border-box',
    },
    tagGroup: {
        display: 'flex', // TagGroup is inline-flex by default, but we want it to be same width as the container
    },
});

type DefaultItem = InteractionTagPrimaryProps & {
    value: string;
};

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: DefaultItem[] = names.map(name => ({
    value: name.replace(' ', '_'),
    children: name,
    media: (<Avatar aria-hidden="true" // use aria-hidden because InteractionTag contains information in the avatar
     name={name} badge={{
            status: 'available',
        }}/>),
    secondaryText: 'Available',
}));

//----- OverflowMenuItem -----//
type OverflowMenuItemProps = {
    tag: TagProps;
};

const useMenuItemStyles = makeStyles({
    menuItem: {
        padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalXS}`,
        ':hover': {
            [`& .${tagClassNames.root}`]: {
                color: tokens.colorNeutralForeground2Hover,
            },
        },
    },
    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 (<MenuItem key={tag.value} className={styles.menuItem}>
      <Tag {...tag} as="span" className={styles.tag}/>
    </MenuItem>);
};

//----- OverflowMenu -----//
/**
 * A menu for viewing tags that have overflowed and are not visible.
 */
const OverflowMenu = () => {
    const { ref, isOverflowing, overflowCount } = useOverflowMenu<HTMLButtonElement>();
    if (!isOverflowing) {
        return null;
    }
    return (<InteractionTag>
      <Menu>
        <MenuTrigger disableButtonEnhancement>
          <InteractionTagPrimary ref={ref} aria-label={`${overflowCount} more tags`}>
            {`+${overflowCount}`}
          </InteractionTagPrimary>
        </MenuTrigger>
        <MenuPopover>
          <MenuList>
            {defaultItems.map(item => (<OverflowMenuItem key={item.value} tag={item}/>))}
          </MenuList>
        </MenuPopover>
      </Menu>
    </InteractionTag>);
};

export const WithOverflow = (): JSXElement => {
    const styles = useStyles();
    return (<div className={styles.container}>
      <Overflow minimumVisible={2} padding={60}>
        <TagGroup className={styles.tagGroup} aria-label="Overflow example">
          {defaultItems.map(({ value, ...rest }) => (<OverflowItem key={value} id={value!}>
              <InteractionTag key={value}>
                <InteractionTagPrimary {...rest}/>
              </InteractionTag>
            </OverflowItem>))}
          <OverflowMenu />
        </TagGroup>
      </Overflow>
    </div>);
};

export default WithOverflow;
```