Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/pages/OperationForm/OperationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export const OperationForm = () => {
};

return (
<RegularForm title="Операция" onSubmit={handleSubmit(onSubmit)}>
<RegularForm onSubmit={handleSubmit(onSubmit)}>
<RegularForm.Title>Операция</RegularForm.Title>
<FormSelectField
name="type"
options={[costOperationOption, profitOperationOption]}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/ProductForm/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const ProductForm = () => {
};

return (
<RegularForm title="Товар" onSubmit={handleSubmit(onSubmit)}>
<RegularForm onSubmit={handleSubmit(onSubmit)}>
<RegularForm.Title>Товар</RegularForm.Title>
<FormInputField name="name" register={register} type="text" errors={errors.name}>
Название
</FormInputField>
Expand Down
6 changes: 4 additions & 2 deletions src/pages/ProfileForm/ProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const ChangeProfileForm = () => {
};

return (
<RegularForm title="Изменить профиль" onSubmit={handleSubmit(onSubmit)}>
<RegularForm onSubmit={handleSubmit(onSubmit)}>
<RegularForm.Title>Изменить профиль</RegularForm.Title>
<FormInputField name="name" register={register} type="text" errors={errors.name}>
Псевдоним
</FormInputField>
Expand All @@ -57,7 +58,8 @@ const ChangePasswordForm = () => {
};

return (
<RegularForm title="Изменить пароль" onSubmit={handleSubmit(onSubmit)}>
<RegularForm onSubmit={handleSubmit(onSubmit)}>
<RegularForm.Title>Изменить пароль</RegularForm.Title>
<FormInputField name="password" register={register} type="password" errors={errors.password}>
Пароль
</FormInputField>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/SignInForm/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export const SignInForm = () => {
};

return (
<RegularForm title="Войти" onSubmit={handleSubmit(onSubmit)}>
<RegularForm onSubmit={handleSubmit(onSubmit)}>
<RegularForm.Title>Войти</RegularForm.Title>
<FormInputField name="email" register={register} type="email" errors={errors.email}>
Email
</FormInputField>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export const SignUpForm = () => {
};

return (
<RegularForm title="Зарегистрироваться" onSubmit={handleSubmit(onSubmit)}>
<RegularForm onSubmit={handleSubmit(onSubmit)}>
<RegularForm.Title>Зарегистрироваться</RegularForm.Title>
<FormInputField name="email" register={register} type="email" errors={errors.email}>
Email
</FormInputField>
Expand Down
26 changes: 6 additions & 20 deletions src/shared/collapse/Collapse.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
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 = {
title: string;
children: ReactNode;
};

export const Collapse = ({ title, children }: CollapseProps) => {
export const Collapse: React.FC<CollapseProps> = ({ title, children }) => {
const [isOpen, setIsOpen] = useState(false);
const [height, setHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(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 (
<div className={s['collapse-container']}>
<button className={s['collapse-button']} onClick={toggleCollapse}>
{title}
</button>
<div
style={{
height: `${isOpen ? height : 0}px`,
height: isOpen ? `${height}px` : '0px',
overflow: 'hidden',
transition: 'height 300ms ease',
}}
Expand Down
26 changes: 26 additions & 0 deletions src/shared/collapse/hooks/useCollapseHeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useEffect, useRef, useState } from "react";

export const useCollapseHeight = (isOpen: boolean) => {
const [height, setHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(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 };
};
10 changes: 7 additions & 3 deletions src/shared/forms/RegularForm/RegularForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import s from './RegularForm.module.scss';
type RegularFormProps = {
onSubmit: FormEventHandler<HTMLFormElement>;
children: ReactNode;
title?: string;
};

export const RegularForm = ({ onSubmit, title, children }: RegularFormProps) => {
const RegularForm = ({ onSubmit, children }: RegularFormProps) => {
return (
<form className={s.form} onSubmit={onSubmit}>
{title && <h2 className={s.title}>{title}</h2>}
{children}
</form>
);
};

const Title = ({ children }: { children: ReactNode }) => <h2 className={s.title}>{children}</h2>;

RegularForm.Title = Title;

export { RegularForm };
3 changes: 2 additions & 1 deletion src/shared/tooltip-buttons/TooltipButtons.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
95 changes: 7 additions & 88 deletions src/shared/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -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<Position, (props: CoordProps) => 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;
Expand All @@ -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<HTMLDivElement>(null);
const targetRef = useRef<HTMLDivElement>(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 (
<>
Expand Down
59 changes: 59 additions & 0 deletions src/shared/tooltip/hooks/useTooltip.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const targetRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<number | null>(null);
const mountTimerRef = useRef<number | null>(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,
};
};
42 changes: 42 additions & 0 deletions src/shared/tooltip/utils/tooltipPosition.ts
Original file line number Diff line number Diff line change
@@ -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<Position, (props: CoordProps) => 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,
}),
};