'use client';
import * as React from 'react';
import { classNames, hasReactNode } from '@vkontakte/vkjs';
import { useAdaptivity } from '../../hooks/useAdaptivity';
import { usePlatform } from '../../hooks/usePlatform';
import type { Elevation, HasAlign } from '../../types';
import { Spinner } from '../Spinner/Spinner';
import { Tappable, type TappableOmitProps } from '../Tappable/Tappable';
import '../Tappable/Tappable.module.css';
import '../Spinner/Spinner.module.css';
import styles from './Button.module.css';
const stylesElevation = {
'1': styles.elevation1,
'2': styles.elevation2,
'3': styles.elevation3,
'4': styles.elevation4,
};
const stylesSize = {
s: styles.sizeS,
m: styles.sizeM,
l: styles.sizeL,
};
const stylesMode = {
primary: styles.modePrimary,
secondary: styles.modeSecondary,
tertiary: styles.modeTertiary,
outline: styles.modeOutline,
link: styles.modeLink,
};
const stylesAppearance = {
'accent': styles.appearanceAccent,
'positive': styles.appearancePositive,
'negative': styles.appearanceNegative,
'neutral': styles.appearanceNeutral,
'overlay': styles.appearanceOverlay,
'accent-invariable': styles.appearanceAccentInvariable,
};
const stylesAlign = {
left: styles.alignLeft,
right: styles.alignRight,
};
const densityClassNames = {
none: styles.densityNone,
regular: styles.densityRegular,
};
export interface VKUIButtonProps extends HasAlign {
/**
* Режим отображения кнопки.
*/
mode?: 'primary' | 'secondary' | 'tertiary' | 'outline' | 'link' | undefined;
/**
* Цветовая схема кнопки.
*/
appearance?:
| 'accent'
| 'positive'
| 'negative'
| 'neutral'
| 'overlay'
| 'accent-invariable'
| undefined;
/**
* Размер кнопки.
*/
size?: 's' | 'm' | 'l' | undefined;
/**
* Растягивает кнопку на всю ширину контейнера.
*/
stretched?: boolean | undefined;
/**
* Контент, отображаемый перед основным содержимым кнопки.
*/
before?: React.ReactNode | undefined;
/**
* Контент, отображаемый после основного содержимого кнопки.
*/
after?: React.ReactNode | undefined;
/**
* Включает состояние загрузки (отображает спиннер).
*
* ⚠️ **Важно для доступности**: При использовании `loading={true}` компонент автоматически
* устанавливает `aria-label` в значение `loadingLabel` (по умолчанию "Загрузка..."),
* чтобы скринридер мог объявить контекст загрузки. Вы можете переопределить это значение,
* передав свойство `loadingLabel`.
*
* @example
*
* // Скринридер объявит: "Загрузка..., кнопка"
*
* @example
*
*/
loading?: boolean | undefined;
/**
* Текст для `aria-label` при состоянии загрузки.
* Подменяет переданный в компонент `aria-label` только когда `loading={true}`.
*/
loadingLabel?: string | undefined;
/**
* Отключает анимацию спиннера загрузки.
*/
disableSpinnerAnimation?: boolean | undefined;
/**
* Добавляет скругленные углы кнопке.
*/
rounded?: boolean | undefined;
/**
* Добавляет тень кнопке.
*/
elevation?: Elevation | undefined;
}
export interface ButtonProps extends Omit, VKUIButtonProps {}
/**
* @see https://vkui.io/components/button
*/
export const Button = ({
size = 's',
mode = 'primary',
appearance = 'accent',
stretched = false,
align = 'center',
children,
before,
after,
getRootRef,
loading,
loadingLabel = 'Загрузка...',
onClick,
disableSpinnerAnimation,
rounded,
disabled,
href,
'aria-label': ariaLabelProp,
elevation,
...restProps
}: ButtonProps): React.ReactNode => {
const hasIconOnly = !children && Boolean(after) !== Boolean(before);
const { density = 'none' } = useAdaptivity();
const platform = usePlatform();
const isDisabled = disabled || loading;
const hasHref = href !== undefined;
const ariaLabel = loading ? loadingLabel : ariaLabelProp;
const buttonProps = React.useMemo(() => {
if (hasHref) {
return isDisabled
? {
// Для disabled/loading ссылок нужно удалить href и добавить role="link"
// согласно https://w3c.github.io/html-aria/#example-communicate-a-disabled-link-with-aria
'role': 'link' as const,
'Component': 'a' as const,
'aria-disabled': isDisabled,
}
: {
href,
'aria-disabled': isDisabled,
};
} else {
return {
Component: 'button' as const,
disabled,
};
}
}, [disabled, hasHref, href, isDisabled]);
return (
{loading && (
)}
{hasReactNode(before) && (
{before}
)}
{hasReactNode(children) && (
{children}
)}
{hasReactNode(after) && (
{after}
)}
);
};