Skip to content

Commit

Permalink
Improvements to the Avatar component (#57)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
  • Loading branch information
germain-gg and t3chguy authored Aug 10, 2023
1 parent 010fc3e commit 0b231c1
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 36 deletions.
14 changes: 14 additions & 0 deletions src/components/Avatar/Avatar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ limitations under the License.
user-select: none;
}

button.avatar {
/**
* The avatar can be a button element, we need to reset its style
*/
padding: 0;
border: 0;
appearance: none;
cursor: pointer;
}

button.avatar:disabled {
cursor: not-allowed;
}

.avatar,
.image {
aspect-ratio: 1 / 1;
Expand Down
34 changes: 34 additions & 0 deletions src/components/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ Square.args = {
type: "square",
};

export const Button = Template.bind({});
Button.args = {
type: "round",
as: "button",
onClick: () => console.log("clicked!"),
};

export const NoImageFallback = Template.bind({});
NoImageFallback.args = {
src: "",
Expand All @@ -56,3 +63,30 @@ LargeNoImageFallback.args = {
src: "",
size: "128px",
};

const ImageLessCollection: StoryFn<typeof AvatarComponent> = (args) => (
<>
<AvatarComponent {...args} id="1" />
&nbsp;
<AvatarComponent {...args} id="2" />
&nbsp;
<AvatarComponent {...args} id="3" />
&nbsp;
<AvatarComponent {...args} id="4" />
&nbsp;
<AvatarComponent {...args} id="5" />
&nbsp;
<AvatarComponent {...args} id="6" />
&nbsp;
<AvatarComponent {...args} id="7" />
&nbsp;
<AvatarComponent {...args} id="8" />
&nbsp;
</>
);

export const AllAvatars = ImageLessCollection.bind({});
AllAvatars.args = {
src: "",
size: "36px",
};
125 changes: 89 additions & 36 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,67 @@ import { SuspenseImg } from "../../utils/SuspenseImg";
import styles from "./Avatar.module.css";
import { useIdColorHash } from "./useIdColorHash";

type AvatarProps = JSX.IntrinsicElements["span"] & {
type AvatarProps = (
| JSX.IntrinsicElements["button"]
| JSX.IntrinsicElements["span"]
) & {
/**
* The avatar image URL, if any.
*/
src?: React.ComponentProps<typeof SuspenseImg>["src"];
/**
* The Matrix ID, Room ID, or Alias to generate the color when no image source
* is provided. Also used as a fallback when name is empty.
*/
id: string;
/**
* The name used for the initial letter displayed when no image source is provided.
*/
name: string;
/**
* Defines the avatar type, typically round, square is usually for spaces.
* @default "round"
*/
type?: "square" | "round";
/**
* The avatar size in CSS units, e.g. `"24px"`.
*/
size?: CSSStyleDeclaration["height"];
/**
* On click handler, will turn the avatar into a button element.
*/
onClick?: (e: React.MouseEvent) => void;
/**
* Key down handler, will turn the avatar into a button element.
*/
onKeyDown?: (e: React.KeyboardEvent) => void;
/**
* Key up handler, will turn the avatar into a button element.
*/
onKeyUp?: (e: React.KeyboardEvent) => void;
/**
* Callback when the image has failed to load.
*/
onError?: React.ComponentProps<typeof SuspenseImg>["onError"];
};

export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(function Avatar(
/**
* Some props warrant that the avatar become a button for accessibility purposes
* @param props Avatar props
* @returns whether the avatar should be a button or not
*/
function shouldBeAButton(props: Partial<AvatarProps>): boolean {
return !!(props.onClick || props.onKeyDown || props.onKeyUp);
}

/**
* Avatar component that will fallback to an initial letter over a coloured
* background if no source is provided or if the source has failed to load.
*/
export const Avatar = forwardRef<
HTMLSpanElement | HTMLButtonElement,
AvatarProps
>(function Avatar(
{
src,
id,
Expand All @@ -47,39 +98,41 @@ export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(function Avatar(
const hash = useIdColorHash(id);
const fallbackInitial = <>{getInitialLetter(name)}</>;

return (
<span
ref={ref}
role="img"
title={id}
{...props}
aria-label=""
data-type={type}
data-color={hash}
className={classnames(styles.avatar, className)}
style={
{
...style,
"--cpd-avatar-size": size,
} as React.CSSProperties
}
>
{!src ? (
fallbackInitial
) : (
<Suspense fallback={fallbackInitial}>
<SuspenseImg
src={src}
className={classnames(styles.image)}
data-type={type}
style={style}
width={size}
height={size}
title={id}
onError={onError}
/>
</Suspense>
)}
</span>
return React.createElement(
shouldBeAButton(props) ? "button" : "span",
{
ref,
role: "img",
title: id,
"aria-label": "",
...props,
"data-type": type,
"data-color": hash,
className: classnames(styles.avatar, className),
style: {
...style,
"--cpd-avatar-size": size,
} as React.CSSProperties,
},
[
<>
{!src ? (
fallbackInitial
) : (
<Suspense fallback={fallbackInitial}>
<SuspenseImg
src={src}
className={classnames(styles.image)}
data-type={type}
style={style}
width={size}
height={size}
title={id}
onError={onError}
/>
</Suspense>
)}
</>,
],
);
});
1 change: 1 addition & 0 deletions src/utils/SuspenseImg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const SuspenseImg: React.FC<SuspenseImgProps> = ({ src, ...props }) => {
imgCache.read(src);
return (
<img
loading="lazy"
alt=""
src={src}
crossOrigin="anonymous"
Expand Down

0 comments on commit 0b231c1

Please sign in to comment.