Skip to content

Commit 5b142da

Browse files
authored
feat(react-headless-components-preview): add AvatarGroup component (#36350)
1 parent 47b4e42 commit 5b142da

29 files changed

Lines changed: 1210 additions & 0 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: add AvatarGroup, AvatarGroupItem, and AvatarGroupPopover components",
4+
"packageName": "@fluentui/react-headless-components-preview",
5+
"email": "dmytrokirpa@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as Accordion from '@fluentui/react-headless-components-preview/accordion';
22
import * as Avatar from '@fluentui/react-headless-components-preview/avatar';
3+
import * as AvatarGroup from '@fluentui/react-headless-components-preview/avatar-group';
34
import * as Badge from '@fluentui/react-headless-components-preview/badge';
45
import * as Breadcrumb from '@fluentui/react-headless-components-preview/breadcrumb';
56
import * as Button from '@fluentui/react-headless-components-preview/button';
@@ -48,6 +49,7 @@ import * as Tooltip from '@fluentui/react-headless-components-preview/tooltip';
4849
console.log({
4950
Accordion,
5051
Avatar,
52+
AvatarGroup,
5153
Badge,
5254
Breadcrumb,
5355
Button,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
## API Report File for "@fluentui/react-headless-components-preview"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import { AvatarGroupContextValue } from '@fluentui/react-avatar';
8+
import { AvatarGroupContextValues } from '@fluentui/react-avatar';
9+
import type { AvatarGroupItemBaseProps } from '@fluentui/react-avatar';
10+
import { AvatarGroupItemSlots } from '@fluentui/react-avatar';
11+
import { AvatarGroupItemBaseState as AvatarGroupItemState } from '@fluentui/react-avatar';
12+
import { AvatarGroupBaseProps as AvatarGroupProps } from '@fluentui/react-avatar';
13+
import type { AvatarGroupProps as AvatarGroupProps_2 } from '@fluentui/react-avatar';
14+
import { AvatarGroupSlots } from '@fluentui/react-avatar';
15+
import { AvatarGroupBaseState as AvatarGroupState } from '@fluentui/react-avatar';
16+
import type { ComponentProps } from '@fluentui/react-utilities';
17+
import type { ComponentState } from '@fluentui/react-utilities';
18+
import type { EventData } from '@fluentui/react-utilities';
19+
import type { EventHandler } from '@fluentui/react-utilities';
20+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
21+
import type { JSXElement } from '@fluentui/react-utilities';
22+
import { PartitionAvatarGroupItems } from '@fluentui/react-avatar';
23+
import { partitionAvatarGroupItems } from '@fluentui/react-avatar';
24+
import { PartitionAvatarGroupItemsOptions } from '@fluentui/react-avatar';
25+
import { PositioningShorthand } from '@fluentui/react-positioning';
26+
import type * as React_2 from 'react';
27+
import { renderAvatarGroup_unstable as renderAvatarGroup } from '@fluentui/react-avatar';
28+
import { renderAvatarGroupItem_unstable as renderAvatarGroupItem } from '@fluentui/react-avatar';
29+
import type { Slot } from '@fluentui/react-utilities';
30+
import type { TooltipBaseProps } from '@fluentui/react-tooltip';
31+
32+
// @public
33+
export const AvatarGroup: ForwardRefComponent<AvatarGroupProps>;
34+
35+
export { AvatarGroupContextValue }
36+
37+
export { AvatarGroupContextValues }
38+
39+
// @public
40+
export const AvatarGroupItem: ForwardRefComponent<AvatarGroupItemProps>;
41+
42+
// @public
43+
export type AvatarGroupItemProps = Omit<AvatarGroupItemBaseProps, 'color' | 'idForColor'>;
44+
45+
export { AvatarGroupItemSlots }
46+
47+
export { AvatarGroupItemState }
48+
49+
// @public
50+
export const AvatarGroupPopover: {
51+
(props: AvatarGroupPopoverProps): JSXElement;
52+
displayName: string;
53+
};
54+
55+
// @public
56+
export type AvatarGroupPopoverPopoverProps = Pick<PopoverProps, 'open' | 'defaultOpen' | 'onOpenChange' | 'openOnHover' | 'openOnContext' | 'mouseLeaveDelay' | 'positioning' | 'withArrow' | 'trapFocus'>;
57+
58+
// @public
59+
export type AvatarGroupPopoverProps = Omit<ComponentProps<Partial<AvatarGroupPopoverSlots>>, 'children'> & AvatarGroupPopoverPopoverProps & {
60+
indicator?: 'count' | 'icon';
61+
count?: number;
62+
children: React_2.ReactNode;
63+
};
64+
65+
// @public (undocumented)
66+
export type AvatarGroupPopoverSlots = {
67+
triggerButton: NonNullable<Slot<'button'>>;
68+
tooltip: NonNullable<Slot<typeof Tooltip>>;
69+
content: NonNullable<Slot<'ul'>>;
70+
popoverSurface: NonNullable<Slot<typeof PopoverSurface>>;
71+
};
72+
73+
// @public
74+
export type AvatarGroupPopoverState = ComponentState<AvatarGroupPopoverSlots> & Required<Pick<AvatarGroupPopoverProps, 'count' | 'indicator'>> & {
75+
popoverOpen: boolean;
76+
layout: AvatarGroupProps_2['layout'];
77+
popover: AvatarGroupPopoverPopoverProps;
78+
};
79+
80+
export { AvatarGroupProps }
81+
82+
export { AvatarGroupSlots }
83+
84+
export { AvatarGroupState }
85+
86+
export { PartitionAvatarGroupItems }
87+
88+
export { partitionAvatarGroupItems }
89+
90+
export { PartitionAvatarGroupItemsOptions }
91+
92+
export { renderAvatarGroup }
93+
94+
export { renderAvatarGroupItem }
95+
96+
// @public
97+
export const renderAvatarGroupPopover: (state: AvatarGroupPopoverState, contextValues: AvatarGroupContextValues) => JSXElement;
98+
99+
// @public
100+
export const useAvatarGroup: (props: AvatarGroupProps, ref: React_2.Ref<HTMLElement>) => AvatarGroupState;
101+
102+
// @public
103+
export const useAvatarGroupContextValues: (state: AvatarGroupState) => AvatarGroupContextValues;
104+
105+
// @public
106+
export const useAvatarGroupItem: (props: AvatarGroupItemProps, ref: React_2.Ref<HTMLElement>) => AvatarGroupItemState;
107+
108+
// @public
109+
export const useAvatarGroupPopover: (props: AvatarGroupPopoverProps) => AvatarGroupPopoverState;
110+
111+
// @public
112+
export const useAvatarGroupPopoverContextValues: (state: AvatarGroupPopoverState) => AvatarGroupContextValues;
113+
114+
// (No @packageDocumentation comment for this package)
115+
116+
```

packages/react-components/react-headless-components-preview/library/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
"import": "./lib/avatar.js",
8888
"require": "./lib-commonjs/avatar.js"
8989
},
90+
"./avatar-group": {
91+
"types": "./dist/avatar-group.d.ts",
92+
"node": "./lib-commonjs/avatar-group.js",
93+
"import": "./lib/avatar-group.js",
94+
"require": "./lib-commonjs/avatar-group.js"
95+
},
9096
"./badge": {
9197
"types": "./dist/badge.d.ts",
9298
"node": "./lib-commonjs/badge.js",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export {
2+
AvatarGroup,
3+
renderAvatarGroup,
4+
useAvatarGroup,
5+
useAvatarGroupContextValues,
6+
AvatarGroupItem,
7+
renderAvatarGroupItem,
8+
useAvatarGroupItem,
9+
AvatarGroupPopover,
10+
renderAvatarGroupPopover,
11+
useAvatarGroupPopover,
12+
useAvatarGroupPopoverContextValues,
13+
} from './components/AvatarGroup/index';
14+
export type {
15+
AvatarGroupSlots,
16+
AvatarGroupProps,
17+
AvatarGroupState,
18+
AvatarGroupContextValue,
19+
AvatarGroupContextValues,
20+
AvatarGroupItemSlots,
21+
AvatarGroupItemProps,
22+
AvatarGroupItemState,
23+
AvatarGroupPopoverSlots,
24+
AvatarGroupPopoverProps,
25+
AvatarGroupPopoverState,
26+
AvatarGroupPopoverPopoverProps,
27+
} from './components/AvatarGroup/index';
28+
29+
export { partitionAvatarGroupItems } from '@fluentui/react-avatar';
30+
export type { PartitionAvatarGroupItems, PartitionAvatarGroupItemsOptions } from '@fluentui/react-avatar';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { isConformant } from '../../testing/isConformant';
4+
import { AvatarGroup } from './AvatarGroup';
5+
import { AvatarGroupItem } from './AvatarGroupItem/AvatarGroupItem';
6+
7+
describe('AvatarGroup', () => {
8+
isConformant({
9+
Component: AvatarGroup,
10+
displayName: 'AvatarGroup',
11+
});
12+
13+
it('renders a default state', () => {
14+
const { getByRole } = render(
15+
<AvatarGroup>
16+
<AvatarGroupItem name="John Doe" />
17+
</AvatarGroup>,
18+
);
19+
20+
expect(getByRole('group')).toBeInTheDocument();
21+
});
22+
23+
it('renders its AvatarGroupItem children', () => {
24+
const { getAllByRole } = render(
25+
<AvatarGroup>
26+
<AvatarGroupItem name="John Doe" />
27+
<AvatarGroupItem name="Jane Smith" />
28+
</AvatarGroup>,
29+
);
30+
31+
expect(getAllByRole('img')).toHaveLength(2);
32+
});
33+
34+
it('forwards the layout prop through context to its items', () => {
35+
const { getByRole } = render(
36+
<AvatarGroup layout="stack">
37+
<AvatarGroupItem name="John Doe" />
38+
</AvatarGroup>,
39+
);
40+
41+
// The group renders as a `role="group"` container regardless of layout.
42+
expect(getByRole('group')).toBeInTheDocument();
43+
});
44+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
5+
6+
import type { AvatarGroupProps } from './AvatarGroup.types';
7+
import { useAvatarGroup } from './useAvatarGroup';
8+
import { useAvatarGroupContextValues } from './useAvatarGroupContextValues';
9+
import { renderAvatarGroup } from './renderAvatarGroup';
10+
11+
/**
12+
* The AvatarGroup component displays a group of Avatars, each represented as an AvatarGroupItem.
13+
*/
14+
export const AvatarGroup: ForwardRefComponent<AvatarGroupProps> = React.forwardRef((props, ref) => {
15+
const state = useAvatarGroup(props, ref);
16+
const contextValues = useAvatarGroupContextValues(state);
17+
18+
return renderAvatarGroup(state, contextValues);
19+
});
20+
21+
AvatarGroup.displayName = 'AvatarGroup';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type {
2+
AvatarGroupBaseProps as AvatarGroupProps,
3+
AvatarGroupBaseState as AvatarGroupState,
4+
AvatarGroupContextValue,
5+
AvatarGroupContextValues,
6+
AvatarGroupSlots,
7+
} from '@fluentui/react-avatar';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { isConformant } from '../../../testing/isConformant';
4+
import { AvatarGroup } from '../AvatarGroup';
5+
import { AvatarGroupPopover } from '../AvatarGroupPopover/AvatarGroupPopover';
6+
import { AvatarGroupItem } from './AvatarGroupItem';
7+
8+
describe('AvatarGroupItem', () => {
9+
isConformant({
10+
Component: AvatarGroupItem,
11+
displayName: 'AvatarGroupItem',
12+
disabledTests: ['has-top-level-file-extra', 'component-has-root-ref'],
13+
});
14+
15+
it('renders the headless Avatar with computed initials', () => {
16+
render(
17+
<AvatarGroup>
18+
<AvatarGroupItem name="John Doe" />
19+
</AvatarGroup>,
20+
);
21+
22+
expect(screen.getByRole('img', { name: 'John Doe' })).toBeInTheDocument();
23+
expect(screen.getByRole('img')).toHaveTextContent('JD');
24+
});
25+
26+
it('renders as a non-overflow item (div) by default', () => {
27+
const { container } = render(
28+
<AvatarGroup>
29+
<AvatarGroupItem name="John Doe" />
30+
</AvatarGroup>,
31+
);
32+
33+
// The item root is a `div` when not inside an AvatarGroupPopover.
34+
expect(container.querySelector('li')).not.toBeInTheDocument();
35+
});
36+
37+
it('renders as an overflow item (li) with a name label inside the popover', () => {
38+
render(
39+
<AvatarGroup>
40+
<AvatarGroupPopover defaultOpen>
41+
<AvatarGroupItem name="Jane Smith" />
42+
</AvatarGroupPopover>
43+
</AvatarGroup>,
44+
);
45+
46+
const listItem = screen.getByRole('listitem', { hidden: true });
47+
expect(listItem.tagName).toBe('LI');
48+
// The overflow label echoes the avatar name and is hidden from AT (avatar already labels it).
49+
const label = screen.getByText('Jane Smith');
50+
expect(label).toHaveAttribute('aria-hidden', 'true');
51+
});
52+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
5+
6+
import type { AvatarGroupItemProps } from './AvatarGroupItem.types';
7+
import { useAvatarGroupItem } from './useAvatarGroupItem';
8+
import { renderAvatarGroupItem } from './renderAvatarGroupItem';
9+
10+
/**
11+
* An AvatarGroupItem represents a single Avatar inside an AvatarGroup. When rendered
12+
* inside an AvatarGroupPopover it is displayed as an overflow item with its name label.
13+
*/
14+
export const AvatarGroupItem: ForwardRefComponent<AvatarGroupItemProps> = React.forwardRef((props, ref) => {
15+
const state = useAvatarGroupItem(props, ref);
16+
17+
return renderAvatarGroupItem(state);
18+
});
19+
20+
AvatarGroupItem.displayName = 'AvatarGroupItem';

0 commit comments

Comments
 (0)