From bef1ea59b1612d7346774fbfa700243146af1eea Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Mon, 24 Feb 2025 22:36:02 +0300 Subject: [PATCH] =?UTF-8?q?[=D0=9F=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D1=8B]=20=D0=9E=D1=82=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BB=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D1=83=D1=8F=20=D1=80=D0=B5=D0=B0=D0=BA=D1=82=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/OperationForm/OperationForm.tsx | 3 +- src/pages/ProductForm/ProductForm.tsx | 3 +- src/pages/ProfileForm/ProfileForm.tsx | 6 +- src/pages/SignInForm/SignInForm.tsx | 3 +- src/pages/SignUpForm/SignUpForm.tsx | 3 +- src/shared/collapse/Collapse.tsx | 26 ++--- .../collapse/hooks/useCollapseHeight.ts | 26 +++++ src/shared/forms/RegularForm/RegularForm.tsx | 10 +- src/shared/tooltip-buttons/TooltipButtons.tsx | 3 +- src/shared/tooltip/Tooltip.tsx | 95 ++----------------- src/shared/tooltip/hooks/useTooltip.ts | 59 ++++++++++++ src/shared/tooltip/utils/tooltipPosition.ts | 42 ++++++++ 12 files changed, 161 insertions(+), 118 deletions(-) create mode 100644 src/shared/collapse/hooks/useCollapseHeight.ts create mode 100644 src/shared/tooltip/hooks/useTooltip.ts create mode 100644 src/shared/tooltip/utils/tooltipPosition.ts diff --git a/src/pages/OperationForm/OperationForm.tsx b/src/pages/OperationForm/OperationForm.tsx index a3153be53..e7aa2902c 100644 --- a/src/pages/OperationForm/OperationForm.tsx +++ b/src/pages/OperationForm/OperationForm.tsx @@ -34,7 +34,8 @@ export const OperationForm = () => { }; return ( - + + Операция { }; return ( - + + Товар Название diff --git a/src/pages/ProfileForm/ProfileForm.tsx b/src/pages/ProfileForm/ProfileForm.tsx index 4ae335d8e..e9c5b7972 100644 --- a/src/pages/ProfileForm/ProfileForm.tsx +++ b/src/pages/ProfileForm/ProfileForm.tsx @@ -30,7 +30,8 @@ const ChangeProfileForm = () => { }; return ( - + + Изменить профиль Псевдоним @@ -57,7 +58,8 @@ const ChangePasswordForm = () => { }; return ( - + + Изменить пароль Пароль diff --git a/src/pages/SignInForm/SignInForm.tsx b/src/pages/SignInForm/SignInForm.tsx index 08b60f2de..bb614e6b4 100644 --- a/src/pages/SignInForm/SignInForm.tsx +++ b/src/pages/SignInForm/SignInForm.tsx @@ -25,7 +25,8 @@ export const SignInForm = () => { }; return ( - + + Войти Email diff --git a/src/pages/SignUpForm/SignUpForm.tsx b/src/pages/SignUpForm/SignUpForm.tsx index d714f03eb..64676cf7f 100644 --- a/src/pages/SignUpForm/SignUpForm.tsx +++ b/src/pages/SignUpForm/SignUpForm.tsx @@ -25,7 +25,8 @@ export const SignUpForm = () => { }; return ( - + + Зарегистрироваться Email diff --git a/src/shared/collapse/Collapse.tsx b/src/shared/collapse/Collapse.tsx index a5b8a730f..d61e5934c 100644 --- a/src/shared/collapse/Collapse.tsx +++ b/src/shared/collapse/Collapse.tsx @@ -1,4 +1,5 @@ -import React, { useState, useRef, useEffect, ReactNode } from 'react'; +import React, { useState, ReactNode } from 'react'; +import { useCollapseHeight } from './hooks/useCollapseHeight'; import s from './Collapse.module.scss'; type CollapseProps = { @@ -6,29 +7,14 @@ type CollapseProps = { children: ReactNode; }; -export const Collapse = ({ title, children }: CollapseProps) => { +export const Collapse: React.FC = ({ title, children }) => { const [isOpen, setIsOpen] = useState(false); - const [height, setHeight] = useState(0); - const contentRef = useRef(null); + const { height, contentRef } = useCollapseHeight(isOpen); const toggleCollapse = () => { - setIsOpen(!isOpen); + setIsOpen((prev) => !prev); }; - useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => - entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)) - ); - - const currentContentRef = contentRef.current; - - currentContentRef && resizeObserver.observe(currentContentRef); - - return () => { - currentContentRef && resizeObserver.unobserve(currentContentRef); - }; - }, []); - return (
{ + const [height, setHeight] = useState(0); + const contentRef = useRef(null); + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)); + }); + + const currentContentRef = contentRef.current; + + if (currentContentRef) { + resizeObserver.observe(currentContentRef); + } + + return () => { + if (currentContentRef) { + resizeObserver.unobserve(currentContentRef); + } + }; + }, []); + + return { height, contentRef }; + }; \ No newline at end of file diff --git a/src/shared/forms/RegularForm/RegularForm.tsx b/src/shared/forms/RegularForm/RegularForm.tsx index 8e4f828e7..586a45019 100644 --- a/src/shared/forms/RegularForm/RegularForm.tsx +++ b/src/shared/forms/RegularForm/RegularForm.tsx @@ -4,14 +4,18 @@ import s from './RegularForm.module.scss'; type RegularFormProps = { onSubmit: FormEventHandler; children: ReactNode; - title?: string; }; -export const RegularForm = ({ onSubmit, title, children }: RegularFormProps) => { +const RegularForm = ({ onSubmit, children }: RegularFormProps) => { return (
- {title &&

{title}

} {children}
); }; + +const Title = ({ children }: { children: ReactNode }) =>

{children}

; + +RegularForm.Title = Title; + +export { RegularForm }; diff --git a/src/shared/tooltip-buttons/TooltipButtons.tsx b/src/shared/tooltip-buttons/TooltipButtons.tsx index 37d399c0d..26e3deaa4 100644 --- a/src/shared/tooltip-buttons/TooltipButtons.tsx +++ b/src/shared/tooltip-buttons/TooltipButtons.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; -import { Position, Tooltip } from '../tooltip/Tooltip'; +import { Position } from '../tooltip/utils/tooltipPosition'; +import {Tooltip} from "../tooltip/Tooltip"; import { Button } from '../button/Button'; import s from './TooltipButtons.module.scss'; diff --git a/src/shared/tooltip/Tooltip.tsx b/src/shared/tooltip/Tooltip.tsx index d91366f9e..9b9164918 100644 --- a/src/shared/tooltip/Tooltip.tsx +++ b/src/shared/tooltip/Tooltip.tsx @@ -1,51 +1,10 @@ -import React, { useState, useRef, useLayoutEffect, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { createPortal } from 'react-dom'; +import { useTooltip } from './hooks/useTooltip'; +import { Position } from './utils/tooltipPosition'; import cn from 'clsx'; import s from './Tooltip.module.scss'; -type Coords = { - top: number; - left: number; -}; - -type CoordProps = { - targetRect: DOMRect; - tooltipRect: DOMRect; - offset: number; -}; - -export type Position = 'top' | 'bottom' | 'left' | 'right'; - -type PositionMap = Record Coords>; - -const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; - -const YLeft = (primary: DOMRect, secondary: DOMRect) => - primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); -const XTop = (primary: DOMRect, secondary: DOMRect) => - primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); - -const positionMap: PositionMap = { - top: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.top + window.scrollY - tooltipRect.height - offset, - left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), - }), - - bottom: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.bottom + window.scrollY + offset, - left: YLeft(targetRect, tooltipRect), - }), - left: ({ targetRect, tooltipRect, offset }) => ({ - top: XTop(targetRect, tooltipRect), - left: targetRect.left + window.scrollX - tooltipRect.width - offset, - }), - - right: ({ targetRect, tooltipRect, offset }) => ({ - top: XTop(targetRect, tooltipRect), - left: targetRect.left + window.scrollX + targetRect.width + offset, - }), -}; - type TooltipProps = { children: ReactNode; content: ReactNode; @@ -54,50 +13,10 @@ type TooltipProps = { }; export const Tooltip = ({ children, content, duration = 1000, position = 'bottom' }: TooltipProps) => { - const [visible, setVisible] = useState(false); - const [mounted, setMounted] = useState(false); - const [coords, setCoords] = useState({ top: 0, left: 0 }); - const tooltipRef = useRef(null); - const targetRef = useRef(null); - const timerRef = useRef(null); - const mountTimerRef = useRef(null); - - const mountTimer = 50; - - const clearTimeouts = () => { - timerRef.current && clearTimeout(timerRef.current); - mountTimerRef.current && clearTimeout(mountTimerRef.current); - }; - - const handleMouseEnter = () => { - clearTimeouts(); - setMounted(true); - mountTimerRef.current = setTimeout(() => setVisible(true), mountTimer); - }; - - const handleMouseLeave = () => { - setVisible(false); - timerRef.current = setTimeout(() => setMounted(false), duration + mountTimer); - }; - - useLayoutEffect(() => { - const target = targetRef.current; - const tooltip = tooltipRef.current; - - if (!target || !tooltip) return; - - if (mounted) { - tooltipRef.current?.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); - const targetRect = target.getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - const calcPosition = positionMap[position] ?? positionMap['bottom']; - setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); - } - - return () => { - clearTimeouts(); - }; - }, [mounted]); + const { visible, mounted, coords, tooltipRef, targetRef, handleMouseEnter, handleMouseLeave } = useTooltip( + position, + duration + ); return ( <> diff --git a/src/shared/tooltip/hooks/useTooltip.ts b/src/shared/tooltip/hooks/useTooltip.ts new file mode 100644 index 000000000..0304c6fd8 --- /dev/null +++ b/src/shared/tooltip/hooks/useTooltip.ts @@ -0,0 +1,59 @@ +import { useState, useRef, useLayoutEffect } from 'react'; +import { positionMap, Position } from '../utils/tooltipPosition'; + +export const useTooltip = (position: Position = 'bottom', duration: number = 1000) => { + const [visible, setVisible] = useState(false); + const [mounted, setMounted] = useState(false); + const [coords, setCoords] = useState({ top: 0, left: 0 }); + const tooltipRef = useRef(null); + const targetRef = useRef(null); + const timerRef = useRef(null); + const mountTimerRef = useRef(null); + + const mountTimer = 50; + + const clearTimeouts = () => { + if (timerRef.current) clearTimeout(timerRef.current); + if (mountTimerRef.current) clearTimeout(mountTimerRef.current); + }; + + const handleMouseEnter = () => { + clearTimeouts(); + setMounted(true); + mountTimerRef.current = window.setTimeout(() => setVisible(true), mountTimer); + }; + + const handleMouseLeave = () => { + setVisible(false); + timerRef.current = window.setTimeout(() => setMounted(false), duration + mountTimer); + }; + + useLayoutEffect(() => { + const target = targetRef.current; + const tooltip = tooltipRef.current; + + if (!target || !tooltip) return; + + if (mounted) { + tooltipRef.current?.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); + const targetRect = target.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + const calcPosition = positionMap[position] ?? positionMap['bottom']; + setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); + } + + return () => { + clearTimeouts(); + }; + }, [mounted, position]); + + return { + visible, + mounted, + coords, + tooltipRef, + targetRef, + handleMouseEnter, + handleMouseLeave, + }; +}; \ No newline at end of file diff --git a/src/shared/tooltip/utils/tooltipPosition.ts b/src/shared/tooltip/utils/tooltipPosition.ts new file mode 100644 index 000000000..c71520b60 --- /dev/null +++ b/src/shared/tooltip/utils/tooltipPosition.ts @@ -0,0 +1,42 @@ +type Coords = { + top: number; + left: number; + }; + + type CoordProps = { + targetRect: DOMRect; + tooltipRect: DOMRect; + offset: number; + }; + + export type Position = 'top' | 'bottom' | 'left' | 'right'; + + const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; + + const YLeft = (primary: DOMRect, secondary: DOMRect) => + primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); + + const XTop = (primary: DOMRect, secondary: DOMRect) => + primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); + + export const positionMap: Record Coords> = { + top: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.top + window.scrollY - tooltipRect.height - offset, + left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), + }), + + bottom: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.bottom + window.scrollY + offset, + left: YLeft(targetRect, tooltipRect), + }), + + left: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + window.scrollX - tooltipRect.width - offset, + }), + + right: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + window.scrollX + targetRect.width + offset, + }), + }; \ No newline at end of file