맥 os 전략지표 드래그 문제 최종 수정

This commit is contained in:
Macbook
2026-06-15 10:06:52 +09:00
parent a3aca639c1
commit 0e09825cab
13 changed files with 451 additions and 48 deletions
@@ -69,17 +69,17 @@ export default function PaletteChip({
const onPointerDown = (e: React.PointerEvent) => {
if (!pointerMode) return;
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
handlePalettePointerDown(e, buildPayload(), e.currentTarget as HTMLElement);
};
const onMouseDown = (e: React.MouseEvent) => {
if (!pointerMode) return;
handlePaletteMouseDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
handlePaletteMouseDown(e, buildPayload(), e.currentTarget as HTMLElement);
};
const onTouchStart = (e: React.TouchEvent) => {
if (!pointerMode) return;
handlePaletteTouchStart(e, buildPayload(), composite ? `${label} + ${label}` : label);
handlePaletteTouchStart(e, buildPayload(), e.currentTarget as HTMLElement);
};
const handleClick = () => {
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
bindPaletteDragOverlayElement,
@@ -11,16 +11,60 @@ import {
} from '../../utils/paletteDragSession';
import { clientXYFromReact } from '../../utils/pointerDrag';
/** body portal 에서도 우측 패널과 동일한 palette 테마 토큰 적용 */
function themeScopeClassName(source: HTMLElement): string {
const page = source.closest('.se-page');
if (!page) return 'se-page';
const parts = ['se-page'];
page.classList.forEach(cls => {
if (cls.startsWith('se-page--')) parts.push(cls);
});
return parts.join(' ');
}
function mountCardClone(host: HTMLElement, source: HTMLElement): void {
host.innerHTML = '';
const rect = source.getBoundingClientRect();
const clone = source.cloneNode(true) as HTMLElement;
clone.classList.remove('se-palette-card--pointer');
clone.classList.add('se-palette-card--dragging');
clone.setAttribute('aria-hidden', 'true');
clone.style.width = `${rect.width}px`;
clone.style.boxSizing = 'border-box';
const themeScope = document.createElement('div');
themeScope.className = `se-palette-drag-overlay-theme ${themeScopeClassName(source)}`;
themeScope.appendChild(clone);
host.appendChild(themeScope);
}
function positionGhostHost(
host: HTMLElement,
x: number,
y: number,
grabOffsetX: number,
grabOffsetY: number,
): void {
host.style.left = `${x - grabOffsetX}px`;
host.style.top = `${y - grabOffsetY}px`;
}
/**
* pointer/touch 드래그 overlay (sessionRef 로 stale closure 방지, window touch 백업과 연동)
* pointer/touch 드래그 overlay — 원본 카드 DOM 클론으로 web HTML5 drag ghost 와 동일 UI
*/
export default function PaletteDragOverlay() {
const [session, setSession] = useState<PaletteDragOverlaySession | null>(null);
const sessionRef = useRef<PaletteDragOverlaySession | null>(null);
const labelRef = useRef<HTMLDivElement | null>(null);
const ghostHostRef = useRef<HTMLDivElement | null>(null);
sessionRef.current = session;
const positionFromSession = useCallback((s: PaletteDragOverlaySession) => {
const host = ghostHostRef.current;
if (!host) return;
positionGhostHost(host, s.x, s.y, s.grabOffsetX, s.grabOffsetY);
}, []);
useEffect(() => {
registerPaletteDragOverlay({
mount: next => {
@@ -37,27 +81,27 @@ export default function PaletteDragOverlay() {
return () => registerPaletteDragOverlay(null);
}, []);
useLayoutEffect(() => {
if (!session?.sourceElement || !ghostHostRef.current) return;
mountCardClone(ghostHostRef.current, session.sourceElement);
positionFromSession(session);
}, [session, positionFromSession]);
useEffect(() => {
return subscribePaletteDrag(ev => {
if (ev.phase !== 'move' && ev.phase !== 'start') return;
const label = labelRef.current;
if (!label) return;
label.style.left = `${ev.x}px`;
label.style.top = `${ev.y}px`;
const s = sessionRef.current;
const host = ghostHostRef.current;
if (!s || !host) return;
positionGhostHost(host, ev.x, ev.y, s.grabOffsetX, s.grabOffsetY);
});
}, []);
const overlayRefCallback = useCallback((el: HTMLDivElement | null) => {
bindPaletteDragOverlayElement(el);
const s = sessionRef.current;
if (el && s) {
const label = labelRef.current;
if (label) {
label.style.left = `${s.x}px`;
label.style.top = `${s.y}px`;
}
}
}, []);
if (el && s) positionFromSession(s);
}, [positionFromSession]);
const onPointerMove = useCallback((e: React.PointerEvent) => {
const s = sessionRef.current;
@@ -118,12 +162,13 @@ export default function PaletteDragOverlay() {
onTouchCancel={onTouchCancel}
>
<div
ref={labelRef}
className="se-palette-drag-overlay-label"
style={{ left: session.x, top: session.y }}
>
{session.payload.label}
</div>
ref={ghostHostRef}
className="se-palette-drag-overlay-ghost"
style={{
left: session.x - session.grabOffsetX,
top: session.y - session.grabOffsetY,
}}
/>
</div>,
document.body,
);
@@ -106,7 +106,7 @@ function SidewaysFilterChip({
value: item.id,
label: item.label,
};
handlePalettePointerDown(e, payload, item.label);
handlePalettePointerDown(e, payload, e.currentTarget as HTMLElement);
};
const onMouseDown = (e: React.MouseEvent) => {
@@ -116,7 +116,7 @@ function SidewaysFilterChip({
value: item.id,
label: item.label,
};
handlePaletteMouseDown(e, payload, item.label);
handlePaletteMouseDown(e, payload, e.currentTarget as HTMLElement);
};
const onTouchStart = (e: React.TouchEvent) => {
@@ -126,7 +126,7 @@ function SidewaysFilterChip({
value: item.id,
label: item.label,
};
handlePaletteTouchStart(e, payload, item.label);
handlePaletteTouchStart(e, payload, e.currentTarget as HTMLElement);
};
const handleClick = () => onSelect();