Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
33c8028
Adding initial implementation of AvatarGroup
sopranopillow Apr 29, 2022
11be710
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow Apr 29, 2022
9a6f9b4
change files
sopranopillow Apr 29, 2022
5fe2af0
updating popover surface items to use ul and li, updating slots to be…
sopranopillow May 3, 2022
155cdde
t push
sopranopillow May 4, 2022
6a42db5
updating types and package.json
sopranopillow May 4, 2022
d1a04b9
updating api
sopranopillow May 4, 2022
69625b5
adding missing dependency
sopranopillow May 4, 2022
f5583b2
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow May 9, 2022
c8bb4c4
removing token replacement in strings, removing commons from types, a…
sopranopillow May 9, 2022
f3bfd1d
reverting yarn.lock
sopranopillow May 9, 2022
ffe8b41
fixing dependencies
sopranopillow May 9, 2022
575d8c6
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow May 10, 2022
cfeea42
updating types and renaming variables
sopranopillow May 10, 2022
2467069
removing strings prop
sopranopillow May 11, 2022
edbfe66
adding requested changes
sopranopillow May 13, 2022
f3bcb81
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow May 13, 2022
6fb6258
removing useAvatarGroup.ts
sopranopillow May 13, 2022
cd74a0e
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow May 17, 2022
5a6bced
adding context to Avatar and AvatarGroup
sopranopillow May 18, 2022
2bd144d
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow May 18, 2022
9761bab
adding single initial for pie layout
sopranopillow May 18, 2022
136b2ea
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow May 20, 2022
539f419
Adding requested changes
sopranopillow May 20, 2022
f802a98
updating snapshots
sopranopillow May 20, 2022
98c7dd5
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow Jun 2, 2022
43b2213
updating stories
sopranopillow Jun 2, 2022
e8e42a6
Merge branch 'avatargroup-implemenation' of https://github.com/sopran…
sopranopillow Jun 2, 2022
4fee5ba
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow Jun 2, 2022
2118864
removing context form Avatar
sopranopillow Jun 2, 2022
8da8f1f
updating AvatarGroup
sopranopillow Jun 2, 2022
bd3ae84
removing v9 button from AvtarGroup
sopranopillow Jun 2, 2022
7501379
removing styling for trigger
sopranopillow Jun 2, 2022
7801453
updating api
sopranopillow Jun 2, 2022
42cc81a
Adding requested changes
sopranopillow Jun 6, 2022
7f1f741
Merge branch 'master' of https://github.com/microsoft/fluentui into a…
sopranopillow Jun 6, 2022
8d932f2
updating stories and snapshots
sopranopillow Jun 6, 2022
6b38759
adding requested changes
sopranopillow Jun 6, 2022
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Adding initial implementation of AvatarGroup",
"packageName": "@fluentui/react-avatar",
"email": "email not defined",
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
"dependentChangeType": "none"
}
26 changes: 18 additions & 8 deletions packages/react-components/react-avatar/etc/react-avatar.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,34 @@ export const AvatarGroup: ForwardRefComponent<AvatarGroupProps>;
// @public (undocumented)
export const avatarGroupClassNames: SlotClassNames<AvatarGroupSlots>;

// @public (undocumented)
export type AvatarGroupContextValue = Pick<AvatarGroupProps, 'size' | 'layout'> & {
color?: AvatarProps['color'];
};

// @public (undocumented)
export type AvatarGroupContextValues = {
avatarGroup: AvatarGroupContextValue;
};

// @public
export type AvatarGroupProps = ComponentProps<AvatarGroupSlots> & {
export type AvatarGroupProps = ComponentProps<Partial<AvatarGroupSlots>> & {
layout?: 'spread' | 'stack' | 'pie';
maxAvatars?: number;
overflowIndicator?: 'number-overflowed' | 'icon';
overflowIndicator?: 'count' | 'icon';
size?: AvatarSizes;
strings?: AvatarGroupStrings;
};

// @public (undocumented)
export type AvatarGroupSlots = {
root: Slot<'div'>;
popoverTrigger?: Slot<typeof Button>;
popoverSurface?: Slot<typeof PopoverSurface>;
root: NonNullable<Slot<'div'>>;
popoverTrigger: NonNullable<Slot<typeof Button>>;
popoverSurface: NonNullable<Slot<typeof PopoverSurface>>;
};

// @public
export type AvatarGroupState = ComponentState<AvatarGroupSlots> & {
export type AvatarGroupState = ComponentState<AvatarGroupSlots> & Required<Pick<AvatarGroupProps, 'layout' | 'maxAvatars' | 'size' | 'overflowIndicator'>> & {
hasOverflow: boolean;
tooltipContent: TooltipProps['content'];
};

Expand Down Expand Up @@ -92,7 +102,7 @@ export function getInitials(displayName: string | undefined | null, isRtl: boole
export const renderAvatar_unstable: (state: AvatarState) => JSX.Element;

// @public
export const renderAvatarGroup_unstable: (state: AvatarGroupState) => JSX.Element;
export const renderAvatarGroup_unstable: (state: AvatarGroupState, contextValues: AvatarGroupContextValues) => JSX.Element;

// @public (undocumented)
export const useAvatar_unstable: (props: AvatarProps, ref: React_2.Ref<HTMLElement>) => AvatarState;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/react-avatar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
"dependencies": {
"@fluentui/react-badge": "9.0.0-rc.8",
"@fluentui/react-button": "9.0.0-rc.9",
"@fluentui/react-context-selector": "9.0.0-rc.8",
"@fluentui/react-icons": "^2.0.166-rc.3",
"@fluentui/react-label": "9.0.0-rc.1",
"@fluentui/react-popover": "9.0.0-rc.9",
"@fluentui/react-tooltip": "9.0.0-rc.9",
"@fluentui/react-theme": "9.0.0-rc.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import { getInitials } from '../../utils/index';
import type { AvatarNamedColor, AvatarProps, AvatarState } from './Avatar.types';
import { PersonRegular } from '@fluentui/react-icons';
import { PresenceBadge } from '@fluentui/react-badge';
import { AvatarGroupContext } from '../../contexts/AvatarGroupContext';
import { useContextSelector } from '@fluentui/react-context-selector';
import { useFluent } from '@fluentui/react-shared-contexts';
import { useMergedEventCallbacks } from '@fluentui/react-utilities';

export const useAvatar_unstable = (props: AvatarProps, ref: React.Ref<HTMLElement>): AvatarState => {
const { dir } = useFluent();
const { name, size = 32, shape = 'circular', active = 'unset', activeAppearance = 'ring', idForColor } = props;
let { color = 'neutral' } = props;
const contextColor = useContextSelector(AvatarGroupContext, ctx => ctx.color);
const avatarGroupLayout = useContextSelector(AvatarGroupContext, ctx => ctx.layout);
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
const size = useContextSelector(AvatarGroupContext, ctx => ctx.size) ?? props.size ?? 32;

const { name, shape = 'circular', active = 'unset', activeAppearance = 'ring', idForColor } = props;
let color = props.color ?? contextColor ?? 'neutral';
Comment thread
sopranopillow marked this conversation as resolved.
Outdated

// Resolve 'colorful' to a specific color name
if (color === 'colorful') {
Expand All @@ -31,11 +37,13 @@ export const useAvatar_unstable = (props: AvatarProps, ref: React.Ref<HTMLElemen
/* excludedPropNames: */ ['name'],
);

const firstInitialOnly = size <= 16 || (avatarGroupLayout === 'pie' && size < 40);

// Resolve the initials slot, defaulted to getInitials.
let initials: AvatarState['initials'] = resolveShorthand(props.initials, {
required: true,
defaultProps: {
children: getInitials(name, dir === 'rtl', { firstInitialOnly: size <= 16 }),
children: getInitials(name, dir === 'rtl', { firstInitialOnly }),
'aria-hidden': true,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ const useStyles = makeStyles({
icon48: { fontSize: '48px' },
});

const useSizeStyles = makeStyles({
export const useSizeStyles = makeStyles({
16: { width: '16px', height: '16px' },
20: { width: '20px', height: '20px' },
24: { width: '24px', height: '24px' },
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { render } from '@testing-library/react';
import { AvatarGroup } from './AvatarGroup';
import { isConformant } from '../../common/isConformant';
import { Avatar } from '../Avatar';

describe('AvatarGroup', () => {
// TODO: Remove component-has-static-classnames-object from disabled tests.
Expand All @@ -18,7 +19,19 @@ describe('AvatarGroup', () => {
// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<AvatarGroup>Default AvatarGroup</AvatarGroup>);
const result = render(
<AvatarGroup>
<Avatar name="Katri Athokas" />
<Avatar name="Elvia Atkins" />
<Avatar name="Cameron Evans" />
<Avatar name="Wanda Howard" />
<Avatar name="Mona Kane" />
<Avatar name="Allan Munger" />
<Avatar name="Daisy Phillips" />
<Avatar name="Robert Tolbert" />
<Avatar name="Kevin Sturgis" />
</AvatarGroup>,
);
Comment on lines +22 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be changed in a future PR, but ideally we wouldn't be using snapshots in any tests. Instead, use testing-library queries to check specific aspects of the rendered tree in different scenarios. See Avatar.test.tsx for some examples of that. (The tests should be testing the specific functions and features AvatarGroup itself; e.g. the logic in useAvatarGroup).

expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { renderAvatarGroup_unstable } from './renderAvatarGroup';
import { useAvatarGroupStyles_unstable } from './useAvatarGroupStyles';
import type { AvatarGroupProps } from './AvatarGroup.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { useAvatarGroupContextValues } from '../../contexts/useAvatarGroupContextValues';

/**
* AvatarGroup component - TODO: add more docs
* The AvatarGroup component represents a group of multiple people or entities by taking care of the arrangement
* of individual Avatars in a spread, stack, or pie layout.
*/
export const AvatarGroup: ForwardRefComponent<AvatarGroupProps> = React.forwardRef((props, ref) => {
const state = useAvatarGroup_unstable(props, ref);
const contextValues = useAvatarGroupContextValues(state);

useAvatarGroupStyles_unstable(state);
return renderAvatarGroup_unstable(state);
return renderAvatarGroup_unstable(state, contextValues);
});

AvatarGroup.displayName = 'AvatarGroup';
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { PopoverSurface } from '@fluentui/react-popover';
import { AvatarSizes } from '../Avatar/Avatar.types';
import { AvatarProps, AvatarSizes } from '../Avatar/Avatar.types';
import { Button } from '@fluentui/react-button';
import { PopoverSurface } from '@fluentui/react-popover';
import { TooltipProps } from '@fluentui/react-tooltip';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

export type AvatarGroupSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div'>>;

/**
* Popover trigger slot that can be used to change the overflow indicator.
*/
popoverTrigger?: Slot<typeof Button>;
Comment thread
sopranopillow marked this conversation as resolved.
popoverTrigger: NonNullable<Slot<typeof Button>>;

/**
* Popover surface that will be displayed when the popover is triggered.
*/
popoverSurface?: Slot<typeof PopoverSurface>;
popoverSurface: NonNullable<Slot<typeof PopoverSurface>>;
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
};

/**
* AvatarGroup Props
*/
export type AvatarGroupProps = ComponentProps<AvatarGroupSlots> & {
export type AvatarGroupProps = ComponentProps<Partial<AvatarGroupSlots>> & {
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
/**
* Layout the Avatars should be displayed as.
* @default spread
Expand All @@ -37,33 +37,38 @@ export type AvatarGroupProps = ComponentProps<AvatarGroupSlots> & {

/**
* Whether the overflow indicator should render an icon instead of the number of overflowed avatars.
* @default false
* @default count
*/
overflowIndicator?: 'number-overflowed' | 'icon';
overflowIndicator?: 'count' | 'icon';

/**
* Size of the avatars.
* @default 32
*/
size?: AvatarSizes;

/**
* Strings for localizing text in the tooltip.
*/
strings?: AvatarGroupStrings;
};

/**
* State used in rendering AvatarGroup
*/
export type AvatarGroupState = ComponentState<AvatarGroupSlots> & {
tooltipContent: TooltipProps['content'];
export type AvatarGroupState = ComponentState<AvatarGroupSlots> &
Required<Pick<AvatarGroupProps, 'layout' | 'maxAvatars' | 'size' | 'overflowIndicator'>> & {
/**
* Whether there are more Avatars than `maxAvatars`.
* @default false
*/
hasOverflow: boolean;

/**
* Tooltip content for the overflow indicator.
*/
tooltipContent: TooltipProps['content'];
};

export type AvatarGroupContextValue = Pick<AvatarGroupProps, 'size' | 'layout'> & {
color?: AvatarProps['color'];
};

export type AvatarGroupStrings = {
/**
* Text applied to the overflow indicator's tooltip.
* Can include the token "\{numOverflowedAvatars\}" which will be replaced with the number of overflowed avatars.
*/
tooltipContent: string;
export type AvatarGroupContextValues = {
avatarGroup: AvatarGroupContextValue;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,86 @@ exports[`AvatarGroup renders a default state 1`] = `
<div>
<div
class="fui-AvatarGroup"
id="avatarGroup-7"
role="group"
>
Default AvatarGroup
<span
aria-label="Katri Athokas"
class="fui-Avatar"
id="avatar-8"
role="img"
>
<span
aria-hidden="true"
class="fui-Avatar__initials"
>
KA
</span>
</span>
<span
aria-label="Elvia Atkins"
class="fui-Avatar"
id="avatar-9"
role="img"
>
<span
aria-hidden="true"
class="fui-Avatar__initials"
>
EA
</span>
</span>
<span
aria-label="Cameron Evans"
class="fui-Avatar"
id="avatar-10"
role="img"
>
<span
aria-hidden="true"
class="fui-Avatar__initials"
>
CE
</span>
</span>
<span
aria-label="Wanda Howard"
class="fui-Avatar"
id="avatar-11"
role="img"
>
<span
aria-hidden="true"
class="fui-Avatar__initials"
>
WH
</span>
</span>
<span
aria-label="Mona Kane"
class="fui-Avatar"
id="avatar-12"
role="img"
>
<span
aria-hidden="true"
class="fui-Avatar__initials"
>
MK
</span>
</span>
<button
aria-describedby="tooltip-14"
aria-haspopup="dialog"
class="fui-Button fui-AvatarGroup__popoverTrigger"
data-tabster="{\\"deloser\\":{}}"
type="button"
>
+4
</button>
<span
hidden=""
/>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import * as React from 'react';
import { Popover, PopoverTrigger } from '@fluentui/react-popover';
import { Tooltip } from '@fluentui/react-tooltip';
import { getSlots } from '@fluentui/react-utilities';
import type { AvatarGroupState, AvatarGroupSlots } from './AvatarGroup.types';
import type { AvatarGroupState, AvatarGroupSlots, AvatarGroupContextValues } from './AvatarGroup.types';
import { AvatarGroupContext } from '../../contexts/AvatarGroupContext';

/**
* Render the final JSX of AvatarGroup
*/
export const renderAvatarGroup_unstable = (state: AvatarGroupState) => {
export const renderAvatarGroup_unstable = (state: AvatarGroupState, contextValues: AvatarGroupContextValues) => {
const { slots, slotProps } = getSlots<AvatarGroupSlots>(state);

// TODO Add additional slots in the appropriate place
return <slots.root {...slotProps.root} />;
return (
<AvatarGroupContext.Provider value={contextValues.avatarGroup}>
<slots.root {...slotProps.root}>
{state.root.children}
{state.hasOverflow && (
<Popover trapFocus size="small">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also need a slot for this Popover so it can be configured by the user (fine to do in a separate PR).

@sopranopillow sopranopillow Jun 6, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Popover is not rendered as an element, it cannot be made into a slot.

<PopoverTrigger>
<Tooltip content={state.tooltipContent} relationship="description" appearance="inverted">
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
<slots.popoverTrigger {...slotProps.popoverTrigger} />
</Tooltip>
</PopoverTrigger>
<slots.popoverSurface {...slotProps.popoverSurface} />
</Popover>
)}
</slots.root>
</AvatarGroupContext.Provider>
);
};
Loading