Skip to content

Commit

Permalink
feat(AvatarGroup): add avatar group component
Browse files Browse the repository at this point in the history
  • Loading branch information
plagoa authored and HQFOX committed Dec 20, 2023
1 parent bd1b1cd commit 0cc6f76
Show file tree
Hide file tree
Showing 9 changed files with 522 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/core/src/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const flexDecorator: DecoratorFn = (Story) => (
);

const meta: Meta<typeof HvAvatar> = {
title: "Components/Avatar",
title: "Components/Avatar/Avatar",
component: HvAvatar,
decorators: [flexDecorator],
parameters: {
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/AvatarGroup/AvatarGroup.styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { theme } from "@hitachivantara/uikit-styles";

import { createClasses } from "../utils/classes";

import { avatarClasses } from "../Avatar/Avatar";

export const { staticClasses, useClasses } = createClasses("HvAvatarGroup", {
root: {
display: "flex",
[`& .${avatarClasses.root}`]: {
border: `2px solid ${theme.colors.atmo2}`,
boxSizing: "content-box",
},
},
row: {
flexDirection: "row",
},
column: {
flexDirection: "column",
},
});
70 changes: 70 additions & 0 deletions packages/core/src/AvatarGroup/AvatarGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { render, screen } from "@testing-library/react";

import { LogIn } from "@hitachivantara/uikit-react-icons";

import { HvAvatarGroup } from "./AvatarGroup";

import { HvAvatar } from "../Avatar/Avatar";

describe("HvAvatarGroup", () => {
it("renders without crashing", () => {
render(<HvAvatarGroup />);
});

it("renders the correct number of avatars", () => {
render(
<HvAvatarGroup>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
</HvAvatarGroup>
);

const renderedAvatars = screen.getAllByRole("img");
expect(renderedAvatars).toHaveLength(3);
});

it("renders the correct number of avatars when `maxVisible` is set", () => {
render(
<HvAvatarGroup maxVisible={1}>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
</HvAvatarGroup>
);

const renderedAvatars = screen.getAllByRole("img");
expect(renderedAvatars).toHaveLength(1);
});

it("renders overflow avatar when number of avatars exceeds maxVisible", () => {
render(
<HvAvatarGroup maxVisible={2}>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
<HvAvatar>
<LogIn role="img" aria-label="login" />
</HvAvatar>
</HvAvatarGroup>
);

const overflowAvatar = screen.getByText("+1");
expect(overflowAvatar).toBeInTheDocument();
});
});
147 changes: 147 additions & 0 deletions packages/core/src/AvatarGroup/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Children, cloneElement, forwardRef } from "react";

import { css } from "@emotion/css";

import { HvSize, theme } from "@hitachivantara/uikit-styles";

import { useDefaultProps } from "../hooks/useDefaultProps";

import { HvBaseProps } from "../types/generic";

import { ExtractNames } from "../utils/classes";

import { staticClasses, useClasses } from "./AvatarGroup.styles";

import { HvAvatar } from "../Avatar/Avatar";

export { staticClasses as avatarGroupClasses };

export type HvAvatarGroupClasses = ExtractNames<typeof useClasses>;

export interface HvAvatarGroupProps extends HvBaseProps {
/** A Jss Object used to override or extend the styles applied to the component. */
classes?: HvAvatarGroupClasses;
/** The avatar size. */
size?: HvSize;
/** The spacing between avatars. */
spacing?: "compact" | "loose";
/** The direction of the group. */
direction?: "row" | "column";
/** Whether the avatars display behind the previous avatar or on top. */
toBack?: boolean;
/**
* The maximum number of visible avatars. If there are more avatars then the value of this property, an added avatar will
* be added to the end of the list, indicating the number of hidden avatars.
*/
maxVisible?: number;
/**
* What to show as an overflow representation.
* If `undefined` a default `HvAvatar` will be displayed along with a HvTooltip with the count of overflowing items.
* */
overflowComponent?: (overflowCount: number) => React.ReactNode;
}

const getSpacingValue = (
spacing: HvAvatarGroupProps["spacing"],
size: HvAvatarGroupProps["size"]
) => {
switch (size) {
case "xs":
return spacing === "compact" ? 24 : 16;
case "sm":
return spacing === "compact" ? 30 : 18;
case "md":
return spacing === "compact" ? 36 : 20;
case "lg":
return spacing === "compact" ? 44 : 24;
case "xl":
return spacing === "compact" ? 72 : 34;
default:
return spacing === "compact" ? 30 : 18;
}
};

/**
* The AvatarGroup component is used to group multiple avatars.
*/
export const HvAvatarGroup = forwardRef<HTMLDivElement, HvAvatarGroupProps>(
(props, ref) => {
const {
className,
classes: classesProp,
children,
size = "sm",
spacing = "loose",
direction = "row",
toBack = true,
maxVisible = 3,
overflowComponent,
...others
} = useDefaultProps("HvAvatarGroup", props);
const { classes, cx } = useClasses(classesProp);

const spacingValue = getSpacingValue(spacing, size);

const totalChildren = Children.count(children);
const zIndexMultiplier = toBack ? -1 : 1;
const willOverflow = totalChildren > maxVisible;

return (
<div
className={cx(classes.root, classes[direction], className)}
ref={ref}
{...others}
>
{Children.map(children, (child: any, index: number) => {
if (index < maxVisible) {
return cloneElement(child, {
style: {
zIndex: 100 + index * zIndexMultiplier,
},
classes: {
container: css({
marginLeft:
direction === "row" ? (index !== 0 ? -spacingValue : 0) : 0,
marginTop:
direction === "column"
? index !== 0
? -spacingValue
: 0
: 0,
}),
},
size,
});
}
})}
{willOverflow && (
<div
style={{
marginLeft: direction === "row" ? -spacingValue : 0,
marginTop: direction === "column" ? -spacingValue : 0,
zIndex: 100 + maxVisible * zIndexMultiplier,
}}
>
{overflowComponent ? (
overflowComponent(totalChildren - maxVisible)
) : (
<HvAvatar
size={size}
backgroundColor={theme.colors.atmo4}
classes={{
avatar: css({
[`&.HvAvatar-${size}`]: {
fontSize: "unset",
},
}),
}}
>
+{totalChildren - maxVisible}
</HvAvatar>
)}
</div>
)}
</div>
);
}
);
1 change: 1 addition & 0 deletions packages/core/src/AvatarGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./AvatarGroup";

0 comments on commit 0cc6f76

Please sign in to comment.