`<wa-number-input>`のステッパーボタンに`beforeinput`イベントを追加

shoelace-style/webawesome

<wa-number-input>のステッパーボタン操作時にbeforeinputイベントが発火されるようになり、event.preventDefault()で値の変更をキャンセルできるようになりました。これによりネイティブの<input type="number">との動作が揃います。

背景

<wa-number-input>はキーボード入力や矢印キー操作ではbeforeinputイベントを発火していましたが、ステッパーボタン(increment/decrementボタン)のクリック時には発火していませんでした。#2296にて報告されたこのバグでは、ネイティブの<input type="number">がステッパー操作時にもbeforeinputを発火することが確認されており、カスタムコンポーネントとの挙動の乖離が問題視されていました。

イベントが発火しないため、ステッパー経由の値変更をアプリケーション側でキャンセルする手段がなく、バリデーションや条件付き入力制御の実装に支障をきたしていました。

技術的な変更

変更の核心はnumber-input.tshandleStepperPointerUpメソッドへの数行の追加です。ステッパーの実際の値変更処理が行われる前に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: truecomposed: 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型の採用により、ウェブ標準に沿った形でステッパー操作のキャンセル機能を実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
c58e0c19

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が明確です。リード文、背景、技術的な変更、設計判断、まとめの各要素がすべて含まれており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```typescript:path/to/file.ts)とGitHubリンク記法([#2296])が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Shadow DOM、InputEvent、イベント伝播など、専門知識を持つエンジニアを対象とした適切な技術レベルで記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションとパラグラフがトピックセンテンスで始まり、1段落1トピックが守られています。段落の長さも適切で、非常に可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事に引用されているコードブロックは、提供されたDiffの内容と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「beforeinput」「InputEvent」「composed」「Shadow DOM」などの技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

イベントの発火タイミングや `composed: true` の役割など、技術的な説明は正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の内容はすべてPRのDescriptionやDiff内のコードで裏付けられており、ハルシネーション(捏造)は検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2310)および関連するIssue番号(#2296)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの内容を的確に要約しており、主題と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に記載のないバージョン情報やリリース予定などの外部知識は含まれておらず、事実に基づいた記述がされています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜発火していませんでした」「〜発火されるようになり」といった時間表現が、PRによる変更の前後関係を正確に反映しています。