맥 os 전략지표 드래그 문제 최종 수정
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -2071,6 +2071,37 @@ body.se-palette-drag-armed .se-palette-section--scroll {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.se-palette-drag-overlay-ghost {
|
||||
position: fixed;
|
||||
z-index: 20041;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* body portal — .se-page 테마 토큰(scope)만 카드 클론에 전달 */
|
||||
.se-palette-drag-overlay-theme.se-page {
|
||||
display: inline-block;
|
||||
flex: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
background: transparent;
|
||||
color: var(--se-text);
|
||||
}
|
||||
|
||||
.se-palette-drag-overlay-ghost .se-palette-card {
|
||||
cursor: grabbing;
|
||||
transform: none;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.se-palette-drag-overlay-ghost .se-palette-card--dragging:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.se-palette-drag-overlay-label {
|
||||
position: fixed;
|
||||
z-index: 20041;
|
||||
|
||||
@@ -34,6 +34,10 @@ export interface PaletteDragOverlaySession {
|
||||
pointerId: number;
|
||||
x: number;
|
||||
y: number;
|
||||
/** 드래그 시작 카드 — overlay 에 동일 UI 클론용 */
|
||||
sourceElement: HTMLElement | null;
|
||||
grabOffsetX: number;
|
||||
grabOffsetY: number;
|
||||
}
|
||||
|
||||
type PaletteDragListener = (event: PaletteDragEvent) => void;
|
||||
@@ -183,17 +187,37 @@ function mountDragOverlay(
|
||||
payload: PaletteDragPayload,
|
||||
xy: { x: number; y: number },
|
||||
pointerId: number,
|
||||
sourceElement: HTMLElement | null,
|
||||
grabOffset: { x: number; y: number },
|
||||
) {
|
||||
activePayload = payload;
|
||||
activePointerId = pointerId;
|
||||
lastDragClientXY = { x: xy.x, y: xy.y };
|
||||
document.body.classList.add('se-palette-drag-active');
|
||||
armPaletteDragScrollLock();
|
||||
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
|
||||
overlayController?.mount({
|
||||
payload,
|
||||
pointerId,
|
||||
x: xy.x,
|
||||
y: xy.y,
|
||||
sourceElement,
|
||||
grabOffsetX: grabOffset.x,
|
||||
grabOffsetY: grabOffset.y,
|
||||
});
|
||||
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
||||
notifyPaletteDragMove(xy.x, xy.y);
|
||||
}
|
||||
|
||||
function grabOffsetFromSource(
|
||||
sourceElement: HTMLElement | null,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
): { x: number; y: number } {
|
||||
if (!sourceElement) return { x: 0, y: 0 };
|
||||
const rect = sourceElement.getBoundingClientRect();
|
||||
return { x: clientX - rect.left, y: clientY - rect.top };
|
||||
}
|
||||
|
||||
export function notifyPaletteDragMove(clientX: number, clientY: number) {
|
||||
if (!activePayload) return;
|
||||
lastDragClientXY = { x: clientX, y: clientY };
|
||||
@@ -232,12 +256,14 @@ function startPaletteDragGesture(
|
||||
startY: number,
|
||||
pointerId: number,
|
||||
payload: PaletteDragPayload,
|
||||
sourceElement: HTMLElement | null,
|
||||
): void {
|
||||
if (activePayload) cleanupDrag();
|
||||
unbindGestureListeners();
|
||||
|
||||
const origin = { x: startX, y: startY };
|
||||
mountDragOverlay(payload, origin, pointerId);
|
||||
const grabOffset = grabOffsetFromSource(sourceElement, startX, startY);
|
||||
mountDragOverlay(payload, origin, pointerId, sourceElement, grabOffset);
|
||||
|
||||
unbindGesture = bindDocumentPointerDrag(
|
||||
pointerId,
|
||||
@@ -257,7 +283,7 @@ function startPaletteDragGesture(
|
||||
export function handlePalettePointerDown(
|
||||
e: React.PointerEvent,
|
||||
payload: PaletteDragPayload,
|
||||
_label: string,
|
||||
sourceElement: HTMLElement,
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (!isPrimaryPointerButton(e)) return;
|
||||
@@ -268,13 +294,13 @@ export function handlePalettePointerDown(
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
startPaletteDragGesture(e.clientX, e.clientY, e.pointerId, payload);
|
||||
startPaletteDragGesture(e.clientX, e.clientY, e.pointerId, payload, sourceElement);
|
||||
}
|
||||
|
||||
export function handlePaletteMouseDown(
|
||||
e: React.MouseEvent,
|
||||
payload: PaletteDragPayload,
|
||||
_label: string,
|
||||
sourceElement: HTMLElement,
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (e.button !== 0) return;
|
||||
@@ -285,13 +311,13 @@ export function handlePaletteMouseDown(
|
||||
if (typeof window !== 'undefined' && 'PointerEvent' in window) return;
|
||||
|
||||
e.preventDefault();
|
||||
startPaletteDragGesture(e.clientX, e.clientY, 1, payload);
|
||||
startPaletteDragGesture(e.clientX, e.clientY, 1, payload, sourceElement);
|
||||
}
|
||||
|
||||
export function handlePaletteTouchStart(
|
||||
e: React.TouchEvent,
|
||||
payload: PaletteDragPayload,
|
||||
_label: string,
|
||||
sourceElement: HTMLElement,
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (typeof window !== 'undefined' && 'PointerEvent' in window) return;
|
||||
@@ -300,7 +326,7 @@ export function handlePaletteTouchStart(
|
||||
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload);
|
||||
startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload, sourceElement);
|
||||
}
|
||||
|
||||
/* ── Web HTML5 ── */
|
||||
|
||||
Reference in New Issue
Block a user