diff --git a/change/@fluentui-react-avatar-1ab0ce5a-6046-4069-8ac6-496201496645.json b/change/@fluentui-react-avatar-1ab0ce5a-6046-4069-8ac6-496201496645.json new file mode 100644 index 00000000000000..341b1123863243 --- /dev/null +++ b/change/@fluentui-react-avatar-1ab0ce5a-6046-4069-8ac6-496201496645.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adding AvatarGroupItem implementation and AvatarGroup context.", + "packageName": "@fluentui/react-avatar", + "email": "esteban.230@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-avatar/etc/react-avatar.api.md b/packages/react-components/react-avatar/etc/react-avatar.api.md index acd961b9ba29b7..8c49b7d03b5355 100644 --- a/packages/react-components/react-avatar/etc/react-avatar.api.md +++ b/packages/react-components/react-avatar/etc/react-avatar.api.md @@ -34,15 +34,19 @@ export const AvatarGroupItem: ForwardRefComponent; export const avatarGroupItemClassNames: SlotClassNames; // @public -export type AvatarGroupItemProps = ComponentProps & {}; +export type AvatarGroupItemProps = ComponentProps, 'avatar'>; // @public (undocumented) export type AvatarGroupItemSlots = { - root: Slot<'div'>; + root: NonNullable>; + avatar: NonNullable>; + overflowLabel: NonNullable>; }; // @public -export type AvatarGroupItemState = ComponentState; +export type AvatarGroupItemState = ComponentState & { + isOverflowItem?: boolean; +}; // @public export type AvatarGroupProps = ComponentProps & { @@ -61,7 +65,7 @@ export type AvatarGroupSlots = { }; // @public -export type AvatarGroupState = ComponentState & { +export type AvatarGroupState = ComponentState & Required> & { tooltipContent: TooltipProps['content']; }; diff --git a/packages/react-components/react-avatar/package.json b/packages/react-components/react-avatar/package.json index 6d4e1898bb65f6..e32a6c18d63ff8 100644 --- a/packages/react-components/react-avatar/package.json +++ b/packages/react-components/react-avatar/package.json @@ -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", diff --git a/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.types.ts b/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.types.ts index 390cdc9dfcb530..b774264753ddf9 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.types.ts +++ b/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.types.ts @@ -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'>; @@ -56,10 +56,12 @@ export type AvatarGroupProps = ComponentProps & { /** * State used in rendering AvatarGroup */ -export type AvatarGroupState = ComponentState & { - tooltipContent: TooltipProps['content']; -}; +export type AvatarGroupState = ComponentState & + Required> & { + tooltipContent: TooltipProps['content']; + }; +// TODO: Remove strings from AvatarGroup. export type AvatarGroupStrings = { /** * Text applied to the overflow indicator's tooltip. diff --git a/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.ts b/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.ts index 77647222201e6b..8b3d74161ad6d6 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.ts +++ b/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.ts @@ -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): 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 diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx index 98a0be76fd4312..f0883e79bf27e6 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx @@ -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(Default AvatarGroupItem); + const result = render(); expect(result.container).toMatchSnapshot(); }); }); diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.tsx b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.tsx index 4cd0c1183f1a8c..9a579535f35e2e 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.tsx +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.tsx @@ -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 = React.forwardRef((props, ref) => { const state = useAvatarGroupItem_unstable(props, ref); diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.types.ts b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.types.ts index 1fdd7f36369153..d8d19638afcd7c 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.types.ts +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.types.ts @@ -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>; + + /** + * Avatar that represents a person or entity. + */ + avatar: NonNullable>; + + /** + * 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>; }; /** * AvatarGroupItem Props */ -export type AvatarGroupItemProps = ComponentProps & {}; +export type AvatarGroupItemProps = ComponentProps, 'avatar'>; /** * State used in rendering AvatarGroupItem */ -export type AvatarGroupItemState = ComponentState; +export type AvatarGroupItemState = ComponentState & { + /** + * Whether the Avatar is an overflow item. + * + * @default false + */ + isOverflowItem?: boolean; +}; diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/__snapshots__/AvatarGroupItem.test.tsx.snap b/packages/react-components/react-avatar/src/components/AvatarGroupItem/__snapshots__/AvatarGroupItem.test.tsx.snap index 490dd390059bc5..9cb36e01df8569 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/__snapshots__/AvatarGroupItem.test.tsx.snap +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/__snapshots__/AvatarGroupItem.test.tsx.snap @@ -5,7 +5,19 @@ exports[`AvatarGroupItem renders a default state 1`] = `
- Default AvatarGroupItem + + + DA + +
`; diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/renderAvatarGroupItem.tsx b/packages/react-components/react-avatar/src/components/AvatarGroupItem/renderAvatarGroupItem.tsx index 9ca22b9d585caf..6b8fac77d0ae91 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/renderAvatarGroupItem.tsx +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/renderAvatarGroupItem.tsx @@ -8,6 +8,10 @@ import type { AvatarGroupItemState, AvatarGroupItemSlots } from './AvatarGroupIt export const renderAvatarGroupItem_unstable = (state: AvatarGroupItemState) => { const { slots, slotProps } = getSlots(state); - // TODO Add additional slots in the appropriate place - return ; + return ( + + + {state.isOverflowItem && } + + ); }; diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItem.ts b/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItem.ts index e6f737437e460b..8203a44a83f064 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItem.ts +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItem.ts @@ -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'; /** @@ -15,17 +18,41 @@ export const useAvatarGroupItem_unstable = ( props: AvatarGroupItemProps, ref: React.Ref, ): 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, + }, }), }; }; diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts b/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts index c369ce15091e24..c051ce8e461061 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts @@ -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 = { root: 'fui-AvatarGroupItem', - // TODO: add class names for all slots on AvatarGroupItemSlots. - // Should be of the form `: 'fui-AvatarGroupItem__` + 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; }; diff --git a/packages/react-components/react-avatar/src/contexts/AvatarGroupContext.ts b/packages/react-components/react-avatar/src/contexts/AvatarGroupContext.ts new file mode 100644 index 00000000000000..3b7a9f578f6b69 --- /dev/null +++ b/packages/react-components/react-avatar/src/contexts/AvatarGroupContext.ts @@ -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 = createContext({}); diff --git a/packages/react-components/react-avatar/src/contexts/AvatarGroupContext.types.ts b/packages/react-components/react-avatar/src/contexts/AvatarGroupContext.types.ts new file mode 100644 index 00000000000000..e55c72213a1e53 --- /dev/null +++ b/packages/react-components/react-avatar/src/contexts/AvatarGroupContext.types.ts @@ -0,0 +1,5 @@ +import type { AvatarGroupProps } from '../AvatarGroup'; + +export type AvatarGroupContextValue = Pick & { + isOverflow?: boolean; +}; diff --git a/packages/react-components/react-avatar/src/contexts/index.ts b/packages/react-components/react-avatar/src/contexts/index.ts new file mode 100644 index 00000000000000..5316e465d33035 --- /dev/null +++ b/packages/react-components/react-avatar/src/contexts/index.ts @@ -0,0 +1 @@ +export * from './AvatarGroupContext'; diff --git a/packages/react-components/react-avatar/src/stories/AvatarGroupItemDefault.stories.tsx b/packages/react-components/react-avatar/src/stories/AvatarGroupItemDefault.stories.tsx index 7e3f4d5ed610fe..06409ddcbbab69 100644 --- a/packages/react-components/react-avatar/src/stories/AvatarGroupItemDefault.stories.tsx +++ b/packages/react-components/react-avatar/src/stories/AvatarGroupItemDefault.stories.tsx @@ -1,4 +1,4 @@ import * as React from 'react'; import { AvatarGroupItem, AvatarGroupItemProps } from '../index'; -export const Default = (props: Partial) => ; +export const Default = (props: Partial) => ;