'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 && ( ); };