This repository has been archived by the owner on Jan 23, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Includes base implementation of <Avatar> component which includes ability to modify intent, size, disable, and set selectable props to component. - It is a starting point to extend this implementation for more custom representations as StepAvatar and Validation which require customization of styles. - One more story is added to show <Avatar> component in different states, it is draft version to validate current implementation only.
- Loading branch information
Showing
6 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
138 changes: 138 additions & 0 deletions
138
packages/storybook-ui-components/stories/2-Avatar.stories.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import React from "react"; | ||
import { withKnobs, text, boolean, select } from "@storybook/addon-knobs"; | ||
import { action } from "@storybook/addon-actions"; | ||
import { Avatar } from "@cockroachlabs/ui-components"; | ||
|
||
export default { | ||
title: "Avatar", | ||
components: Avatar, | ||
decorators: [withKnobs], | ||
}; | ||
|
||
const baseConfig = { | ||
intent: "default", | ||
children: "jb", | ||
size: "l", | ||
disabled: false, | ||
selectable: false, | ||
onClick: () => console.log("click"), | ||
}; | ||
|
||
const intentConfigs = [ | ||
{ | ||
description: "Default", | ||
intent: "default", | ||
}, | ||
{ | ||
description: "Info", | ||
intent: "info", | ||
}, | ||
{ | ||
description: "Warning", | ||
intent: "warning", | ||
}, | ||
{ | ||
description: "Danger", | ||
intent: "danger", | ||
}, | ||
{ | ||
description: "Disabled", | ||
disabled: true, | ||
}, | ||
]; | ||
|
||
const withIntentNotSelectable = intentConfigs.map(c => ({ | ||
...baseConfig, | ||
...c, | ||
})); | ||
|
||
const withIntentSelectable = withIntentNotSelectable.map(c => ({ | ||
...baseConfig, | ||
...c, | ||
description: `${c.description} Selectable`, | ||
selectable: true, | ||
})); | ||
|
||
const withIntentSelectableSizeM = withIntentSelectable.map(c => ({ | ||
...c, | ||
size: "m", | ||
})); | ||
|
||
const withIntentSelectableSizeS = withIntentSelectable.map(c => ({ | ||
...c, | ||
size: "s", | ||
})); | ||
|
||
const Container = ({ children }) => ( | ||
<div | ||
style={{ | ||
display: "flex", | ||
flexDirection: "row", | ||
justifyContent: "space-around", | ||
marginBottom: "24px", | ||
}} | ||
> | ||
{children} | ||
</div> | ||
); | ||
|
||
const ItemWrapper = ({ children, title }) => ( | ||
<div | ||
style={{ display: "flex", flexDirection: "column", alignItems: "center" }} | ||
> | ||
<label style={{ marginBottom: "8px" }}>{title}</label> | ||
{children} | ||
</div> | ||
); | ||
|
||
export const example = () => ( | ||
<section> | ||
<h3>Size L</h3> | ||
<h4>Hover on avatar to see changes (for selectable items only)</h4> | ||
|
||
<Container> | ||
{withIntentNotSelectable.map(({ description, ...props }, idx) => ( | ||
<ItemWrapper title={description} key={idx}> | ||
<Avatar {...props} /> | ||
</ItemWrapper> | ||
))} | ||
</Container> | ||
|
||
<Container> | ||
{withIntentSelectable.map(({ description, ...props }, idx) => ( | ||
<ItemWrapper title={description} key={idx}> | ||
<Avatar {...props} /> | ||
</ItemWrapper> | ||
))} | ||
</Container> | ||
|
||
<h3>Size M</h3> | ||
<Container> | ||
{withIntentSelectableSizeM.map(({ description, ...props }, idx) => ( | ||
<ItemWrapper title={description} key={idx}> | ||
<Avatar {...props} /> | ||
</ItemWrapper> | ||
))} | ||
</Container> | ||
<h3>Size S</h3> | ||
<Container> | ||
{withIntentSelectableSizeS.map(({ description, ...props }, idx) => ( | ||
<ItemWrapper title={description} key={idx}> | ||
<Avatar {...props} /> | ||
</ItemWrapper> | ||
))} | ||
</Container> | ||
</section> | ||
); | ||
|
||
export const demo = () => ( | ||
<Avatar | ||
size={select("Size", ["xs", "s", "m", "l"], "l")} | ||
disabled={boolean("Disabled", false)} | ||
intent={select("Intent", ["default", "info", "warning", "danger"], "info")} | ||
onClick={action("button-click")} | ||
selectable={boolean("Selectable", true)} | ||
> | ||
{text("Text", "RL")} | ||
</Avatar> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
@import "../styles/tokens.scss"; | ||
|
||
// TODO (koorosh): Probably has to be extracted to tokens. | ||
@mixin selectable-border($color) { | ||
box-sizing: border-box; | ||
border: 2px solid $color; | ||
} | ||
|
||
.avatar { | ||
position: relative; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
flex-shrink: 0; | ||
font-family: Source Sans Pro, sans-serif; // TODO (koorosh): replace with fonts from tokes | ||
font-style: normal; | ||
font-weight: 600; | ||
line-height: 1; | ||
border-radius: 50%; | ||
overflow: hidden; | ||
user-select: none; | ||
} | ||
|
||
.intent-default { | ||
background: $crl-neutral-2; | ||
color: $crl-neutral-7; | ||
} | ||
|
||
.intent-info { | ||
background: $crl-blue-1; | ||
color: $crl-base-blue; | ||
} | ||
|
||
.intent-warning { | ||
background: $crl-yellow-1; | ||
color: $crl-yellow-4; | ||
} | ||
|
||
.intent-danger { | ||
background: $crl-red-1; | ||
color: $crl-red-4; | ||
} | ||
|
||
.size-l { | ||
width: 40px; | ||
height: 40px; | ||
font-size: 14px; | ||
letter-spacing: 0.1px; | ||
} | ||
|
||
.size-m { | ||
width: 32px; | ||
height: 32px; | ||
font-size: 12px; | ||
letter-spacing: 0.3px; | ||
} | ||
|
||
.size-s { | ||
width: 24px; | ||
height: 24px; | ||
font-size: 14px; | ||
letter-spacing: 0.1px; | ||
} | ||
|
||
.size-xs { | ||
width: 16px; | ||
height: 16px; | ||
font-size: 10px; | ||
letter-spacing: 0.1px; | ||
} | ||
|
||
.disabled { | ||
background: $crl-neutral-2; | ||
color: $crl-base-text--light; | ||
|
||
&:focus, &:hover { | ||
border: none; | ||
} | ||
} | ||
|
||
.selectable:not(.disabled) { | ||
&:focus, &:hover { | ||
&.intent-default { | ||
@include selectable-border($crl-neutral-7); | ||
} | ||
|
||
&.intent-info { | ||
@include selectable-border($crl-base-blue); | ||
} | ||
|
||
&.intent-warning { | ||
@include selectable-border($crl-yellow-4); | ||
} | ||
|
||
&.intent-danger { | ||
@include selectable-border($crl-red-4); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import React from "react"; | ||
import { shallow } from "enzyme"; | ||
|
||
import Avatar, { AvatarIntent, AvatarSize } from "./Avatar"; | ||
|
||
describe("Avatar", () => { | ||
describe("Default props", () => { | ||
it("assigns default style classes based on props", () => { | ||
const wrapper = shallow(<Avatar>CL</Avatar>); | ||
const className = wrapper.prop("className"); | ||
expect(className).toContain("intent-default"); | ||
expect(className).toContain("size-l"); | ||
expect(className).not.toContain("disabled"); | ||
expect(className).not.toContain("selectable"); | ||
}); | ||
}); | ||
|
||
describe("Handle onClick prop", () => { | ||
it("calls callback function on element click", () => { | ||
const onClickSpy = jasmine.createSpy(); | ||
const wrapper = shallow(<Avatar onClick={onClickSpy}>CL</Avatar>); | ||
wrapper.simulate("click"); | ||
expect(onClickSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it("does not call onClick function when disabled prop is True", () => { | ||
const onClickSpy = jasmine.createSpy(); | ||
const wrapper = shallow( | ||
<Avatar disabled={true} onClick={onClickSpy}> | ||
CL | ||
</Avatar>, | ||
); | ||
wrapper.simulate("click"); | ||
expect(onClickSpy).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe("Content rendering", () => { | ||
it("renders with string passed as children", () => { | ||
expect(shallow(<Avatar>foo bar</Avatar>).text()).toEqual("foo bar"); | ||
}); | ||
|
||
it("renders with empty content", () => { | ||
expect(shallow(<Avatar />).text()).toEqual(""); | ||
}); | ||
}); | ||
|
||
describe("Intent prop", () => { | ||
const intents: AvatarIntent[] = ["danger", "default", "info", "warning"]; | ||
|
||
intents.forEach(intent => { | ||
it("applies correct classNames depending on intent", () => { | ||
expect( | ||
shallow(<Avatar intent={intent} />) | ||
.find("div.avatar") | ||
.hasClass(`intent-${intent}`), | ||
).toBeTruthy(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("Size prop", () => { | ||
const sizes: AvatarSize[] = ["xs", "s", "m", "l"]; | ||
|
||
sizes.forEach(size => { | ||
it("applies correct classNames depending on size", () => { | ||
expect( | ||
shallow(<Avatar size={size} />) | ||
.find("div.avatar") | ||
.hasClass(`size-${size}`), | ||
).toBeTruthy(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("Disabled prop", () => { | ||
it("excludes intent className", () => { | ||
const wrapper = shallow(<Avatar intent="danger" disabled />); | ||
expect(wrapper.hasClass("disabled")).toBeTruthy(); | ||
expect(wrapper.hasClass("intent-danger")).toBeFalsy(); | ||
}); | ||
|
||
it("excludes selectable className", () => { | ||
const wrapper = shallow(<Avatar disabled selectable />); | ||
expect(wrapper.hasClass("disabled")).toBeTruthy(); | ||
expect(wrapper.hasClass("selectable")).toBeFalsy(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React, { useCallback, useMemo } from "react"; | ||
import classNames from "classnames/bind"; | ||
|
||
import styles from "./Avatar.module.scss"; | ||
import objectToClassnames from "../utils/objectToClassnames"; | ||
|
||
export interface AvatarProps { | ||
children?: string; | ||
size?: AvatarSize; | ||
intent?: AvatarIntent; | ||
disabled?: boolean; | ||
selectable?: boolean; | ||
onClick?: () => void; | ||
} | ||
|
||
export type AvatarSize = "xs" | "s" | "m" | "l"; | ||
export type AvatarIntent = "default" | "info" | "warning" | "danger"; | ||
|
||
const cx = classNames.bind(styles); | ||
|
||
const Avatar: React.FC<AvatarProps> = ({ | ||
children, | ||
intent = "default", | ||
size = "l", | ||
disabled = false, | ||
selectable = false, | ||
onClick, | ||
}) => { | ||
const classnames = useMemo( | ||
() => | ||
cx("avatar", objectToClassnames({ size }), { | ||
disabled, | ||
selectable: !disabled && selectable, | ||
[`intent-${intent}`]: !disabled, | ||
}), | ||
[intent, size, disabled, selectable], | ||
); | ||
|
||
const onClickHandler = useCallback(() => { | ||
if (!disabled && onClick) { | ||
onClick(); | ||
} | ||
}, [onClick, disabled]); | ||
|
||
return ( | ||
<div className={classnames} onClick={onClickHandler}> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
export default Avatar; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as Avatar } from "./Avatar"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { Badge } from "./Badge"; | ||
export { Avatar } from "./Avatar"; |