`<wa-number-input>`のステッパーボタンに`beforeinput`イベントを追加
<wa-number-input>のステッパーボタン操作時にbeforeinputイベントが発火されるようになり、event.preventDefault()で値の変更をキャンセルできるようになりました。これによりネイティブの<input type="number">との動作が揃います。
背景
<wa-number-input>はキーボード入力や矢印キー操作ではbeforeinputイベントを発火していましたが、ステッパーボタン(increment/decrementボタン)のクリック時には発火していませんでした。#2296にて報告されたこのバグでは、ネイティブの<input type="number">がステッパー操作時にもbeforeinputを発火することが確認されており、カスタムコンポーネントとの挙動の乖離が問題視されていました。
イベントが発火しないため、ステッパー経由の値変更をアプリケーション側でキャンセルする手段がなく、バリデーションや条件付き入力制御の実装に支障をきたしていました。
技術的な変更
変更の核心はnumber-input.tsのhandleStepperPointerUpメソッドへの数行の追加です。ステッパーの実際の値変更処理が行われる前にbeforeinputイベントをディスパッチし、キャンセルされた場合は処理を中断します。
変更前:
private handleStepperPointerUp(direction: 'up' | 'down', event: PointerEvent) {
if (this.disabled || this.readonly) return;
if (direction === 'up') {
this.input.stepUp();
} else {
変更後:
private handleStepperPointerUp(direction: 'up' | 'down', event: PointerEvent) {
if (this.disabled || this.readonly) return;
const beforeInputEvent = new InputEvent('beforeinput', { bubbles: true, cancelable: true, composed: true });
this.dispatchEvent(beforeInputEvent);
if (beforeInputEvent.defaultPrevented) return;
if (direction === 'up') {
this.input.stepUp();
} else {
InputEventの初期化オプションとしてcancelable: trueとcomposed: trueが指定されています。cancelable: trueによりpreventDefault()が有効になり、composed: trueによりShadow DOMの境界を越えてホスト要素のリスナーまでイベントが伝播します。
テストはnumber-input.test.tsに4ケースが追加されています。increment/decrementそれぞれでのイベント発火確認、preventDefault()による値変更のキャンセル確認、キャンセル時にinputおよびchangeイベントが発火しないことの確認が含まれており、イベントのライフサイクル全体をカバーしています。
設計判断
イベントの生成にEventではなく InputEvent が採用されました。beforeinputはHTMLの仕様上InputEventのインスタンスであるため、event instanceof InputEventによるチェックが正しく動作し、inputTypeプロパティへのアクセスも可能です。ネイティブ要素との型レベルの互換性を保つ選択といえます。
また、イベントのディスパッチをstepUp()/stepDown()の呼び出し前に置いていることで、「変更前にキャンセル可能」というネイティブのbeforeinputセマンティクスが正確に再現されています。既存のdisabled/readonlyチェックの後にイベントを発火させることで、それらの状態ではbeforeinputが発火しない動作も自然に実現されています。
まとめ
本PRは最小限のコード追加でネイティブ<input type="number">との動作互換を回復した変更です。composed: trueによるShadow DOM対応とInputEvent型の採用により、ウェブ標準に沿った形でステッパー操作のキャンセル機能を実現しています。