Skip to content
Draft
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
13 changes: 13 additions & 0 deletions .changeset/famous-suns-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@salt-ds/core": minor
---

Added a `kind` prop to `Avatar` to distinguish usage by what the Avatar represents.

- `kind="person"` (default) renders a circular Avatar.
- `kind="entity"` renders a square Avatar.

```tsx
<Avatar name="John Doe" />
<Avatar kind="entity" name="JPMC" />
```
19 changes: 19 additions & 0 deletions packages/core/src/__tests__/__e2e__/avatar/Avatar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ describe("Given an Avatar", () => {
cy.findByTestId("UserGroupSolidIcon").should("exist");
});

it("should default to representing a person with a circular shape and person fallback icon", () => {
cy.mount(<Default />);
cy.findByTestId("UserSolidIcon").should("exist");
cy.get(".saltAvatar").should("not.have.class", "saltAvatar-entity");
});

it("should render a square shape and business fallback icon when representing a business", () => {
cy.mount(<Default kind="entity" />);
cy.findByTestId("BankIcon").should("exist");
cy.get(".saltAvatar").should("have.class", "saltAvatar-entity");
});

it("should support a custom fallback icon when representing a business", () => {
const fallbackIcon = <UserGroupSolidIcon />;
cy.mount(<Default kind="entity" fallbackIcon={fallbackIcon} />);
cy.findByTestId("UserGroupSolidIcon").should("exist");
cy.findByTestId("BankIcon").should("not.exist");
});

it("should preserve native button semantics when rendered as a button", () => {
cy.mount(
<Default name="Juanito Jones" render={<button type="button" />} />,
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/avatar/Avatar.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
user-select: none;
}

.saltAvatar-entity {
border-radius: var(--saltAvatar-entity-borderRadius, var(--salt-palette-corner-weak, 0));
}

.saltAvatar-accent {
--avatar-background: var(--salt-sentiment-accent-background);
}
Expand Down
19 changes: 16 additions & 3 deletions packages/core/src/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,19 @@ export interface AvatarProps extends HTMLAttributes<HTMLDivElement> {
*/
size?: number;
/**
* Icon to be used as a default item. Defaults to `UserIcon`.
* Icon to be used as a default item. Defaults to the semantic `UserIcon` when
* `kind` is `"person"`, and the semantic `EntityIcon` when `kind`
* is `"entity"`.
*/
fallbackIcon?: ReactNode;
/**
* What the Avatar depicts, which determines its default shape and fallback icon:
* - `"person"` renders a circular Avatar (default).
* - `"entity"` renders a square Avatar.
*
* @default "person"
*/
kind?: "person" | "entity";
/**
* Changes Avatar's color.
*/
Expand Down Expand Up @@ -95,6 +105,7 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
color = "accent",
name,
nameToInitials = defaultNameToInitials,
kind = "person",
src,
size = DEFAULT_AVATAR_SIZE,
style: styleProp,
Expand All @@ -105,11 +116,12 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
ref,
) {
const targetWindow = useWindow();
const { UserIcon } = useIcon();
const { UserIcon, EntityIcon } = useIcon();

const DefaultFallbackIcon = kind === "entity" ? EntityIcon : UserIcon;
const fallbackIcon =
fallbackIconProp === undefined ? (
<UserIcon aria-hidden />
<DefaultFallbackIcon aria-hidden />
) : (
fallbackIconProp
);
Expand Down Expand Up @@ -156,6 +168,7 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
className={clsx(
withBaseName(),
withBaseName(color),
withBaseName(kind),
{
[withBaseName("withImage")]: hasImgNotFailing,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BankIcon,
CalendarIcon,
ChevronDownIcon,
ChevronLeftIcon,
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface SemanticIconMap {
CollapseRightIcon: ElementType;
CompletedIcon: ElementType;
DecreaseIcon: ElementType;
EntityIcon: ElementType;
ErrorIcon: ElementType;
ErrorStatusAdornment: ElementType;
ExpandGroupIcon: ElementType;
Expand Down Expand Up @@ -91,6 +93,7 @@ const defaultIconMap: SemanticIconMap = {
CollapseRightIcon: DoubleChevronRightIcon,
CompletedIcon: SuccessCircleSolidIcon,
DecreaseIcon: TriangleDownIcon,
EntityIcon: BankIcon,
ErrorIcon: ErrorSolidIcon,
ErrorStatusAdornment: ErrorAdornmentIcon,
ExpandGroupIcon: ChevronRightIcon,
Expand Down
21 changes: 20 additions & 1 deletion packages/core/stories/avatar/avatar.qa.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default {

export const AllVariantsGrid: StoryFn<QAContainerProps> = (props) => (
<QAContainer height={500} width={1000} {...props}>
<Avatar size={1} name="Alex Brailescu" src={persona1 as string} />
<Avatar size={1} name="Alex Brailescu" src={persona1} />
<Avatar size={2} src="bad_url" name="Peter Piper" />
<Avatar size={3} src="bad_url" />
<Avatar size={3} fallbackIcon={<SaltShakerIcon />} />
Expand Down Expand Up @@ -43,3 +43,22 @@ AllVariantsGrid.parameters = {
disableSnapshot: false,
},
};

export const EntityGrid: StoryFn<QAContainerProps> = (props) => (
<QAContainer height={200} width={1000} {...props}>
<Avatar kind="entity" size={1} name="Alex Brailescu" src={persona1} />
<Avatar kind="entity" size={2} src="bad_url" name="Peter Piper" />
<Avatar kind="entity" size={3} src="bad_url" />
<Avatar kind="entity" size={3} />
<Avatar kind="entity" size={3} fallbackIcon={<SaltShakerIcon />} />
<Avatar kind="entity" name="Peter Piper" color="category-1" />
<Avatar kind="entity" name="Peter Piper" color="category-2" />
<Avatar kind="entity" name="Peter Piper" color="category-3" />
</QAContainer>
);

EntityGrid.parameters = {
chromatic: {
disableSnapshot: false,
},
};
19 changes: 17 additions & 2 deletions packages/core/stories/avatar/avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
Avatar,
FlowLayout,
Label,
StackLayout,
Text,
useAvatarImage,
} from "@salt-ds/core";
import { UserGroupSolidIcon } from "@salt-ds/icons";
Expand All @@ -26,13 +26,28 @@ const CustomAvatarButton = (props: ComponentProps<"button">) => (

export const Default = Template.bind({});

export const Kind: StoryFn<typeof Avatar> = (args) => {
return (
<FlowLayout gap={3}>
<StackLayout align="center" gap={1}>
<Avatar {...args} kind="person" name="John Doe" />
<Text>Person</Text>
</StackLayout>
<StackLayout align="center" gap={1}>
<Avatar {...args} kind="entity" name="JPMC" />
<Text>Entity</Text>
</StackLayout>
</FlowLayout>
);
};

export const Sizes: StoryFn<typeof Avatar> = (args) => {
return (
<FlowLayout gap={7} align="end">
{sizes.map((size) => (
<StackLayout key={size} align="center">
<Avatar {...args} key={size} size={size} />
<Label>size: {size}</Label>
<Text>size: {size}</Text>
</StackLayout>
))}
</FlowLayout>
Expand Down
26 changes: 20 additions & 6 deletions site/docs/components/avatar/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ data:
$ref: ./#/data
---

## Avatar kinds

Choose the avatar kind that best matches the identity you’re communicating. Avatars can represent an individual person or a named non-person entity such as a team, organization, or business. Person and Entity kinds use different shapes to help users quickly understand what the avatar represents.

### Person (circle)

Use kind=”person” when the identity is a single individual (e.g., user profile, author, assignee, commenter). We recommend using an image when available; otherwise fall back to initials or a person icon. If you need to represent multiple people together, use Avatar Group.

<LivePreview componentName="avatar" exampleName="Person" />

### Entity (square)

Use kind="entity" when the identity is a named non-person entity such as a team, business, organization, company, or account. Use an Entity avatar when representing the team as a single named identity; use Avatar Group when representing the team’s members. Use a logo/image when available; otherwise use entity initials/code or an appropriate icon (e.g., office/building).

<LivePreview componentName="avatar" exampleName="Entity" />

## Image

You can pass an image as the avatar image using the `src` prop.
Expand Down Expand Up @@ -47,18 +63,16 @@ Common interactive use cases include:

<LivePreview componentName="avatar" exampleName="Interactive" />

## Name to color

You can deterministically derive an avatar's `color` from its `name` so that the same person is always represented by the same [categorical color](/salt/foundations/color#categorical). In the example below, a simple hash of the name is used to pick one of the 20 categorical colors.

<LivePreview componentName="avatar" exampleName="NameToColor" />

## Sizes

You can use the `size` prop to modify the avatar size. Each avatar variant has a default size across all four densities, equal to the [size foundation](/salt/foundations/size) `size-base`: 20px (HD), 28px (MD), 36px (LD), and 44px (TD). The size property acts as a multiplier of the base size.

<LivePreview componentName="avatar" exampleName="Sizes" />

## Shape

Avatar is round by default. Setting `shape="square"` can help distinguish entities such as organizations, products or teams from individuals, but round remains a valid choice for these cases.

## Custom fallback icon

`Avatar` renders with an icon defined by `UserSolidIcon`, but you can pass an alternative icon as `fallbackIcon`.
Expand Down
Binary file added site/public/img/examples/entityAvatar.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions site/src/examples/avatar/Entity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Avatar, FlexLayout } from "@salt-ds/core";
import type { ReactElement } from "react";

export const Entity = (): ReactElement => (
<FlexLayout>
<Avatar
name="Alex Brailescu"
src="/img/examples/entityAvatar.jpg"
kind="entity"
/>
<Avatar
name="012"
kind="entity"
nameToInitials={(name = "") => name.slice(0, 3)}
/>
<Avatar kind="entity" />
</FlexLayout>
);
10 changes: 10 additions & 0 deletions site/src/examples/avatar/Person.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Avatar, FlexLayout } from "@salt-ds/core";
import type { ReactElement } from "react";

export const Person = (): ReactElement => (
<FlexLayout>
<Avatar name="Alex Brailescu" src={"/img/examples/avatar.png"} />
<Avatar name="Alex Brailescu" />
<Avatar />
</FlexLayout>
);
2 changes: 2 additions & 0 deletions site/src/examples/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export * from "./Categories";
export * from "./CustomFallbackIcon";
export * from "./Entity";
export * from "./Image";
export * from "./Initials";
export * from "./Interactive";
export * from "./NameToColor";
export * from "./Person";
export * from "./Sizes";
Loading