モバイルでステッパーを操作しても仮想キーボードが開かないように修正
<wa-number-input> のステッパーボタンをタッチ操作した際に仮想キーボードが表示される問題を修正しました。pointerType を判別してタッチ時のフォーカス処理を抑制するアプローチにより、マウス操作との一貫した挙動を維持しています。
背景
モバイルデバイスで <wa-number-input> のインクリメント・デクリメントボタンをタップすると、数値入力フィールドにフォーカスが移動し、仮想キーボードが表示されてページがスクロールされるという問題がありました。ディスカッション #2033 でこの問題が報告されています。
ネイティブの <input type="number"> はステッパーボタン操作後にフォーカスを移動する仕様ですが、モバイルではこの挙動がかえってUXを損なっていました。マウスとタッチの入力デバイスを区別して処理を分岐することで、それぞれの文脈に合ったふるまいを実現しています。
技術的な変更
イベントリスナーを click から pointerup に切り替え、PointerEvent.pointerType を参照することでタッチ操作とマウス操作を判別するように変更されました。
変更前:
private handleStepperClick(direction: 'up' | 'down') {
// ...
this.input.focus();
}
private maintainFocusOnPointerDown(event: PointerEvent) {
event.preventDefault();
this.input.focus();
}
変更後:
private handleStepperPointerUp(direction: 'up' | 'down', event: PointerEvent) {
// ...
// Avoid focusing the input on touch to prevent the virtual keyboard from showing
if (event.pointerType !== 'touch') {
this.input.focus();
}
}
private handleStepperPointerDown(event: PointerEvent) {
// Avoid focusing the input on touch to prevent the virtual keyboard from showing
if (event.pointerType === 'touch') return;
event.preventDefault();
this.input.focus();
}
テンプレート側では、@click を @pointerup に置き換え、イベントオブジェクトをハンドラに渡すよう変更されています。
変更前:
@pointerdown=${this.maintainFocusOnPointerDown}
@click=${() => this.handleStepperClick('down')}
変更後:
@pointerdown=${this.handleStepperPointerDown}
@pointerup=${(event: PointerEvent) => this.handleStepperPointerUp('down', event)}
テストコードも同様に incrementButton.click() / decrementButton.click() の呼び出しを dispatchEvent(new PointerEvent('pointerup', { bubbles: true })) に置き換えており、実際のイベントフローと整合するようになっています。
設計判断
click イベントではなく pointerup を採用した点 が、この修正の核心的な設計判断です。
click イベントはポインターデバイスの種別情報を持たないため、タッチとマウスを区別できません。一方、PointerEvent は pointerType プロパティに 'touch'・'mouse'・'pen' のいずれかを持ち、入力デバイスの判別が可能です。pointerup を採用することで、既存の pointerdown ハンドラと統一されたイベントモデルに整理されています。
また、pointerdown と pointerup の両方でタッチ判定を行っている点も注目されます。pointerdown でタッチ時に event.preventDefault() をスキップすることで、ブラウザのデフォルト動作によるフォーカス移動を防ぎ、pointerup でも同様にフォーカス処理を省略することで二重の防護を実現しています。
まとめ
PointerEvent.pointerType を活用することで、タッチとマウスそれぞれに最適なフォーカス挙動を単一のコンポーネントで実現しました。イベントリスナーを click から pointerup に統一したことで、入力デバイスの文脈を意識したインタラクション設計のパターンとして参考になる変更です。