Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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": "esteban.230@hotmail.com",
"dependentChangeType": "patch"
}
19 changes: 12 additions & 7 deletions packages/react-components/react-avatar/etc/react-avatar.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
```ts

import { Button } from '@fluentui/react-button';
import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import { ComponentProps } from '@fluentui/react-utilities';
import { ComponentState } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { Label } from '@fluentui/react-label';
import { PopoverSurface } from '@fluentui/react-popover';
import { PresenceBadge } from '@fluentui/react-badge';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
import { Slot } from '@fluentui/react-utilities';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { TooltipProps } from '@fluentui/react-tooltip';

Expand All @@ -34,15 +35,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'> | Slot<'li'>>;
avatar: NonNullable<Slot<typeof Avatar>>;
label?: Slot<typeof Label>;
};

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

// @public
export type AvatarGroupProps = ComponentProps<AvatarGroupSlots> & {
Expand All @@ -61,7 +66,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
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.11",
"@fluentui/react-button": "9.0.0-rc.12",
"@fluentui/react-context-selector": "9.0.0-rc.9",
"@fluentui/react-icons": "^2.0.166-rc.3",
"@fluentui/react-label": "9.0.0-rc.4",
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
"@fluentui/react-popover": "9.0.0-rc.12",
"@fluentui/react-tooltip": "9.0.0-rc.12",
"@fluentui/react-theme": "9.0.0-rc.9",
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 { AvatarProps, 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,14 +56,25 @@ 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.
* Can include the token "\{numOverflowedAvatars\}" which will be replaced with the number of overflowed avatars.
*/
tooltipContent: string;
};

export type AvatarGroupContextValue = Pick<AvatarGroupProps, 'size' | 'layout'> & {
overflowItem?: boolean;
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
color?: AvatarProps['color'];
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
};
Comment thread
sopranopillow marked this conversation as resolved.
Outdated

export type AvatarGroupContextValues = {
avatarGroup: AvatarGroupContextValue;
};
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',
Comment thread
behowell marked this conversation as resolved.
size = 36,
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
strings = avatarGroupDefaultStrings,
} = props;

return {
// TODO: Replace with actual logic.
Comment thread
behowell marked this conversation as resolved.
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.
Comment thread
sopranopillow marked this conversation as resolved.
*/
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 { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { Label } from '@fluentui/react-label';
import type { Avatar } from '../../Avatar';

export type AvatarGroupItemSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div'> | Slot<'li'>>;
Comment thread
sopranopillow marked this conversation as resolved.
Outdated

/**
* 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.
*/
label?: Slot<typeof Label>;
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
};

/**
* 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.label && <slots.label {...slotProps.label} />}
</slots.root>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as React from 'react';
import { getNativeElementProps } from '@fluentui/react-utilities';
import { Avatar } from '../Avatar/Avatar';
import { AvatarGroupContext } from '../../contexts/AvatarGroupContext';
import { Label } from '@fluentui/react-label';
import { resolveShorthand } from '@fluentui/react-utilities';
import { useContextSelector } from '@fluentui/react-context-selector';
import type { AvatarGroupItemProps, AvatarGroupItemState } from './AvatarGroupItem.types';

/**
Expand All @@ -15,17 +19,42 @@ export const useAvatarGroupItem_unstable = (
props: AvatarGroupItemProps,
ref: React.Ref<HTMLElement>,
): AvatarGroupItemState => {
const groupOverflowItem = useContextSelector(AvatarGroupContext, ctx => ctx.overflowItem);
const groupSize = useContextSelector(AvatarGroupContext, ctx => ctx.size);
const itemColor = useContextSelector(AvatarGroupContext, ctx => ctx.color);
// Since the primary slot is not an input element, getPartitionedNativeProps cannot be used here.
const { style, className, ...avatarSlotProps } = props;
Comment thread
sopranopillow marked this conversation as resolved.
Outdated

return {
// TODO add appropriate props/defaults
isOverflowItem: groupOverflowItem,
components: {
// TODO add each slot's element type or component
root: 'div',
avatar: Avatar,
label: Label,
},
// 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: groupOverflowItem ? 'li' : 'div',
role: groupOverflowItem ? 'listitem' : undefined,
},
}),
avatar: resolveShorthand(props.avatar, {
required: true,
defaultProps: {
ref,
size: groupSize,
color: itemColor,
...avatarSlotProps,
},
}),
label: resolveShorthand(props.label, {
required: true,
defaultProps: {
children: groupOverflowItem ? props.name : null,
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
},
}),
};
};
Original file line number Diff line number Diff line change
@@ -1,33 +1,64 @@
import { makeStyles, mergeClasses } 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',
label: 'fui-AvatarGroupItem__label',
};

/**
* 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)': {
marginTop: tokens.spacingVerticalS,
},
Comment thread
sopranopillow marked this conversation as resolved.
},
});

// TODO add additional classes for different states and/or slots
/**
* Styles for the label slot
*/
const useLabelStyles = makeStyles({
overflowItem: {
marginLeft: tokens.spacingHorizontalS,
position: 'relative',
Comment thread
sopranopillow marked this conversation as resolved.
Outdated
},
});

/**
* 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 labelStyles = useLabelStyles();

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.label) {
state.label.className = mergeClasses(
avatarGroupItemClassNames.label,
isOverflowItem && labelStyles.overflowItem,
state.label.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 '../AvatarGroup';

/**
* 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,2 @@
export * from './AvatarGroupContext';
export * from './useAvatarGroupContextValues';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AvatarGroupContextValue, AvatarGroupContextValues, AvatarGroupState } from '../AvatarGroup';

export const useAvatarGroupContextValues = (state: AvatarGroupState): AvatarGroupContextValues => {
const { size, layout } = state;

const avatarGroup: AvatarGroupContextValue = {
color: 'colorful',
layout,
size,
};

return { avatarGroup };
};
Comment thread
sopranopillow marked this conversation as resolved.
Outdated