Skip to content

Commit

Permalink
feat: init Avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
snitin315 committed May 12, 2024
1 parent 61e3fb9 commit 91b034b
Show file tree
Hide file tree
Showing 17 changed files with 1,112 additions and 20 deletions.
14 changes: 14 additions & 0 deletions packages/blade/src/components/Avatar/Avatar.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { AvatarProps } from './types';
import { throwBladeError } from '~utils/logger';

const Avatar = (_props: AvatarProps): React.ReactElement => {
throwBladeError({
message: 'Avatar is not yet implemented for React Native',
moduleName: 'Avatar',
});

// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
};

export { Avatar };
192 changes: 192 additions & 0 deletions packages/blade/src/components/Avatar/Avatar.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import React from 'react';
import type { AvatarProps } from './types';
import { StyledAvatar } from './StyledAvatar';
import { DefaultAvatarIcon } from './DefaultAvatarIcon';

import { useAvatarGroupContext } from './AvatarGroupContext';
import { getStyledProps } from '~components/Box/styledProps';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { useDropdown } from '~components/Dropdown/useDropdown';
import { getActionListContainerRole } from '~components/ActionList/getA11yRoles';
import BaseButton from '~components/Button/BaseButton/BaseButton';
import { throwBladeError } from '~utils/logger';

const getInitials = (name: string): string => {
// Combine first and last name initials
const names = name.split(' ');
if (names.length === 1) {
return name.substring(0, 2);
}
return names[0].substring(0, 1) + names[names.length - 1].substring(0, 1);
};

const _Avatar = ({
name = 'Nitin Kumar',
color = 'neutral',
size = 'xsmall',
variant = 'circle',
icon,
href,
target,
rel,
// Image Props
src,
alt,
srcSet,
crossOrigin,
referrerPolicy,
testID,
...styledProps
}: AvatarProps): React.ReactElement => {
if (src && !alt && !name) {
throwBladeError({
moduleName: 'Avatar',
message: '"alt" or "name" prop is required when the "src" prop is provided.',
});
}
const accessibilityLabel = alt ?? name;
const groupProps = useAvatarGroupContext();
console.log('🚀 ~ groupProps:', groupProps);
const avatarSize = groupProps?.size ?? size;

const {
onTriggerClick,
onTriggerKeydown,
dropdownBaseId,
isOpen,
activeIndex,
hasFooterAction,
triggererRef,
dropdownTriggerer,
} = useDropdown();
const isInsideDropdown = dropdownTriggerer === 'Avatar';

const dropDownTriggerProps = isInsideDropdown
? {
ref: triggererRef,
onClick: onTriggerClick,
accessibilityProps: {
label: accessibilityLabel,
hasPopup: getActionListContainerRole(hasFooterAction, 'DropdownButton'),
expanded: isOpen,
controls: `${dropdownBaseId}-actionlist`,
activeDescendant: activeIndex >= 0 ? `${dropdownBaseId}-${activeIndex}` : undefined,
},
}
: {};

const getChildrenToRender = (): React.ReactElement => {
if (src) {
return (
<BaseButton
variant="secondary"
color={color}
size="xsmall"
isPressAnimationDisabled={true}
imgProps={{
src,
alt: alt ?? name,
srcSet,
crossOrigin,
referrerPolicy,
}}
href={href}
target={target}
rel={rel}
{...dropDownTriggerProps}
onKeyDown={(e) => {
if (isInsideDropdown) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
onTriggerKeydown?.({ event: e as any });
}
}}
/>
);
}

if (name && !src) {
return (
<BaseButton
variant="secondary"
color={color}
size="xsmall"
iconSize={avatarSize}
isPressAnimationDisabled={true}
href={href}
target={target}
rel={rel}
onKeyDown={(e) => {
if (isInsideDropdown) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
onTriggerKeydown?.({ event: e as any });
}
}}
{...dropDownTriggerProps}
>
{getInitials(name)}
</BaseButton>
);
}

return (
<BaseButton
variant="secondary"
color={color}
size="xsmall"
iconSize={avatarSize}
icon={icon ?? DefaultAvatarIcon}
isPressAnimationDisabled={true}
href={href}
target={target}
rel={rel}
onKeyDown={(e) => {
if (isInsideDropdown) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
onTriggerKeydown?.({ event: e as any });
}
}}
{...dropDownTriggerProps}
/>
);
};

return (
<StyledAvatar
{...metaAttribute({ name: MetaConstants.Avatar, testID })}
{...getStyledProps(styledProps)}
backgroundColor="surface.background.gray.intense"
variant={variant}
color={color}
size={avatarSize}
>
{getChildrenToRender()}
</StyledAvatar>
);
};

/**
* ### Avatar Component
*
* The Avatar component is used to group related buttons together.
*
* ---
*
* #### Usage
*
* ```jsx
<Avatar name="Nitin Kumar" src="https://avatars.githubusercontent.com/u/46647141?v=4" />
* ```
*
* ---
*
* Checkout {@link https://blade.razorpay.com/?path=/docs/components-avatar Avatar Documentation}
*
*/
const Avatar = assignWithoutSideEffects(_Avatar, {
displayName: 'Avatar',
componentId: 'Avatar',
});

export { Avatar };
export type { AvatarProps };
110 changes: 110 additions & 0 deletions packages/blade/src/components/Avatar/AvatarGroup.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import type { AvatarGroupProps, AvatarGroupContextType } from './types';
import { StyledAvatarGroup } from './StyledAvatarGroup';
import { StyledAvatar } from './StyledAvatar';
import { AvatarGroupProvider } from './AvatarGroupContext';
import { getStyledProps } from '~components/Box/styledProps';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { throwBladeError } from '~utils/logger';
import { isValidAllowedChildren } from '~utils/isValidAllowedChildren';
import BaseButton from '~components/Button/BaseButton/BaseButton';

const _AvatarGroup = ({
children,
size = 'xsmall',
maxCount = 2,
testID,
...styledProps
}: AvatarGroupProps): React.ReactElement => {
const contextValue: AvatarGroupContextType = {
size,
};

return (
<AvatarGroupProvider value={contextValue}>
<StyledAvatarGroup
{...metaAttribute({ name: MetaConstants.AvatarGroup, testID })}
{...getStyledProps(styledProps)}
role="group"
size={size}
>
{React.Children.map(children, (child, index) => {
if (__DEV__) {
// throw error if child is not an Avatar
if (!isValidAllowedChildren(child, 'Avatar')) {
throwBladeError({
moduleName: 'AvatarGroup',
message: `Only "Avatar" component is allowed as a children.`,
});
}
}

if (maxCount && maxCount <= React.Children.count(children)) {
if (index === maxCount) {
return (
<StyledAvatar
{...metaAttribute({ name: MetaConstants.Avatar, testID })}
backgroundColor="surface.background.gray.intense"
size={size}
color="neutral"
variant="circle"
>
<BaseButton
variant="secondary"
color="neutral"
size="xsmall"
iconSize={size}
isPressAnimationDisabled={true}
>
{`+${String(React.Children.count(children) - maxCount)}`}
</BaseButton>
</StyledAvatar>
);
}

if (index > maxCount) {
return null;
}
}

return child;
})}
</StyledAvatarGroup>
</AvatarGroupProvider>
);
};

/**
* ### AvatarGroup Component
*
* The Avatar component is used to group related buttons together.
*
* ---
*
* #### Usage
*
* ```jsx
const App = () => {
return (
<Avatar>
<Button icon={RefreshIcon}>Sync</Button>
<Button icon={ShareIcon}>Share</Button>
<Button icon={DownloadIcon}>Download</Button>
</Avatar>
);
}
* ```
*
* ---
*
* Checkout {@link https://blade.razorpay.com/?path=/docs/components-buttongroup FileUpload Documentation}
*
*/
const AvatarGroup = assignWithoutSideEffects(_AvatarGroup, {
displayName: 'AvatarGroup',
componentId: 'AvatarGroup',
});

export { AvatarGroup };
export type { AvatarGroupProps };
12 changes: 12 additions & 0 deletions packages/blade/src/components/Avatar/AvatarGroupContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import type { AvatarGroupContextType } from './types';

const AvatarGroupContext = React.createContext<AvatarGroupContextType>({});
const AvatarGroupProvider = AvatarGroupContext.Provider;

const useAvatarGroupContext = (): AvatarGroupContextType => {
const context = React.useContext(AvatarGroupContext);
return context;
};

export { useAvatarGroupContext, AvatarGroupProvider };
Loading

0 comments on commit 91b034b

Please sign in to comment.