>(_Avatar);
+
+export default UNSTABLE_Avatar;
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/__tests__/UNSTABLE_Avatar.test.tsx b/packages/web-react/src/components/UNSTABLE_Avatar/__tests__/UNSTABLE_Avatar.test.tsx
new file mode 100644
index 0000000000..5414b22270
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/__tests__/UNSTABLE_Avatar.test.tsx
@@ -0,0 +1,84 @@
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest';
+import { sizeExtendedPropsTest } from '../../../../tests/providerTests/dictionaryPropsTest';
+import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
+import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
+import { Icon } from '../../Icon';
+import { UNSTABLE_Avatar } from '../UNSTABLE_Avatar';
+
+describe('UNSTABLE_Avatar', () => {
+ classNamePrefixProviderTest(UNSTABLE_Avatar, 'UNSTABLE_Avatar');
+
+ sizeExtendedPropsTest(UNSTABLE_Avatar);
+
+ stylePropsTest(UNSTABLE_Avatar);
+
+ restPropsTest(UNSTABLE_Avatar, 'div');
+
+ it('should have default classname', () => {
+ render(JB);
+
+ expect(screen.getByTitle('Jiří Bárta')).toHaveClass('UNSTABLE_Avatar');
+ });
+
+ it('should have default classname with elementType', () => {
+ render(
+
+ JB
+ ,
+ );
+
+ expect(screen.getByTitle('Jiří Bárta')).toHaveClass('UNSTABLE_Avatar');
+ expect(screen.getByTitle('Jiří Bárta')).toHaveAttribute('href', '#');
+ });
+
+ it('should have square classname with isSquare', () => {
+ render(
+
+ JB
+ ,
+ );
+
+ expect(screen.getByTitle('Jiří Bárta')).toHaveClass('UNSTABLE_Avatar');
+ expect(screen.getByTitle('Jiří Bárta')).toHaveClass('UNSTABLE_Avatar--square');
+ });
+
+ it('should have size classname', () => {
+ render(
+
+ JB
+ ,
+ );
+
+ expect(screen.getByTitle('Jiří Bárta')).toHaveClass('UNSTABLE_Avatar');
+ expect(screen.getByTitle('Jiří Bárta')).toHaveClass('UNSTABLE_Avatar--xsmall');
+ });
+
+ it('should render Icon', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTitle('Jiří Bárta').querySelector('svg')).toBeInTheDocument();
+ });
+
+ it('should render text children', () => {
+ render(JB);
+
+ expect(screen.getByTitle('Jiří Bárta')).toHaveTextContent('JB');
+ });
+
+ it('should render image', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('test-image')).toBeInTheDocument();
+ });
+});
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/__tests__/useAvatarStyleProps.test.ts b/packages/web-react/src/components/UNSTABLE_Avatar/__tests__/useAvatarStyleProps.test.ts
new file mode 100644
index 0000000000..1129a60a13
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/__tests__/useAvatarStyleProps.test.ts
@@ -0,0 +1,31 @@
+import { renderHook } from '@testing-library/react';
+import { SizesExtended } from '../../../constants/dictionaries';
+import { useAvatarStyleProps } from '../useAvatarStyleProps';
+
+describe('useAvatarStyleProps', () => {
+ const defaultProps = { size: SizesExtended.MEDIUM }; // default size passed to the hook from the component
+
+ it('should return defaults', () => {
+ const props = { ...defaultProps };
+ const { result } = renderHook(() => useAvatarStyleProps(props));
+
+ expect(result.current.classProps).toBe('UNSTABLE_Avatar UNSTABLE_Avatar--medium');
+ });
+
+ it('should return square avatar', () => {
+ const props = {
+ ...defaultProps,
+ isSquare: true,
+ };
+ const { result } = renderHook(() => useAvatarStyleProps(props));
+
+ expect(result.current.classProps).toBe('UNSTABLE_Avatar UNSTABLE_Avatar--medium UNSTABLE_Avatar--square');
+ });
+
+ it.each(Object.values(SizesExtended))('should return %s size avatar', (size) => {
+ const props = { size };
+ const { result } = renderHook(() => useAvatarStyleProps(props));
+
+ expect(result.current.classProps).toBe(`UNSTABLE_Avatar UNSTABLE_Avatar--${size}`);
+ });
+});
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/constants.ts b/packages/web-react/src/components/UNSTABLE_Avatar/constants.ts
new file mode 100644
index 0000000000..f68404af02
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/constants.ts
@@ -0,0 +1,24 @@
+import { SizesExtended } from '../../constants';
+
+export const DEMO_SIZES = [
+ {
+ size: SizesExtended.XSMALL,
+ boxSize: 16,
+ },
+ {
+ size: SizesExtended.SMALL,
+ boxSize: 20,
+ },
+ {
+ size: SizesExtended.MEDIUM,
+ boxSize: 24,
+ },
+ {
+ size: SizesExtended.LARGE,
+ boxSize: 28,
+ },
+ {
+ size: SizesExtended.XLARGE,
+ boxSize: 32,
+ },
+];
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarIcon.tsx b/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarIcon.tsx
new file mode 100644
index 0000000000..534a6a9c6c
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarIcon.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Icon } from '../../Icon';
+import { DEMO_SIZES } from '../constants';
+import { UNSTABLE_Avatar } from '../UNSTABLE_Avatar';
+
+const AvatarIcon = () => (
+ <>
+
+ {DEMO_SIZES.map(({ size, boxSize }) => (
+
+
+
+ ))}
+
+
+ {DEMO_SIZES.map(({ size, boxSize }) => (
+
+
+
+ ))}
+
+ >
+);
+
+export default AvatarIcon;
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarImage.tsx b/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarImage.tsx
new file mode 100644
index 0000000000..a94b979aee
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarImage.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { DEMO_SIZES } from '../constants';
+import UNSTABLE_Avatar from '../UNSTABLE_Avatar';
+
+const AvatarImage = () => (
+ <>
+
+ {DEMO_SIZES.map(({ size }) => (
+
+
+
+ ))}
+
+
+ {DEMO_SIZES.map(({ size }) => (
+
+
+
+ ))}
+
+ >
+);
+
+export default AvatarImage;
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarText.tsx b/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarText.tsx
new file mode 100644
index 0000000000..43f4e0859d
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/demo/AvatarText.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { DEMO_SIZES } from '../constants';
+import { UNSTABLE_Avatar } from '../UNSTABLE_Avatar';
+
+const AvatarText = () => (
+ <>
+
+ {DEMO_SIZES.map(({ size }) => (
+
+ JB
+
+ ))}
+
+
+ {DEMO_SIZES.map(({ size }) => (
+
+ JB
+
+ ))}
+
+ >
+);
+
+export default AvatarText;
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/demo/index.tsx b/packages/web-react/src/components/UNSTABLE_Avatar/demo/index.tsx
new file mode 100644
index 0000000000..f5d3c25775
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/demo/index.tsx
@@ -0,0 +1,28 @@
+// Because there is no `dist` directory during the CI run
+/* eslint-disable import/no-extraneous-dependencies, import/extensions, import/no-unresolved */
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment, import/extensions, import/no-unresolved
+// @ts-ignore: No declaration file -- @see https://jira.almacareer.tech/browse/DS-561
+import icons from '@lmc-eu/spirit-icons/icons';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import DocsSection from '../../../../docs/DocsSections';
+import { IconsProvider } from '../../../context';
+import AvatarIcon from './AvatarIcon';
+import AvatarImage from './AvatarImage';
+import AvatarText from './AvatarText';
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+);
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/index.html b/packages/web-react/src/components/UNSTABLE_Avatar/index.html
new file mode 100644
index 0000000000..23972ef557
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/index.html
@@ -0,0 +1 @@
+{{> web-react/demo}}
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/index.ts b/packages/web-react/src/components/UNSTABLE_Avatar/index.ts
new file mode 100644
index 0000000000..3be542d254
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/index.ts
@@ -0,0 +1,3 @@
+export * from './UNSTABLE_Avatar';
+export * from './useAvatarStyleProps';
+export { default as UNSTABLE_Avatar } from './UNSTABLE_Avatar';
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/stories/UNSTABLE_Avatar.stories.tsx b/packages/web-react/src/components/UNSTABLE_Avatar/stories/UNSTABLE_Avatar.stories.tsx
new file mode 100644
index 0000000000..4e819858c2
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/stories/UNSTABLE_Avatar.stories.tsx
@@ -0,0 +1,58 @@
+import { Markdown } from '@storybook/blocks';
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { Sizes, SizesExtended } from '../../../constants';
+import { Icon } from '../../Icon';
+import ReadMe from '../README.md';
+import { UNSTABLE_Avatar } from '..';
+
+const meta: Meta = {
+ title: 'Experimental/UNSTABLE_Avatar',
+ component: UNSTABLE_Avatar,
+ parameters: {
+ docs: {
+ page: () => {ReadMe},
+ },
+ },
+ argTypes: {
+ children: {
+ control: 'select',
+ options: ['icon', 'image', 'text'],
+ description: `This is the place for the content of the Avatar. In the real code
+ you can pass in any children you want. In this demo we have predefined options:
+ \`icon\`, \`image\` and \`text\`. Please note the predefined options
+ in this demo are not customizable.`,
+ mapping: {
+ icon: ,
+ image: ,
+ text: JB,
+ },
+ },
+ elementType: {
+ control: 'text',
+ table: {
+ defaultValue: { summary: 'div' },
+ },
+ },
+ isSquare: {
+ control: 'boolean',
+ },
+ size: {
+ control: 'select',
+ options: [...Object.values(SizesExtended)],
+ },
+ },
+ args: {
+ children: 'text',
+ elementType: 'div',
+ isSquare: false,
+ size: Sizes.MEDIUM,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Playground: Story = {
+ name: 'UNSTABLE_Avatar',
+};
diff --git a/packages/web-react/src/components/UNSTABLE_Avatar/useAvatarStyleProps.ts b/packages/web-react/src/components/UNSTABLE_Avatar/useAvatarStyleProps.ts
new file mode 100644
index 0000000000..f6a5a11226
--- /dev/null
+++ b/packages/web-react/src/components/UNSTABLE_Avatar/useAvatarStyleProps.ts
@@ -0,0 +1,33 @@
+import classNames from 'classnames';
+import { ElementType } from 'react';
+import { useClassNamePrefix } from '../../hooks';
+import { AvatarSize, SpiritAvatarProps } from '../../types';
+import { applySize, compose } from '../../utils';
+
+export interface AvatarStyles {
+ /** className props */
+ classProps: string;
+ /** props to be passed to the element */
+ props: SpiritAvatarProps;
+}
+
+const getAvatarSizeClassname = (className: string, size: AvatarSize): string =>
+ compose(applySize>(size))(className);
+
+export function useAvatarStyleProps(
+ props: SpiritAvatarProps,
+): AvatarStyles {
+ const { isSquare, size, ...restProps } = props;
+
+ const avatarClass = useClassNamePrefix('UNSTABLE_Avatar');
+ const avatarSquareClass = `${avatarClass}--square`;
+
+ const classProps = classNames(avatarClass, getAvatarSizeClassname(avatarClass, size as AvatarSize), {
+ [avatarSquareClass]: isSquare,
+ });
+
+ return {
+ classProps,
+ props: restProps,
+ };
+}
diff --git a/packages/web-react/src/components/index.ts b/packages/web-react/src/components/index.ts
index d4a800d73a..f62a29c8b7 100644
--- a/packages/web-react/src/components/index.ts
+++ b/packages/web-react/src/components/index.ts
@@ -33,5 +33,6 @@ export * from './TextField';
export * from './TextFieldBase';
export * from './Toast';
export * from './Tooltip';
+export * from './UNSTABLE_Avatar';
export * from './UNSTABLE_Divider';
export * from './VisuallyHidden';
diff --git a/packages/web-react/src/types/avatar.ts b/packages/web-react/src/types/avatar.ts
new file mode 100644
index 0000000000..39a03de10b
--- /dev/null
+++ b/packages/web-react/src/types/avatar.ts
@@ -0,0 +1,24 @@
+import { ElementType, JSXElementConstructor } from 'react';
+import { ChildrenProps, SizeExtendedDictionaryType, StyleProps, TransferProps } from './shared';
+
+export type AvatarSize = SizeExtendedDictionaryType | S;
+
+export interface AriaAvatarElementTypeProps {
+ /**
+ * The HTML element or React element used to render the Avatar, e.g. 'div', 'span'.
+ *
+ * @default 'div'
+ */
+ elementType?: E | JSXElementConstructor;
+}
+
+export interface AvatarProps extends ChildrenProps, StyleProps, TransferProps {}
+
+export interface SpiritAvatarProps
+ extends AriaAvatarElementTypeProps,
+ AvatarProps {
+ /** Whether the Avatar should be square. */
+ isSquare?: boolean;
+ /** Size of the Avatar */
+ size?: AvatarSize;
+}
diff --git a/packages/web-react/src/types/index.ts b/packages/web-react/src/types/index.ts
index 4e74a18dce..ba69f0b3d2 100644
--- a/packages/web-react/src/types/index.ts
+++ b/packages/web-react/src/types/index.ts
@@ -1,5 +1,6 @@
export * from './accordion';
export * from './alert';
+export * from './avatar';
export * from './breadcrumbs';
export * from './button';
export * from './checkbox';
diff --git a/packages/web/src/scss/components/UNSTABLE_Avatar/README.md b/packages/web/src/scss/components/UNSTABLE_Avatar/README.md
index e978b8d0c5..74ed9840a3 100644
--- a/packages/web/src/scss/components/UNSTABLE_Avatar/README.md
+++ b/packages/web/src/scss/components/UNSTABLE_Avatar/README.md
@@ -32,7 +32,8 @@ Add `UNSTABLE_Avatar--square` modifier to make the avatar a square.
## Sizes
-The Avatar component supports `xsmall`, `small`, `medium`, `large`, and `xlarge` sizes.
+The Avatar component is available in all [extended sizes][dictionary-size].
+Use the `UNSTABLE_Avatar--` modifier class to change the size of the Avatar component.
```html
@@ -99,3 +100,5 @@ take care of the text length and case. The rest is handled by the component.
ℹ️ Don't forget to add the `aria-label` attribute for accessible title, especially when
using an abbreviation. The `aria-hidden` attribute is set on the text span, because the `aria-label`
attribute is set on the container and the abbreviation is not useful for screen readers.
+
+[dictionary-size]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#size