가상매매 그리드 배치 오류수정
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* 그리드형 n×n 레이아웃 선택 (TradingView 스타일 팝업) — 알림 목록·가상매매 공용
|
* 그리드형 n×n 레이아웃 선택 (TradingView 스타일 팝업) — 알림 목록·가상매매 공용
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
gridPresetsByGroup,
|
gridPresetsByGroup,
|
||||||
getTradeNotificationGridPreset,
|
getTradeNotificationGridPreset,
|
||||||
@@ -134,6 +135,8 @@ const LayoutIcon: React.FC<{ preset: TradeNotificationGridPreset }> = ({ preset
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const POPOVER_WIDTH = 320;
|
||||||
|
|
||||||
const GridLayoutPicker: React.FC<Props> = ({
|
const GridLayoutPicker: React.FC<Props> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -142,14 +145,46 @@ const GridLayoutPicker: React.FC<Props> = ({
|
|||||||
popoverAlign = 'right',
|
popoverAlign = 'right',
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [popoverPos, setPopoverPos] = useState<{ top: number; left: number } | null>(null);
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
const active = getTradeNotificationGridPreset(value);
|
const active = getTradeNotificationGridPreset(value);
|
||||||
const groups = [...gridPresetsByGroup().entries()].sort((a, b) => a[0] - b[0]);
|
const groups = [...gridPresetsByGroup().entries()].sort((a, b) => a[0] - b[0]);
|
||||||
|
|
||||||
|
const updatePopoverPos = useCallback(() => {
|
||||||
|
const el = rootRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const width = Math.min(POPOVER_WIDTH, window.innerWidth - 48);
|
||||||
|
const left = popoverAlign === 'left'
|
||||||
|
? rect.left
|
||||||
|
: rect.right - width;
|
||||||
|
setPopoverPos({
|
||||||
|
top: rect.bottom + 8,
|
||||||
|
left: Math.max(8, Math.min(left, window.innerWidth - width - 8)),
|
||||||
|
});
|
||||||
|
}, [popoverAlign]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setPopoverPos(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatePopoverPos();
|
||||||
|
window.addEventListener('resize', updatePopoverPos);
|
||||||
|
window.addEventListener('scroll', updatePopoverPos, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updatePopoverPos);
|
||||||
|
window.removeEventListener('scroll', updatePopoverPos, true);
|
||||||
|
};
|
||||||
|
}, [open, updatePopoverPos]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
const onDoc = (e: MouseEvent) => {
|
const onDoc = (e: MouseEvent) => {
|
||||||
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
|
const t = e.target as Node;
|
||||||
|
if (rootRef.current?.contains(t) || popoverRef.current?.contains(t)) return;
|
||||||
|
setOpen(false);
|
||||||
};
|
};
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') setOpen(false);
|
if (e.key === 'Escape') setOpen(false);
|
||||||
@@ -164,9 +199,55 @@ const GridLayoutPicker: React.FC<Props> = ({
|
|||||||
|
|
||||||
const popoverClass = [
|
const popoverClass = [
|
||||||
cls(variant, 'popover'),
|
cls(variant, 'popover'),
|
||||||
|
cls(variant, 'popover--portal'),
|
||||||
popoverAlign === 'left' ? cls(variant, 'popover--align-left') : '',
|
popoverAlign === 'left' ? cls(variant, 'popover--align-left') : '',
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
const popover = open && popoverPos ? (
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className={popoverClass}
|
||||||
|
role="dialog"
|
||||||
|
aria-label="그리드 배치"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: popoverPos.top,
|
||||||
|
left: popoverPos.left,
|
||||||
|
right: 'auto',
|
||||||
|
zIndex: 10000,
|
||||||
|
width: Math.min(POPOVER_WIDTH, window.innerWidth - 48),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className={cls(variant, 'popover-title')}>그리드 배치</p>
|
||||||
|
{groups.map(([group, presets]) => (
|
||||||
|
<div key={group} className={cls(variant, 'group')}>
|
||||||
|
<span className={cls(variant, 'group-lbl')}>{group}</span>
|
||||||
|
<div className={cls(variant, 'group-row')}>
|
||||||
|
{presets.map(preset => (
|
||||||
|
<button
|
||||||
|
key={preset.id}
|
||||||
|
type="button"
|
||||||
|
className={[
|
||||||
|
`${variant}-grid-preset-btn`,
|
||||||
|
value === preset.id ? `${variant}-grid-preset-btn--on` : '',
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
title={`${preset.label} (${preset.cols}열)`}
|
||||||
|
aria-label={`${preset.label} 배치`}
|
||||||
|
aria-pressed={value === preset.id}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(preset.id);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LayoutIcon preset={preset} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cls(variant, 'picker')} ref={rootRef}>
|
<div className={cls(variant, 'picker')} ref={rootRef}>
|
||||||
<button
|
<button
|
||||||
@@ -186,37 +267,7 @@ const GridLayoutPicker: React.FC<Props> = ({
|
|||||||
<LayoutIcon preset={active} />
|
<LayoutIcon preset={active} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
{popover && createPortal(popover, document.body)}
|
||||||
<div className={popoverClass} role="dialog" aria-label="그리드 배치">
|
|
||||||
<p className={cls(variant, 'popover-title')}>그리드 배치</p>
|
|
||||||
{groups.map(([group, presets]) => (
|
|
||||||
<div key={group} className={cls(variant, 'group')}>
|
|
||||||
<span className={cls(variant, 'group-lbl')}>{group}</span>
|
|
||||||
<div className={cls(variant, 'group-row')}>
|
|
||||||
{presets.map(preset => (
|
|
||||||
<button
|
|
||||||
key={preset.id}
|
|
||||||
type="button"
|
|
||||||
className={[
|
|
||||||
`${variant}-grid-preset-btn`,
|
|
||||||
value === preset.id ? `${variant}-grid-preset-btn--on` : '',
|
|
||||||
].filter(Boolean).join(' ')}
|
|
||||||
title={`${preset.label} (${preset.cols}열)`}
|
|
||||||
aria-label={`${preset.label} 배치`}
|
|
||||||
aria-pressed={value === preset.id}
|
|
||||||
onClick={() => {
|
|
||||||
onChange(preset.id);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayoutIcon preset={preset} />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ export const VirtualViewHeaderControls: React.FC<ViewProps> = ({
|
|||||||
<VirtualGridLayoutPicker
|
<VirtualGridLayoutPicker
|
||||||
value={gridPreset}
|
value={gridPreset}
|
||||||
onChange={onGridPresetChange}
|
onChange={onGridPresetChange}
|
||||||
disabled={!showSignalChartToggle}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showSignalChartToggle && onGlobalDisplayModeChange && (
|
{showSignalChartToggle && onGlobalDisplayModeChange && (
|
||||||
|
|||||||
Reference in New Issue
Block a user