원인은 `sheet-pointer-input.js`의 `onPointerDown`이 `closest('.bds-sheet-cell')`를 못 찾으면 `if (!args) return`으로 .NET 호출 자체를 안 하기 때문입니다. 즉 no-data 영역 우클릭은 **PointerDown이 아예 안 들어오고, 직전 헤더 클릭 값이 그대로 남아있어서** 메뉴에 계속 보입니다.
## 수정 — `Datasheet.razor.cs` 3군데
```csharp
private bool _pointerHitCell;
private void HandleCellMouseDown(object? sender, SheetPointerEventArgs args)
{
_pointerHitCell = true; // ← 추가
LastPointerRow = args.Row;
LastPointerCol = args.Col;
...
}
// 새 핸들러
private Task<bool> HandleWindowMouseDown(MouseEventArgs e)
{
if (!_pointerHitCell) // 셀/헤더가 아닌 곳을 눌렀다 → 좌표 무효화
{
LastPointerRow = -1;
LastPointerCol = -1;
}
_pointerHitCell = false;
return Task.FromResult(false);
}
```
```csharp
private async Task AddWindowEventsAsync()
{
...
await _windowEventService.RegisterMouseEvent("mouseup", HandleWindowMouseUp);
await _windowEventService.RegisterMouseEvent("mousedown", HandleWindowMouseDown); // ← 추가
}
```
## 왜 순서가 안전한가
이벤트 순서가 `시트 요소 pointerdown` → `window mousedown` → `contextmenu` 입니다. 헤더 우클릭이면 플래그가 먼저 true가 되어 리셋을 건너뛰고, no-data 영역이면 pointerdown이 없어서 window mousedown에서 -1로 초기화된 뒤 메뉴가 열립니다. 메뉴 항목 조건(`LastPointerRow == -1 && LastPointerCol >= 0`)은 그대로 두면 됩니다.
참고로 JS에서 `if (!args)`일 때 row/col을 -1/-1로 리포트하는 방법도 있지만, 그 값이 `HandlePointerDown`까지 흘러가 `ClearSelections()`와 에디터 Accept 경로를 타므로 권장하지 않습니다.
맞습니다, 앞의 window mousedown 방식은 틀렸습니다. 메뉴 팝오버는 `_sheetContainer` 밖에 있어서 메뉴 항목을 누르는 순간 리셋이 걸리고, 재렌더로 항목이 사라져 OnClick이 안 탑니다. 상태를 클릭마다 갱신할 게 아니라 **우클릭(메뉴가 열리는 그 순간)에만 딱 한 번 기록**해야 합니다.
## 0. 되돌리기
`_pointerHitCell`, `HandleWindowMouseDown`, `RegisterMouseEvent("mousedown", ...)`, 그리고 `HandleCellMouseDown` 안의 `LastPointerRow/Col` 대입을 모두 제거하세요.
## 1. `wwwroot/js/menu-target.js`
```js
handleContextMenu(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.hasAttribute("contentEditable"))
return
e.preventDefault()
const cell = e.target.closest('.bds-sheet-cell')
const hasPos = cell && cell.dataset.row !== undefined && cell.dataset.col !== undefined
this.dotnetHelper.invokeMethodAsync(this.handlerName,
{clientX: e.clientX, clientY: e.clientY},
hasPos ? parseInt(cell.dataset.row) : -1,
hasPos ? parseInt(cell.dataset.col) : -1)
}
```
## 2. `Menu/SheetMenuTarget.razor`
```csharp
@using BlazorDatasheet.DataStructures.Geometry
[Parameter] public EventCallback<CellPosition> OnContextMenuTarget { get; set; }
[JSInvokable(nameof(TargetOnContextMenu))]
public async Task TargetOnContextMenu(MouseEventArgs obj, int row, int col)
{
if (Trigger != MenuTrigger.OnContextMenu)
return;
if (OnContextMenuTarget.HasDelegate)
await OnContextMenuTarget.InvokeAsync(new CellPosition(row, col));
await ShowMenu(MenuTrigger.OnContextMenu, obj);
}
```
## 3. `Datasheet.razor` / `Datasheet.razor.cs`
```razor
<SheetMenuTarget MenuId="SelectionMenu"
MenuData="_sheet"
OnContextMenuTarget="HandleContextMenuTarget"
Margin="0"
... 나머지 그대로 />
```
```csharp
public int LastPointerRow { get; private set; } = -1;
public int LastPointerCol { get; private set; } = -1;
private void HandleContextMenuTarget(CellPosition pos)
{
LastPointerRow = pos.row;
LastPointerCol = pos.col;
}
```
이제 값이 바뀌는 시점은 우클릭 한 번뿐입니다. 헤더 우클릭이면 `(-1, col)`, no-data 영역이면 `.bds-sheet-cell`이 없으므로 `(-1, -1)`로 들어와 항목이 숨겨지고, 메뉴 항목 클릭 시에는 아무것도 갱신되지 않아 OnClick이 정상적으로 발생합니다. 페이지 쪽 조건식(`LastPointerRow == -1 && LastPointerCol >= 0`)은 그대로 두시면 됩니다.
한 가지: 헤더의 캐럿(▾) 버튼 메뉴는 `MenuTrigger.OnClick` 경로라 이 값이 갱신되지 않습니다. 안 쓰신다면 `MenuOptions.HeaderMenuEnabled = false`로 끄는 편이 혼선이 없습니다.
카테고리 없음