Skip to content

Commit

Permalink
feat: add avatar addons
Browse files Browse the repository at this point in the history
  • Loading branch information
anuraghazra committed Jul 10, 2024
1 parent 962f571 commit 41cf82d
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 58 deletions.
45 changes: 42 additions & 3 deletions packages/blade/src/components/Avatar/Avatar.web.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { AvatarProps } from './types';
import { StyledAvatar } from './StyledAvatar';
import { useAvatarGroupContext } from './AvatarGroupContext';
import { AvatarButton } from './AvatarButton';
import {
avatarToBottomAddonSize,
avatarToIndicatorSize,
avatarTopAddonOffsets,
} from './avatarTokens';
import { getStyledProps } from '~components/Box/styledProps';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { throwBladeError } from '~utils/logger';
import { UserIcon } from '~components/Icons';
import type { BladeElementRef } from '~utils/types';
import BaseBox from '~components/Box/BaseBox';
import { getComponentId } from '~utils/isValidAllowedChildren';

const getInitials = (name: string): string => {
// Combine first and last name initials
Expand All @@ -31,6 +37,9 @@ const _Avatar: React.ForwardRefRenderFunction<BladeElementRef, AvatarProps> = (
href,
target,
rel,
isSelected,
bottomAddon: BottomAddon,
topAddon: TopAddon,
// Image Props
src,
alt,
Expand All @@ -52,18 +61,25 @@ const _Avatar: React.ForwardRefRenderFunction<BladeElementRef, AvatarProps> = (
...styledProps
},
ref,
): ReactElement => {
) => {
if (__DEV__) {
if (src && !alt && !name) {
throwBladeError({
moduleName: 'Avatar',
message: '"alt" or "name" prop is required when the "src" prop is provided.',
});
}
if (TopAddon && getComponentId(TopAddon) !== 'Indicator') {
throwBladeError({
moduleName: 'Avatar',
message: 'TopAddon only accepts `Indicator` component.',
});
}
}

const groupProps = useAvatarGroupContext();
const avatarSize = groupProps?.size ?? size;
const isInteractive = Boolean(onClick || href);

const commonButtonProps = {
variant,
Expand All @@ -74,6 +90,7 @@ const _Avatar: React.ForwardRefRenderFunction<BladeElementRef, AvatarProps> = (
rel,
onBlur,
onFocus,
isSelected,
onClick,
onMouseLeave,
onMouseMove,
Expand Down Expand Up @@ -112,15 +129,37 @@ const _Avatar: React.ForwardRefRenderFunction<BladeElementRef, AvatarProps> = (
return <AvatarButton {...commonButtonProps} icon={icon ?? UserIcon} />;
};

const isSquare = variant === 'square';
return (
<StyledAvatar
{...metaAttribute({ name: MetaConstants.Avatar, testID })}
{...getStyledProps(styledProps)}
backgroundColor="surface.background.gray.intense"
variant={variant}
size={avatarSize}
isInteractive={isInteractive}
>
{getChildrenToRender()}
<BaseBox width="100%" height="100%" position="relative">
{TopAddon ? (
<BaseBox
position="absolute"
top={avatarTopAddonOffsets[variant][size].top}
right={avatarTopAddonOffsets[variant][size].right}
>
{React.cloneElement(TopAddon, { size: avatarToIndicatorSize[size], display: 'block' })}
</BaseBox>
) : null}
{getChildrenToRender()}
{BottomAddon ? (
<BaseBox
position="absolute"
bottom={isSquare ? '-10%' : '0%'}
right={isSquare ? '-10%' : '0%'}
>
<BottomAddon display="block" size={avatarToBottomAddonSize[size]} />
</BaseBox>
) : null}
</BaseBox>
</StyledAvatar>
);
};
Expand Down
11 changes: 9 additions & 2 deletions packages/blade/src/components/Avatar/AvatarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ const _AvatarButton: React.ForwardRefRenderFunction<BladeElementRef, AvatarButto
onPointerEnter,
onTouchStart,
onTouchEnd,
isSelected,
},
ref,
): React.ReactElement => {
const isLink = Boolean(href);
const isClickable = Boolean(onClick);
const isInteractive = isClickable || isLink;
const as = isInteractive ? (href ? 'a' : 'button') : 'div';

const defaultRel = target === '_blank' ? 'noreferrer noopener' : undefined;
const iconColor = getTextColorToken({
property: 'icon',
Expand All @@ -52,7 +57,9 @@ const _AvatarButton: React.ForwardRefRenderFunction<BladeElementRef, AvatarButto
return (
<StyledAvatarButton
ref={ref as never}
as={href ? 'a' : 'button'}
as={as as never}
isInteractive={isInteractive}
isSelected={isSelected}
size={size}
color={color}
href={href}
Expand All @@ -61,7 +68,7 @@ const _AvatarButton: React.ForwardRefRenderFunction<BladeElementRef, AvatarButto
rel={rel ?? defaultRel}
accessibilityProps={{
...makeAccessible({
role: isLink ? 'link' : 'button',
role: isInteractive ? (isLink ? 'link' : 'button') : 'presentation',
}),
}}
onBlur={onBlur}
Expand Down
41 changes: 24 additions & 17 deletions packages/blade/src/components/Avatar/StyledAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@ import { avatarSizeTokens, avatarBorderRadiusTokens } from './avatarTokens';
import BaseBox from '~components/Box/BaseBox';
import { makeBorderSize, makeSize } from '~utils';

const StyledAvatar = styled(BaseBox)<StyledAvatarProps>(({ theme, variant, size }) => {
return {
display: 'flex',
width: makeSize(avatarSizeTokens[size]),
height: makeSize(avatarSizeTokens[size]),
borderRadius: makeBorderSize(theme.border.radius[avatarBorderRadiusTokens[variant]]),
outline: `${makeBorderSize(theme.border.width.thinner)} solid ${
theme.colors.surface.border.gray.subtle
}`,

'&:hover': {
outline: `${makeBorderSize(theme.border.width.thick)} solid ${
theme.colors.surface.border.gray.muted
const StyledAvatar = styled(BaseBox)<StyledAvatarProps & { isInteractive?: boolean }>(
({ theme, variant, size, isInteractive }) => {
return {
display: 'flex',
width: makeSize(avatarSizeTokens[size]),
height: makeSize(avatarSizeTokens[size]),
borderRadius: makeBorderSize(theme.border.radius[avatarBorderRadiusTokens[variant]]),
outline: `${makeBorderSize(theme.border.width.thinner)} solid ${
theme.colors.surface.border.gray.subtle
}`,
borderColor: theme.colors.surface.border.gray.muted,
},
};
});

...(isInteractive
? {
'&:hover': {
outline: `${makeBorderSize(theme.border.width.thick)} solid ${
theme.colors.surface.border.gray.muted
}`,
borderColor: theme.colors.surface.border.gray.muted,
backgroundColor: theme.colors.surface.background.gray.moderate,
},
}
: {}),
};
},
);

export { StyledAvatar };
30 changes: 23 additions & 7 deletions packages/blade/src/components/Avatar/StyledAvatarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@ import { makeBorderSize, makeSize } from '~utils';
import getIn from '~utils/lodashButBetter/get';
import { getFocusRingStyles } from '~utils/getFocusRingStyles';

const StyledAvatarButton = styled.button<AvatarButtonProps>(
({ theme, size = 'medium', variant = 'circle', color = 'neutral' }) => {
const StyledAvatarButton = styled.button<AvatarButtonProps & { isInteractive?: boolean }>(
({
theme,
size = 'medium',
variant = 'circle',
color = 'neutral',
isSelected,
isInteractive,
}) => {
return {
display: 'block',
textAlign: 'center',
textDecoration: 'none',
cursor: 'pointer',
cursor: isInteractive ? 'default' : 'pointer',
minHeight: makeSize(avatarSizeTokens[size]),
height: makeSize(avatarSizeTokens[size]),
width: makeSize(avatarSizeTokens[size]),
border: 'none',
border: isSelected
? `${makeBorderSize(theme.border.width.thicker)} solid ${
theme.colors.surface.border.primary.normal
}`
: 'none',
borderRadius: makeBorderSize(theme.border.radius[avatarBorderRadiusTokens[variant]]),
backgroundColor: getIn(theme.colors, avatarColorTokens.background[color]),

Expand All @@ -26,9 +38,13 @@ const StyledAvatarButton = styled.button<AvatarButtonProps>(
objectFit: 'cover',
},

'&:focus-visible': {
...getFocusRingStyles({ theme }),
},
...(isInteractive
? {
'&:focus-visible': {
...getFocusRingStyles({ theme }),
},
}
: {}),
};
},
);
Expand Down
14 changes: 14 additions & 0 deletions packages/blade/src/components/Avatar/TrustedBadge.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { throwBladeError } from '~utils/logger';

import type { IconComponent } from '~components/Icons';

const TrustedBadge: IconComponent = () => {
throwBladeError({
message: 'Truste is not yet implemented for React Native',
moduleName: 'Truste',
});

return <></>;
};

export { TrustedBadge };
Loading

0 comments on commit 41cf82d

Please sign in to comment.