Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plasma-new-hope(avatar): Refactoring + a11y #1180

Merged
merged 5 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/plasma-b2c/api/plasma-b2c.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ import { SpacingProps } from '@salutejs/plasma-core';
import { SpacingProps as SpacingProps_2 } from '@salutejs/plasma-new-hope/styled-components';
import { SpinnerProps } from '@salutejs/plasma-core';
import { SSRProvider } from '@salutejs/plasma-new-hope/styled-components';
import { StatusLabels } from '@salutejs/plasma-new-hope/types/components/Avatar/Avatar.types';
import { StatusType } from '@salutejs/plasma-hope';
import { StyledCard } from '@salutejs/plasma-hope';
import { StyledComponent } from 'styled-components';
Expand Down Expand Up @@ -333,6 +334,7 @@ customText?: string | undefined;
status?: "active" | "inactive" | undefined;
isScalable?: boolean | undefined;
focused?: boolean | undefined;
statusLabels?: StatusLabels | undefined;
} & RefAttributes<HTMLDivElement>>;

// @public (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,32 @@ export function App() {
```

### Доступность
Добавляем `"role"` и `"tabIndex"`.

#### Avatar c использованием изображения

В данном случае руководствуемся принципом универсального дизайна, т.е. незрячий должен получить ту же информацию, что и зрячий.

Поэтому добавляем/используем свойства: `role`, `tabIndex` и `aria-label`.

Примечание:
- если указано свойство `name` то `aria-label` можно опустить;

```tsx live
import React from 'react';
import { Avatar } from '@salutejs/{{ package }}';

export function App() {
return (
<>
<Avatar role="button" tabIndex={0} name="Иван Фадеев" url="https://avatars.githubusercontent.com/u/1813468?v=4" />
Yakutoc marked this conversation as resolved.
Show resolved Hide resolved
</>
);
}
```

#### Avatar c текстом

В этом случае достаточно указать свойство `name`.

```tsx live
import React from 'react';
Expand All @@ -90,8 +115,33 @@ import { Avatar } from '@salutejs/{{ package }}';
export function App() {
return (
<>
<Avatar role="button" tabIndex="0" name="Иван Фадеев" />
<Avatar role="button" tabIndex={0} name="Иван Фадеев" />
</>
);
}
```

#### Avatar и статус

Если указано свойство `status` его значение будет так же озвучено в комбинации со свойством `name` или `aria-label`.

```tsx live
import React from 'react';
import { Avatar } from '@salutejs/{{ package }}';

export function App() {
return (
<>
<Avatar role="button" tabIndex={0} name="Иван Фадеев" status="inactive" />
</>
);
}
```

Озвучит как `ИФ. Неактивен`. (В данном примере озвучиваются инициалы, производное от ФИО)

#### Свойство statusLabels

Опциональное свойство для корректной озвучки значений свойства `status`.

По-умолчанию стоит значение для русскоговорящих `{ active: 'Активен', inactive: 'Неактивен' }`.
Yakutoc marked this conversation as resolved.
Show resolved Hide resolved
43 changes: 40 additions & 3 deletions packages/plasma-new-hope/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import { indicatorConfig, indicatorTokens } from '../Indicator';

import { classes, tokens } from './Avatar.tokens';
import { base, Wrapper, Image, StatusIcon, Text } from './Avatar.styles';
import { AvatarProps } from './Avatar.types';
import { base as viewCSS } from './variations/_size/base';
import { base as focusedCSS } from './variations/_focused/base';
import { getInitialsForName } from './utils';
import type { AvatarProps, StatusLabels } from './Avatar.types';

const StatusLabelsDefault: StatusLabels = {
active: 'Активен',
inactive: 'Неактивен',
};
Yakutoc marked this conversation as resolved.
Show resolved Hide resolved

const getAvatarContent = ({
customText,
Expand All @@ -29,7 +34,27 @@ const getAvatarContent = ({
return <Text>{initials}</Text>;
};

const getAriaLabel = ({
url,
name,
status,
'aria-label': ariaLabelProp,
statusLabels,
}: Pick<AvatarProps, 'url' | 'status' | 'name' | 'aria-label'> & {
statusLabels: StatusLabels;
}) => {
if (!url) {
return;
}

// INFO: включаем aria-label чтобы озвучить что на изображении
const ariaLabel = !ariaLabelProp || ariaLabelProp.trim() === '' ? name : ariaLabelProp;

return status ? `${ariaLabel}. ${statusLabels[status]}` : ariaLabel;
};

const mergedConfig = mergeConfig(indicatorConfig);

const Indicator: React.FunctionComponent<
React.HTMLAttributes<HTMLDivElement> & { status: AvatarProps['status'] }
> = component(mergedConfig) as never;
Expand All @@ -51,18 +76,30 @@ export const avatarRoot = (Root: RootProps<HTMLDivElement, AvatarProps>) => {
className,
focused = true,
isScalable,
statusLabels = StatusLabelsDefault,
...rest
} = props;

const initials = useMemo(() => getInitialsForName(name), [name]);
const ariaLabel = getAriaLabel({
...props,
statusLabels,
});

return (
<Root ref={ref} size={avatarSize} className={cx(classes.avatarItem, className)} focused={focused} {...rest}>
<Root
ref={ref}
size={avatarSize}
className={cx(classes.avatarItem, className)}
aria-label={ariaLabel}
focused={focused}
{...rest}
>
<Wrapper isScalable={isScalable}>{getAvatarContent({ customText, url, initials, name })}</Wrapper>

{status && (
<StatusIcon>
<StyledIndicator status={status} />
<StyledIndicator aria-label={statusLabels[status]} status={status} />
shuga2704 marked this conversation as resolved.
Show resolved Hide resolved
</StatusIcon>
)}
</Root>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ type CustomAvatarProps = {
customText?: string;
// Статус профиля
status?: 'active' | 'inactive';
// Скейл при наведении
// Масштабируемый при наведении
isScalable?: boolean;
// Фокус
focused?: boolean;
/**
* Словарь для озвучивания значений свойства status [a11y]
* @default
* { active: 'Активен', inactive: 'Неактивен' }
*/
statusLabels?: StatusLabels;
};

export type StatusLabels = Record<'active' | 'inactive', string>;

export type AvatarProps = HTMLAttributes<HTMLDivElement> & CustomAvatarProps;
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,38 @@ export const Default: Story = {
};

export const Accessibility: Story = {
args: {
role: 'button',
name: 'Геннадий Силуянович',
tabIndex: 0,
view: 'default',
size: 'xxl',
status: 'active',
focused: true,
},
};

export const AccessibilityWithURL: Story = {
args: {
role: 'button',
tabIndex: 0,
view: 'default',
size: 'xxl',
name: 'Иван Фадеев',
status: 'active',
focused: true,
name: 'Микула Селянинович',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
};

export const AccessibilityWithCustomText: Story = {
args: {
role: 'button',
tabIndex: 0,
view: 'default',
size: 'xxl',
status: 'inactive',
focused: true,
customText: 'ФИО',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Avatar } from '../Avatar/Avatar';
import { AvatarGroup } from './AvatarGroup';

type Story = StoryObj<ComponentProps<typeof AvatarGroup>>;
type Avatar = ComponentProps<typeof Avatar>;

const meta: Meta<typeof AvatarGroup> = {
title: 'plasma_b2c/AvatarGroup',
Expand All @@ -33,40 +34,57 @@ export const Default: Story = {

export const DynamicSize: Story = {
args: { totalCount: 10, visibleCount: 3 },
render: (args: ComponentProps<typeof AvatarGroup>) => {
const itemLength = args.totalCount;
render: ({ visibleCount, totalCount, ...args }: ComponentProps<typeof AvatarGroup>) => {
const itemLength = totalCount;

return (
<AvatarGroup {...args}>
{Array(args.visibleCount)
{Array(visibleCount)
.fill(true)
.map((_, index) => (
<Avatar size="xxl" customText={index + 1} />
<Avatar size="xxl" key={index} customText={index + 1} />
))}

{itemLength > args.visibleCount && (
<Avatar size="xxl" customText={`+${itemLength - args.visibleCount}`} />
)}
{itemLength > visibleCount && <Avatar size="xxl" customText={`+${itemLength - visibleCount}`} />}
</AvatarGroup>
);
},
};

const list: Array<Avatar> = [
{
name: 'Илья Муромец',
status: 'active',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
{
name: 'Алеша Попович',
status: 'active',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
{
name: 'Добрыня Никитич',
status: 'active',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
{
name: 'Микула Селянинович',
status: 'inactive',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
{
name: 'Ставр Годинович',
status: 'inactive',
},
];

export const Accessibility: Story = {
render: (args: ComponentProps<typeof AvatarGroup>) => {
return (
<AvatarGroup {...args}>
{Array(5)
.fill(true)
.map(() => (
<Avatar
role="button"
tabIndex={0}
focused
size="xxl"
url="https://avatars.githubusercontent.com/u/1813468?v=4"
/>
))}
{list.map((props) => (
<Avatar role="button" tabIndex={0} focused key={props.name} size="xxl" {...props} />
))}
</AvatarGroup>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,38 @@ export const Default: Story = {
};

export const Accessibility: Story = {
args: {
role: 'button',
name: 'Геннадий Силуянович',
tabIndex: 0,
view: 'default',
size: 'xxl',
status: 'active',
focused: true,
},
};

export const AccessibilityWithURL: Story = {
args: {
role: 'button',
tabIndex: 0,
view: 'default',
size: 'xxl',
name: 'Иван Фадеев',
status: 'active',
focused: true,
name: 'Микула Селянинович',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
};

export const AccessibilityWithCustomText: Story = {
args: {
role: 'button',
tabIndex: 0,
view: 'default',
size: 'xxl',
status: 'inactive',
focused: true,
customText: 'ФИО',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,36 @@ export const Default: Story = {
export const Accessibility: Story = {
args: {
role: 'button',
name: 'Геннадий Силуянович',
tabIndex: 0,
view: 'default',
size: 'xxl',
name: 'Иван Фадеев',
status: 'active',
focused: true,
},
};

export const AccessibilityWithURL: Story = {
args: {
role: 'button',
tabIndex: 0,
view: 'default',
size: 'xxl',
status: 'active',
focused: true,
name: 'Микула Селянинович',
url: 'https://avatars.githubusercontent.com/u/1813468?v=4',
},
};

export const AccessibilityWithCustomText: Story = {
args: {
role: 'button',
tabIndex: 0,
view: 'default',
size: 'xxl',
status: 'inactive',
focused: true,
customText: 'ФИО',
},
};