fix(study): 펜/지우개 버튼 focus zoom — mousedown/pointerdown 단계 차단

사용자 보고: "펜이나 지우개를 누르면 자동으로 해당 부분 확대". iOS Safari 의 button
focus 가 mousedown/pointerdown 단계에 발동 → 그 영역으로 자동 zoom in. click 시점의
clickThenBlur 는 이미 늦음 (focus 잡힌 후 blur 시켜도 zoom 유지).

Fix: 모든 toolbar / header button 에 onmousedown={preventDefault} +
onpointerdown={preventDefault} 추가. focus 자체가 안 잡혀서 zoom trigger 없음.
click 이벤트는 별도라 onclick 정상 작동. clickThenBlur 는 잔존 케이스 2차 안전망으로 유지.

대상 buttons:
- HandwriteCanvas toolbar: 펜 / 지우개 / 가늘게/보통/굵게 / Undo/Redo/Trash / PNG 저장
- [id]/+page 헤더: 패널 토글 / 다음 시도

IconButton.svelte Props 에 onmousedown/onpointerdown prop 명시 추가 (기존
{...rest} spread 가 button element 로 전달은 됐지만 TypeScript caller 측 type
narrow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-27 13:14:01 +09:00
parent 39f1b0d124
commit b45091c8cb
3 changed files with 30 additions and 5 deletions
@@ -642,7 +642,14 @@
'touch-action: manipulation; user-select: none; -webkit-user-select: none; ' +
'-webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;';
// button click 직후 즉시 blur — focus 시 발생하는 자동 zoom in 차단.
// iOS Safari 의 button focus 는 mousedown/pointerdown 단계에 발동 → 그 영역으로
// 자동 zoom (사용자 보고: "펜/지우개 누르면 해당 부분 확대"). click 시점의
// clickThenBlur 는 이미 늦음 — focus 가 잡히는 시점이 mousedown 이라 그 단계에서
// preventDefault 해야 focus 자체가 안 잡힘. click 이벤트는 별도라 onclick 정상.
function blockBtnFocus(e: Event) { e.preventDefault(); }
// (clickThenBlur 는 잔존 케이스 안전망으로 유지 — focus 가 어떻게든 잡힌
// 케이스에서 즉시 blur. mousedown 차단이 1차, blur 가 2차.)
function clickThenBlur(fn: () => void) {
return (e: Event) => {
const t = e?.currentTarget as HTMLElement | null;
@@ -660,6 +667,8 @@
>
<button
type="button"
onmousedown={blockBtnFocus}
onpointerdown={blockBtnFocus}
onclick={clickThenBlur(() => (tool = 'pen'))}
style={BTN_STYLE}
tabindex={-1}
@@ -671,6 +680,8 @@
</button>
<button
type="button"
onmousedown={blockBtnFocus}
onpointerdown={blockBtnFocus}
onclick={clickThenBlur(() => (tool = 'eraser'))}
style={BTN_STYLE}
tabindex={-1}
@@ -686,6 +697,8 @@
{#each [['thin', '가늘게'], ['normal', '보통'], ['thick', '굵게']] as [w, label]}
<button
type="button"
onmousedown={blockBtnFocus}
onpointerdown={blockBtnFocus}
onclick={clickThenBlur(() => (widthMode = w))}
style={BTN_STYLE}
tabindex={-1}
@@ -697,9 +710,9 @@
<span class="w-px h-5 bg-default mx-1" aria-hidden></span>
<IconButton icon={Undo2} size="sm" aria-label="실행 취소" onclick={clickThenBlur(undo)} disabled={strokes.length === 0} style={BTN_STYLE} tabindex={-1} />
<IconButton icon={Redo2} size="sm" aria-label="다시 실행" onclick={clickThenBlur(redo)} disabled={undoStack.length === 0} style={BTN_STYLE} tabindex={-1} />
<IconButton icon={Trash2} size="sm" aria-label="모두 지우기" onclick={clickThenBlur(clearAll)} disabled={strokes.length === 0} style={BTN_STYLE} tabindex={-1} />
<IconButton icon={Undo2} size="sm" aria-label="실행 취소" onclick={clickThenBlur(undo)} disabled={strokes.length === 0} style={BTN_STYLE} tabindex={-1} onmousedown={blockBtnFocus} onpointerdown={blockBtnFocus} />
<IconButton icon={Redo2} size="sm" aria-label="다시 실행" onclick={clickThenBlur(redo)} disabled={undoStack.length === 0} style={BTN_STYLE} tabindex={-1} onmousedown={blockBtnFocus} onpointerdown={blockBtnFocus} />
<IconButton icon={Trash2} size="sm" aria-label="모두 지우기" onclick={clickThenBlur(clearAll)} disabled={strokes.length === 0} style={BTN_STYLE} tabindex={-1} onmousedown={blockBtnFocus} onpointerdown={blockBtnFocus} />
<span class="text-xs text-dim ml-2">stroke {strokes.length}</span>
<div class="flex-1"></div>
@@ -708,6 +721,8 @@
{/if}
<button
type="button"
onmousedown={blockBtnFocus}
onpointerdown={blockBtnFocus}
onclick={clickThenBlur(takeSnapshot)}
disabled={snapshotting || strokes.length === 0}
style={BTN_STYLE}
@@ -21,6 +21,8 @@
href?: string;
target?: string;
onclick?: (e: MouseEvent) => void;
onmousedown?: (e: MouseEvent) => void;
onpointerdown?: (e: PointerEvent) => void;
class?: string;
}
@@ -56,7 +56,7 @@
'touch-action: manipulation; user-select: none; -webkit-user-select: none; ' +
'-webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;';
// button click 직후 즉시 blur — iPad/Chrome 의 button focus zoom 차단.
// button click 직후 즉시 blur — iPad/Chrome 의 button focus zoom 의 2차 안전망.
function clickThenBlur(fn) {
return (e) => {
const target = e?.currentTarget;
@@ -64,6 +64,10 @@
fn();
};
}
// iOS Safari 의 button focus 가 mousedown/pointerdown 단계에 발동 → 그 영역으로
// 자동 zoom (사용자 보고: "펜/지우개 누르면 해당 부분 확대"). focus 자체를 막아야
// zoom trigger 안 됨. click 이벤트는 별도라 onclick 정상 작동.
function blockBtnFocus(e) { e.preventDefault(); }
// iPadOS Apple Pencil Scribble / Apple Intelligence 가 펜 stroke 를 텍스트 선택
// 제스처로 해석해 callout 메뉴 ("복사하기 / 선택 영역 찾기 / 찾아보기 / 번역") 를
@@ -212,6 +216,8 @@
<button
type="button"
onmousedown={blockBtnFocus}
onpointerdown={blockBtnFocus}
onclick={clickThenBlur(() => (panelOpen = !panelOpen))}
style={BTN_STYLE}
tabindex={-1}
@@ -238,6 +244,8 @@
<button
type="button"
onmousedown={blockBtnFocus}
onpointerdown={blockBtnFocus}
onclick={clickThenBlur(bumpRep)}
style={BTN_STYLE}
tabindex={-1}