キャンバス要素だけにonWheelを付けると、隣接の固定UI上でブラウザのネイティブピンチズーム/elastic overscrollが発火する
パン/ズームできるキャンバスUI(マインドマップ・ホワイトボード等)で、キャンバス本体(例: .canvas要素)にだけonWheel/preventDefault()を付ける実装は、カーソルがキャンバスの外(同じ画面上の固定ボタン・FAB・fixedヘッダー等)にある間のwheelイベントを取りこぼせない。その領域ではpreventDefaultされないwheelイベントがブラウザの既定動作(トラックパッドのピンチページズーム、二本指スクロールのelastic overscroll)に流れ、「ページ全体が拡大/バウンドする」ように見えるバグになる。固定ボタン自体が動いたわけではなく、ページ全体がブラウザネイティブズームされて見えるのが真相なので、「固定UIが一緒に動いた」と誤診断しやすい(transformのcontaining block問題と見た目の症状が似ているが原因は別)。
適用条件: フルビューポートのキャンバスUIで、キャンバス本体の上だけではない、同一画面内の任意の場所(固定ボタンの上も含む)でブラウザのネイティブピンチ/スクロールZOOMを完全に抑制して自前のズーム/パンに置き換えたい場合。
対処方法: (1) wheelハンドラを特定要素ではなくwindowレベルで{ passive: false }を付けて登録し、画面全体のwheelイベントを捕捉して必ずpreventDefault()する。(2) ただしモーダル・テキスト入力中の要素など、既定のスクロール/選択動作を残したい領域はevent.target.closest('.除外セレクタ')で判定して早期 returnし、preventDefaultしないことで既定動作を温存する。(3) SafariはpinchをctrlKey付きwheelではなくgesturestart/gesturechange/gestureendとして合成するため、これらもdocumentレベルでpreventDefaultする。(4) 補完としてhtml, bodyにoverscroll-behavior: noneを付与し、wheelハンドラが取りこぼしたなかった分のelasticバウンドも押さえる。
確認方法: 固定ボタンの中心座標を直接指定したInput.dispatchMouseEvent(type:'mouseWheel')(CDP経由)を発火させ、キャンバスのZOOM値が変化しつつ固定ボタン自身のgetBoundingClientRect()/boundingBox()が一切変化しないことを確認すれば、「ページズームではなくキャンバスズームが起きている」ことを実証できる。除外対象(モーダル内コンテンツ等)はscrollTopが実際に変化することで既定動作の温存を確認できる。テストスクリプトではモーダルスライドイントランジション完了後に座標を取得しないと、途中の座標をつかむことになりテストが偽陰性に失敗する。
教訓: 「キャンバス本体にpreventDefaultを付ければ十分」という直感は、パン/ズーム対象のキャンバスと同じ画面に固定UIが共存する限り成り立たない。ハンドラのスコープは、キャンバスの矩形ではなく「カーソルがこの操作領域内にあればどこでもキャンバス操作として扱う」という意味合いで考える必要がある。