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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Adding AvatarGroupItem implementation and AvatarGroup context.",
"packageName": "@fluentui/react-avatar",
"email": "[email protected]",
"dependentChangeType": "patch"
}
12 changes: 8 additions & 4 deletions packages/react-components/react-avatar/etc/react-avatar.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ export const AvatarGroupItem: ForwardRefComponent<AvatarGroupItemProps>;
export const avatarGroupItemClassNames: SlotClassNames<AvatarGroupItemSlots>;

// @public
export type AvatarGroupItemProps = ComponentProps<AvatarGroupItemSlots> & {};
export type AvatarGroupItemProps = ComponentProps<Partial<AvatarGroupItemSlots>, 'avatar'>;

// @public (undocumented)
export type AvatarGroupItemSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div', 'li'>>;
avatar: NonNullable<Slot<typeof Avatar>>;
overflowLabel: NonNullable<Slot<'span'>>;
};

// @public
export type AvatarGroupItemState = ComponentState<AvatarGroupItemSlots>;
export type AvatarGroupItemState = ComponentState<AvatarGroupItemSlots> & {
isOverflowItem?: boolean;
};

// @public
export type AvatarGroupProps = ComponentProps<AvatarGroupSlots> & {
Expand All @@ -61,7 +65,7 @@ export type AvatarGroupSlots = {
};

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

Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-avatar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"@fluentui/react-badge": "9.0.0-rc.12",
"@fluentui/react-button": "9.0.0-rc.13",
"@fluentui/react-context-selector": "9.0.0-rc.10",
"@fluentui/react-icons": "^2.0.166-rc.3",
"@fluentui/react-popover": "9.0.0-rc.13",
"@fluentui/react-tooltip": "9.0.0-rc.13",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { PopoverSurface } from '@fluentui/react-popover';
import { 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 { AvatarSizes } from '../Avatar/Avatar.types';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

export type AvatarGroupSlots = {
root: Slot<'div'>;
Expand Down Expand Up @@ -56,10 +56,12 @@ export type AvatarGroupProps = ComponentProps<AvatarGroupSlots> & {
/**
* 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'>> & {
tooltipContent: TooltipProps['content'];
};

// TODO: Remove strings from AvatarGroup.
export type AvatarGroupStrings = {
/**
* Text applied to the overflow indicator's tooltip.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ import { avatarGroupDefaultStrings } from './AvatarGroup.strings';
* @param ref - reference to root HTMLElement of AvatarGroup
*/
export const useAvatarGroup_unstable = (props: AvatarGroupProps, ref: React.Ref<HTMLElement>): AvatarGroupState => {
const { children, strings = avatarGroupDefaultStrings } = props;
const {
children,
layout = 'spread',
maxAvatars = 5,
overflowIndicator = 'number-overflowed',
size = 32,
strings = avatarGroupDefaultStrings,
} = props;

return {
// TODO: Replace with actual logic.
layout,
maxAvatars,
overflowIndicator,
size,
tooltipContent: strings.tooltipContent.replace('{numOverflowedAvatars}', String(10)),
components: {
// TODO add each slot's element type or component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ describe('AvatarGroupItem', () => {
isConformant({
Component: AvatarGroupItem,
displayName: 'AvatarGroupItem',
disabledTests: ['component-has-static-classname', 'component-has-static-classname-exported'],
// TODO: enable component-has-static-classnames-object
disabledTests: [
'component-has-static-classname',
'component-has-static-classname-exported',
'component-has-static-classnames-object',
],
primarySlot: 'avatar',
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<AvatarGroupItem>Default AvatarGroupItem</AvatarGroupItem>);
const result = render(<AvatarGroupItem name="Default AvatarGroupItem" />);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { AvatarGroupItemProps } from './AvatarGroupItem.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';

/**
* AvatarGroupItem component - TODO: add more docs
* The AvatarGroupItem component represents a single person or entity.
* AvatarGroupItem should only be used in an AvatarGroup component.
*/
export const AvatarGroupItem: ForwardRefComponent<AvatarGroupItemProps> = React.forwardRef((props, ref) => {
const state = useAvatarGroupItem_unstable(props, ref);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { Avatar } from '../../Avatar';

export type AvatarGroupItemSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div', 'li'>>;

/**
* Avatar that represents a person or entity.
*/
avatar: NonNullable<Slot<typeof Avatar>>;

/**
* Label used for the name of the AvatarGroupItem when rendered as an overflow item.
* The content of the label, by default, is the `name` prop from the `avatar` slot.
*/
overflowLabel: NonNullable<Slot<'span'>>;
};

/**
* AvatarGroupItem Props
*/
export type AvatarGroupItemProps = ComponentProps<AvatarGroupItemSlots> & {};
export type AvatarGroupItemProps = ComponentProps<Partial<AvatarGroupItemSlots>, 'avatar'>;

/**
* State used in rendering AvatarGroupItem
*/
export type AvatarGroupItemState = ComponentState<AvatarGroupItemSlots>;
export type AvatarGroupItemState = ComponentState<AvatarGroupItemSlots> & {
/**
* Whether the Avatar is an overflow item.
*
* @default false
*/
isOverflowItem?: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ exports[`AvatarGroupItem renders a default state 1`] = `
<div
class="fui-AvatarGroupItem"
>
Default AvatarGroupItem
<span
aria-label="Default AvatarGroupItem"
class="fui-Avatar fui-AvatarGroupItem__avatar"
id="avatar-8"
role="img"
>
<span
class="fui-Avatar__initials"
id="avatar-8__initials"
>
DA
</span>
</span>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import type { AvatarGroupItemState, AvatarGroupItemSlots } from './AvatarGroupIt
export const renderAvatarGroupItem_unstable = (state: AvatarGroupItemState) => {
const { slots, slotProps } = getSlots<AvatarGroupItemSlots>(state);

// TODO Add additional slots in the appropriate place
return <slots.root {...slotProps.root} />;
return (
<slots.root {...slotProps.root}>
<slots.avatar {...slotProps.avatar} />
{state.isOverflowItem && <slots.overflowLabel {...slotProps.overflowLabel} />}
</slots.root>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as React from 'react';
import { getNativeElementProps } from '@fluentui/react-utilities';
import { Avatar } from '../Avatar/Avatar';
import { AvatarGroupContext } from '../../contexts/AvatarGroupContext';
import { resolveShorthand } from '@fluentui/react-utilities';
import { useContextSelector } from '@fluentui/react-context-selector';
import type { AvatarGroupItemProps, AvatarGroupItemState } from './AvatarGroupItem.types';

/**
Expand All @@ -15,17 +18,41 @@ export const useAvatarGroupItem_unstable = (
props: AvatarGroupItemProps,
ref: React.Ref<HTMLElement>,
): AvatarGroupItemState => {
const groupIsOverflow = useContextSelector(AvatarGroupContext, ctx => ctx.isOverflow);
const groupSize = useContextSelector(AvatarGroupContext, ctx => ctx.size);
// Since the primary slot is not an intrinsic element, getPartitionedNativeProps cannot be used here.
const { style, className, ...avatarSlotProps } = props;

return {
// TODO add appropriate props/defaults
isOverflowItem: groupIsOverflow,
components: {
// TODO add each slot's element type or component
root: 'div',
avatar: Avatar,
overflowLabel: 'span',
},
// TODO add appropriate slots, for example:
// mySlot: resolveShorthand(props.mySlot),
root: getNativeElementProps('div', {
ref,
...props,
root: resolveShorthand(props.root, {
required: true,
defaultProps: {
style,
className,
as: groupIsOverflow ? 'li' : 'div',
role: groupIsOverflow ? 'listitem' : undefined,
},
}),
avatar: resolveShorthand(props.avatar, {
required: true,
defaultProps: {
ref,
size: groupSize,
color: 'colorful',
...avatarSlotProps,
},
}),
overflowLabel: resolveShorthand(props.overflowLabel, {
required: true,
defaultProps: {
children: props.name,
},
}),
};
};
Original file line number Diff line number Diff line change
@@ -1,33 +1,63 @@
import { makeStyles, mergeClasses } from '@griffel/react';
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
import type { AvatarGroupItemSlots, AvatarGroupItemState } from './AvatarGroupItem.types';
import type { SlotClassNames } from '@fluentui/react-utilities';

export const avatarGroupItemClassNames: SlotClassNames<AvatarGroupItemSlots> = {
root: 'fui-AvatarGroupItem',
// TODO: add class names for all slots on AvatarGroupItemSlots.
// Should be of the form `<slotName>: 'fui-AvatarGroupItem__<slotName>`
avatar: 'fui-AvatarGroupItem__avatar',
overflowLabel: 'fui-AvatarGroupItem__overflowLabel',
};

/**
* Styles for the root slot
*/
const useStyles = makeStyles({
root: {
// TODO Add default styles for the root element
const useRootStyles = makeStyles({
base: {
alignItems: 'center',
display: 'flex',
},
overflowItem: {
'&:not(:first-child)': {
...shorthands.padding(tokens.spacingVerticalXS, tokens.spacingVerticalXS),
},
},
});

// TODO add additional classes for different states and/or slots
/**
* Styles for the label slot
*/
const useOverflowLabelStyles = makeStyles({
overflowItem: {
marginLeft: tokens.spacingHorizontalS,
},
});

/**
* Apply styling to the AvatarGroupItem slots based on the state
*/
export const useAvatarGroupItemStyles_unstable = (state: AvatarGroupItemState): AvatarGroupItemState => {
const styles = useStyles();
state.root.className = mergeClasses(avatarGroupItemClassNames.root, styles.root, state.root.className);
const { isOverflowItem } = state;

const rootStyles = useRootStyles();
const overflowLabelStyles = useOverflowLabelStyles();

state.root.className = mergeClasses(
avatarGroupItemClassNames.root,
rootStyles.base,
isOverflowItem && rootStyles.overflowItem,
state.root.className,
);

state.avatar.className = mergeClasses(avatarGroupItemClassNames.avatar, state.avatar.className);

// TODO Add class names to slots, for example:
// state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className);
if (state.overflowLabel) {
state.overflowLabel.className = mergeClasses(
avatarGroupItemClassNames.overflowLabel,
isOverflowItem && overflowLabelStyles.overflowItem,
state.overflowLabel.className,
);
}

return state;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from '@fluentui/react-context-selector';
import type { Context } from '@fluentui/react-context-selector';
import type { AvatarGroupContextValue } from './AvatarGroupContext.types';

/**
* AvatarGroupContext is provided by AvatarGroup, and is consumed by Avatar to determine default values of some props.
*/
export const AvatarGroupContext: Context<AvatarGroupContextValue> = createContext({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { AvatarGroupProps } from '../AvatarGroup';

export type AvatarGroupContextValue = Pick<AvatarGroupProps, 'size' | 'layout'> & {
isOverflow?: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AvatarGroupContext';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from 'react';
import { AvatarGroupItem, AvatarGroupItemProps } from '../index';

export const Default = (props: Partial<AvatarGroupItemProps>) => <AvatarGroupItem {...props} />;
export const Default = (props: Partial<AvatarGroupItemProps>) => <AvatarGroupItem name="Katri Athokas" {...props} />;