diff --git a/client/src/components/DraggableContainer.tsx b/client/src/components/DraggableContainer.tsx index d76d295..23b2b34 100644 --- a/client/src/components/DraggableContainer.tsx +++ b/client/src/components/DraggableContainer.tsx @@ -19,6 +19,8 @@ interface DraggableContainerProps { enabled?: boolean; /** Включить возможность перетаскивания (по умолчанию true) */ draggable?: boolean; + /** Ref элемента-хэндла для перетаскивания. Если указан, перетаскивание будет работать только при клике на этот элемент */ + dragHandleRef?: React.RefObject; /** Включить снэпинг к ближайшей четверти экрана при отпускании (по умолчанию true) */ enableSnapping?: boolean; /** Автоматическое flex-выравнивание в зависимости от прижатого угла (по умолчанию false) */ @@ -114,11 +116,22 @@ interface DraggableContainerProps { * * * + * + * @example + * // С указанием элемента-хэндла для перетаскивания (например, только за шапку) + * const headerRef = useRef(null); + * + *
+ *
Header (drag me!)
+ *
Content
+ *
+ *
*/ export default function DraggableContainer({ children, enabled = true, draggable = true, + dragHandleRef, enableSnapping = false, autoAlign = false, constrainToBounds = false, @@ -204,6 +217,17 @@ export default function DraggableContainer({ const [isDragging, setIsDragging] = useState(false); const [dragStartAlignment, setDragStartAlignment] = useState(""); + // Проверяет, что событие началось на элементе-хэндле или его потомках + const isEventOnDragHandle = (target: EventTarget | null): boolean => { + if (!dragHandleRef?.current || !target) return true; // Если хэндл не указан, разрешаем перетаскивание откуда угодно + + const element = target as Node; + return ( + dragHandleRef.current === element || + dragHandleRef.current.contains(element) + ); + }; + const getAlignmentClassesFromPosition = (pos: Position): string => { if (!autoAlign) return ""; @@ -281,11 +305,13 @@ export default function DraggableContainer({ const handleMouseDown = (e: React.MouseEvent) => { if (!draggable) return; + if (!isEventOnDragHandle(e.target)) return; startDrag(e.clientX, e.clientY); }; const handleTouchStart = (e: React.TouchEvent) => { if (!draggable) return; + if (!isEventOnDragHandle(e.target)) return; if (e.touches.length > 0) { startDrag(e.touches[0].clientX, e.touches[0].clientY); } @@ -411,6 +437,22 @@ export default function DraggableContainer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDragging]); + // Устанавливаем cursor стили на элемент-хэндл + useEffect(() => { + if (!draggable || !dragHandleRef?.current) return; + + const handleElement = dragHandleRef.current; + handleElement.style.cursor = isDragging ? "grabbing" : "grab"; + handleElement.style.userSelect = "none"; + handleElement.style.touchAction = "none"; + + return () => { + handleElement.style.cursor = ""; + handleElement.style.userSelect = ""; + handleElement.style.touchAction = ""; + }; + }, [draggable, dragHandleRef, isDragging]); + const getContainerStyle = (): React.CSSProperties => { const style: React.CSSProperties = { position: "fixed", @@ -452,10 +494,12 @@ export default function DraggableContainer({
; } -function PopupHeader({ title, leftButton }: PopupHeaderProps) { +function PopupHeader({ title, leftButton, ref }: PopupHeaderProps) { const { setPopup } = usePopupStore(); return ( -
+
{leftButton}
{title && (

{title}

diff --git a/client/src/components/PopupWrapper.tsx b/client/src/components/PopupWrapper.tsx index 5db65a0..d6a9f57 100644 --- a/client/src/components/PopupWrapper.tsx +++ b/client/src/components/PopupWrapper.tsx @@ -7,6 +7,7 @@ interface PopupWrapperProps { className?: string; title?: string; leftButton?: React.ReactNode; + headerRef?: React.RefObject; } function PopupWrapper({ @@ -14,6 +15,7 @@ function PopupWrapper({ className, title, leftButton, + headerRef, }: PopupWrapperProps) { return (
- +
{children}
); diff --git a/client/src/components/popups/ChatPopup.tsx b/client/src/components/popups/ChatPopup.tsx index 49da40d..de918dd 100644 --- a/client/src/components/popups/ChatPopup.tsx +++ b/client/src/components/popups/ChatPopup.tsx @@ -7,6 +7,7 @@ import PopupWrapper from "../PopupWrapper"; import DraggableContainer from "../DraggableContainer"; export default function ChatPopup() { + const headerRef = useRef(null); const [messages, setMessages] = useState([ { senderId: "1", @@ -38,8 +39,9 @@ export default function ChatPopup() { centerVertical constrainToBounds initialPosition={{ right: "5vw" }} + dragHandleRef={headerRef} > - +
diff --git a/client/src/components/popups/ParticipantsPopup.tsx b/client/src/components/popups/ParticipantsPopup.tsx index 3dedab6..ceabff3 100644 --- a/client/src/components/popups/ParticipantsPopup.tsx +++ b/client/src/components/popups/ParticipantsPopup.tsx @@ -14,6 +14,7 @@ import DraggableContainer from "../DraggableContainer"; export default function ParticipantsPopup() { const participants = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const headerRef = useRef(null); return ( - +
{participants.map((participant, index) => ( diff --git a/client/src/components/popups/QRCodePopup.tsx b/client/src/components/popups/QRCodePopup.tsx index 8b9b5ca..0f41cbd 100644 --- a/client/src/components/popups/QRCodePopup.tsx +++ b/client/src/components/popups/QRCodePopup.tsx @@ -5,6 +5,7 @@ import usePopupStore from "../../store/popupStore"; import SharePopup from "./SharePopup"; import QRCode from "react-qr-code"; import DraggableContainer from "../DraggableContainer"; +import { useRef } from "react"; interface QRCodePopupProps { link: string; @@ -12,6 +13,7 @@ interface QRCodePopupProps { function QRCodePopup({ link }: QRCodePopupProps) { const { setPopup } = usePopupStore(); + const headerRef = useRef(null); return ( (null); function handleShare() { if (navigator.share) { @@ -28,9 +30,11 @@ function SharePopup({ link }: { link: string }) { centerVertical constrainToBounds initialPosition={{ right: "5vw" }} + dragHandleRef={headerRef} >