feat(ui): migrate Avatar from Base UI with updated size scale
Removed Base UI dependency from Avatar component and reimplemented
with native HTML elements. Updated size scale with new x-small and
x-large options.
Breaking changes:
- Base UI-specific props (render, etc.) are no longer supported
- Component now uses native div/img elements instead of Base UI primitives
- Size scale updated: large changed from 3rem to 2.5rem
- Added x-small (1.25rem) and x-large (3rem) sizes
Migration:
- <Avatar src="..." name="..." render={...} />
+ <Avatar src="..." name="..." />
- <Avatar size="large" />
+ <Avatar size="x-large" />
New features:
- Added purpose prop with 'informative' (default) and 'decoration' options
- Informative avatars announce name to screen readers
- Decorative avatars hidden from screen readers (use when name appears adjacent)
- Five size options: x-small, small, medium, large, x-large
Documentation updates:
- Updated size examples to show all five sizes
- Added Purpose story and documentation
- Updated prop definitions and usage examples
- Updated changeset with migration guide for size changes
Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Migrated Avatar component from Base UI to custom implementation with size changes:
|
||||
|
||||
- Base UI-specific props are no longer supported
|
||||
- Size values have been updated:
|
||||
- New `x-small` size added (1.25rem / 20px)
|
||||
- `small` size unchanged (1.5rem / 24px)
|
||||
- `medium` size unchanged (2rem / 32px, default)
|
||||
- `large` size **changed from 3rem to 2.5rem** (40px)
|
||||
- New `x-large` size added (3rem / 48px)
|
||||
|
||||
Migration:
|
||||
|
||||
```diff
|
||||
# Remove Base UI-specific props
|
||||
- <Avatar src="..." name="..." render={...} />
|
||||
+ <Avatar src="..." name="..." />
|
||||
|
||||
# Update large size usage to x-large for same visual size
|
||||
- <Avatar src="..." name="..." size="large" />
|
||||
+ <Avatar src="..." name="..." size="x-large" />
|
||||
```
|
||||
|
||||
Added `purpose` prop for accessibility control (`'informative'` or `'decoration'`).
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
snippetUsage,
|
||||
snippetSizes,
|
||||
snippetFallback,
|
||||
snippetPurpose,
|
||||
} from './avatar.props';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
@@ -58,6 +59,17 @@ If the image is not available, the avatar will show the initials of the name.
|
||||
code={snippetFallback}
|
||||
/>
|
||||
|
||||
### The `purpose` prop
|
||||
|
||||
Control how the avatar is announced to screen readers using the `purpose` prop.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<AvatarSnippet story="Purpose" />}
|
||||
code={snippetPurpose}
|
||||
/>
|
||||
|
||||
<Theming component="Avatar" />
|
||||
|
||||
<ChangelogComponent component="avatar" />
|
||||
|
||||
@@ -10,10 +10,15 @@ export const avatarPropDefs: Record<string, PropDef> = {
|
||||
},
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium', 'large'],
|
||||
values: ['x-small', 'small', 'medium', 'large', 'x-large'],
|
||||
default: 'medium',
|
||||
responsive: true,
|
||||
},
|
||||
purpose: {
|
||||
type: 'enum',
|
||||
values: ['informative', 'decoration'],
|
||||
default: 'informative',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -26,6 +31,10 @@ export const snippetUsage = `import { Avatar } from '@backstage/ui';
|
||||
/>`;
|
||||
|
||||
export const snippetSizes = `<Flex gap="4" direction="column">
|
||||
<Avatar
|
||||
src="https://avatars.githubusercontent.com/u/1540635?v=4"
|
||||
name="Charles de Dreuille" size="x-small"
|
||||
/>
|
||||
<Avatar
|
||||
src="https://avatars.githubusercontent.com/u/1540635?v=4"
|
||||
name="Charles de Dreuille" size="small"
|
||||
@@ -38,9 +47,43 @@ export const snippetSizes = `<Flex gap="4" direction="column">
|
||||
src="https://avatars.githubusercontent.com/u/1540635?v=4"
|
||||
name="Charles de Dreuille" size="large"
|
||||
/>
|
||||
<Avatar
|
||||
src="https://avatars.githubusercontent.com/u/1540635?v=4"
|
||||
name="Charles de Dreuille" size="x-large"
|
||||
/>
|
||||
</Flex>`;
|
||||
|
||||
export const snippetFallback = `<Avatar
|
||||
src="https://avatars.githubusercontent.com/u/15406AAAAAAAAA"
|
||||
name="Charles de Dreuille"
|
||||
/>`;
|
||||
|
||||
export const snippetPurpose = `<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text variant="title-x-small">Informative (default)</Text>
|
||||
<Text variant="body-medium">
|
||||
Use when avatar appears alone. Announced as "Charles de Dreuille" to screen readers:
|
||||
</Text>
|
||||
<Flex gap="2" align="center">
|
||||
<Avatar
|
||||
src="https://avatars.githubusercontent.com/u/1540635?v=4"
|
||||
name="Charles de Dreuille"
|
||||
purpose="informative"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="1">
|
||||
<Text variant="title-x-small">Decoration</Text>
|
||||
<Text variant="body-medium">
|
||||
Use when name appears adjacent to avatar. Hidden from screen readers to avoid redundancy:
|
||||
</Text>
|
||||
<Flex gap="2" align="center">
|
||||
<Avatar
|
||||
src="https://avatars.githubusercontent.com/u/1540635?v=4"
|
||||
name="Charles de Dreuille"
|
||||
purpose="decoration"
|
||||
/>
|
||||
<Text>Charles de Dreuille</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>`;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar';
|
||||
import { ButtonProps as ButtonProps_2 } from 'react-aria-components';
|
||||
import { CellProps as CellProps_2 } from 'react-aria-components';
|
||||
import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components';
|
||||
@@ -57,17 +56,14 @@ export type AlignItems = 'stretch' | 'start' | 'center' | 'end';
|
||||
|
||||
// @public (undocumented)
|
||||
export const Avatar: ForwardRefExoticComponent<
|
||||
AvatarProps & RefAttributes<HTMLSpanElement>
|
||||
AvatarProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AvatarProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Avatar_2.Root> {
|
||||
// (undocumented)
|
||||
export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
name: string;
|
||||
// (undocumented)
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
// (undocumented)
|
||||
purpose?: 'decoration' | 'informative';
|
||||
size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
|
||||
src: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='x-small'] {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='small'] {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
@@ -45,6 +50,11 @@
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='large'] {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='x-large'] {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
@@ -53,6 +63,7 @@
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bui-AvatarFallback {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Avatar } from './index';
|
||||
import { Flex } from '../..';
|
||||
import { Flex, Text } from '../..';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/Avatar',
|
||||
@@ -46,9 +46,42 @@ export const Sizes: Story = {
|
||||
},
|
||||
render: args => (
|
||||
<Flex>
|
||||
<Avatar {...args} size="x-small" />
|
||||
<Avatar {...args} size="small" />
|
||||
<Avatar {...args} size="medium" />
|
||||
<Avatar {...args} size="large" />
|
||||
<Avatar {...args} size="x-large" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const Purpose: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: args => (
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text variant="title-x-small">Informative (default)</Text>
|
||||
<Text variant="body-medium">
|
||||
Use when avatar appears alone. Announced as "{args.name}" to screen
|
||||
readers:
|
||||
</Text>
|
||||
<Flex gap="2" align="center">
|
||||
<Avatar {...args} purpose="informative" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="1">
|
||||
<Text variant="title-x-small">Decoration</Text>
|
||||
<Text variant="body-medium">
|
||||
Use when name appears adjacent to avatar. Hidden from screen readers
|
||||
to avoid redundancy:
|
||||
</Text>
|
||||
<Flex gap="2" align="center">
|
||||
<Avatar {...args} purpose="decoration" />
|
||||
<Text>{args.name}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -14,48 +14,72 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, ElementRef } from 'react';
|
||||
import { Avatar as AvatarPrimitive } from '@base-ui-components/react/avatar';
|
||||
import { forwardRef, useState, useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { AvatarProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import styles from './Avatar.module.css';
|
||||
|
||||
/** @public */
|
||||
export const Avatar = forwardRef<
|
||||
ElementRef<typeof AvatarPrimitive.Root>,
|
||||
AvatarProps
|
||||
>((props, ref) => {
|
||||
export const Avatar = forwardRef<HTMLDivElement, AvatarProps>((props, ref) => {
|
||||
const { classNames, dataAttributes, cleanedProps } = useStyles('Avatar', {
|
||||
size: 'medium',
|
||||
purpose: 'informative',
|
||||
...props,
|
||||
});
|
||||
|
||||
const { className, src, name, ...rest } = cleanedProps;
|
||||
const { className, src, name, purpose, ...rest } = cleanedProps;
|
||||
|
||||
const [imageStatus, setImageStatus] = useState<
|
||||
'loading' | 'loaded' | 'error'
|
||||
>('loading');
|
||||
|
||||
useEffect(() => {
|
||||
setImageStatus('loading');
|
||||
const img = new Image();
|
||||
img.onload = () => setImageStatus('loaded');
|
||||
img.onerror = () => setImageStatus('error');
|
||||
img.src = src;
|
||||
|
||||
return () => {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
const initials = name
|
||||
.split(' ')
|
||||
.map(word => word[0])
|
||||
.join('')
|
||||
.toLocaleUpperCase('en-US')
|
||||
.slice(0, 2);
|
||||
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
<div
|
||||
ref={ref}
|
||||
role="img"
|
||||
aria-label={purpose === 'informative' ? name : undefined}
|
||||
aria-hidden={purpose === 'decoration' ? true : undefined}
|
||||
className={clsx(classNames.root, styles[classNames.root], className)}
|
||||
{...dataAttributes}
|
||||
{...rest}
|
||||
>
|
||||
<AvatarPrimitive.Image
|
||||
className={clsx(classNames.image, styles[classNames.image])}
|
||||
src={src}
|
||||
/>
|
||||
<AvatarPrimitive.Fallback
|
||||
className={clsx(classNames.fallback, styles[classNames.fallback])}
|
||||
>
|
||||
{(name || '')
|
||||
.split(' ')
|
||||
.map(word => word[0])
|
||||
.join('')
|
||||
.toLocaleUpperCase('en-US')
|
||||
.slice(0, 2)}
|
||||
</AvatarPrimitive.Fallback>
|
||||
</AvatarPrimitive.Root>
|
||||
{imageStatus === 'loaded' ? (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
className={clsx(classNames.image, styles[classNames.image])}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={clsx(classNames.fallback, styles[classNames.fallback])}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
Avatar.displayName = 'Avatar';
|
||||
|
||||
@@ -14,12 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Avatar } from '@base-ui-components/react/avatar';
|
||||
|
||||
/** @public */
|
||||
export interface AvatarProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Avatar.Root> {
|
||||
export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
/**
|
||||
* URL of the image to display
|
||||
*/
|
||||
src: string;
|
||||
|
||||
/**
|
||||
* Name of the person - used for generating initials and accessibility labels
|
||||
*/
|
||||
name: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
|
||||
/**
|
||||
* Size of the avatar
|
||||
* @defaultValue 'medium'
|
||||
*/
|
||||
size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
|
||||
|
||||
/**
|
||||
* Determines how the avatar is presented to assistive technologies.
|
||||
* - 'informative': Avatar is announced as "\{name\}" to screen readers
|
||||
* - 'decoration': Avatar is hidden from screen readers (use when name appears in adjacent text)
|
||||
* @defaultValue 'informative'
|
||||
*/
|
||||
purpose?: 'decoration' | 'informative';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user