import React, { useCallback, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import styles from './Controls.module.css'; export { Select, type SelectOption, type SelectProps } from './Select'; type ButtonProps = { children: React.ReactNode; onClick?: () => void; variant?: 'primary' | 'ghost'; disabled?: boolean; title?: string | undefined; /** Подпись для скринридеров (иконки без текста). */ ariaLabel?: string | undefined; /** Компактная кнопка под одну иконку. */ iconOnly?: boolean; /** Позиция тултипа относительно кнопки. */ tooltipPlacement?: 'top' | 'bottom' | 'bottom-left'; 'data-testid'?: string; }; export function Button({ children, onClick, variant = 'ghost', disabled = false, title, ariaLabel, iconOnly = false, tooltipPlacement = 'top', 'data-testid': testId, }: ButtonProps) { const btnRef = useRef(null); const hostRef = useRef(null); const [tipPos, setTipPos] = useState<{ x: number; y: number } | null>(null); const showTip = useCallback(() => { if (!title) return; const el = disabled ? hostRef.current : btnRef.current; if (!el) return; const r = el.getBoundingClientRect(); if (tooltipPlacement === 'bottom-left') { setTipPos({ x: r.right, y: r.bottom }); return; } if (tooltipPlacement === 'bottom') { setTipPos({ x: r.left + r.width / 2, y: r.bottom }); return; } setTipPos({ x: r.left + r.width / 2, y: r.top }); }, [disabled, title, tooltipPlacement]); const hideTip = useCallback(() => { setTipPos(null); }, []); const btnClass = [ styles.button, variant === 'primary' ? styles.buttonPrimary : '', iconOnly ? styles.iconOnly : '', ] .filter(Boolean) .join(' '); const tipClass = tooltipPlacement === 'bottom-left' ? styles.tooltipBottomLeft : tooltipPlacement === 'bottom' ? styles.tooltipBottom : styles.tooltipTop; const tip = title && tipPos && typeof document !== 'undefined' ? createPortal(
{title}
, document.body, ) : null; const button = ( ); // Disabled buttons don't receive mouse events — host span keeps tooltip usable. // Single DOM root (not Fragment) so flex/grid parents don't mis-place the button. if (disabled && title) { return ( {button} {tip} ); } return ( {button} {tip} ); } type InputProps = { value: string; placeholder?: string; onChange: (v: string) => void; autoFocus?: boolean; onKeyDown?: (e: React.KeyboardEvent) => void; onBlur?: () => void; }; export function Input({ value, placeholder, onChange, autoFocus, onKeyDown, onBlur }: InputProps) { return ( onChange(e.target.value)} onKeyDown={onKeyDown} onBlur={onBlur} /> ); }