`<wa-input>` / `<wa-number-input>` の無効値をネイティブ入力と一致するよう空文字列にサニタイズ
type="number" や type="date" など特定の型を持つ <wa-input> および <wa-number-input> に無効な値を設定した際、ネイティブの <input> と同様に空文字列へ正規化されるようになりました。
背景
#2323 で報告されたこのバグは、<wa-input type="number"> に対して "abc" などの非数値文字列を設定すると、カスタム要素の value プロパティがそのまま "abc" を返し続けるという問題でした。ネイティブの <input type="number"> では同じ操作を行うと、ブラウザが値をサニタイズして "" を返すのが正しい動作です。
影響を受けていた型は以下の4種類です:
numberdatetimedatetime-local
これらはいずれも、ブラウザが仕様に基づいて無効な入力を自動的に空文字列へ正規化する型です。カスタム要素がこの挙動を再現できていなかったため、value プロパティを経由するコードとネイティブ入力を想定したコードで動作が乖離していました。
技術的な変更
修正は updated() ライフサイクルフック内に「ネイティブ入力の値を参照して内部状態を同期する」ロジックを追加することで実現しています。
input.ts の変更前:
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('value') || changedProperties.has('defaultValue')) {
this.customStates.set('blank', !this.value);
this.updateValidity();
}
}
変更後:
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('value') || changedProperties.has('defaultValue') || changedProperties.has('type')) {
// Types where the browser sanitizes invalid input to an empty string. Mirror that behavior so `value` stays
// consistent with the native input (e.g. setting "abc" on type="number" resolves to "").
const sanitizingTypes = ['number', 'date', 'time', 'datetime-local'];
if (this.input && sanitizingTypes.includes(this.type) && this.value && this.input.value !== this.value) {
this._value = this.input.value;
}
this.customStates.set('blank', !this.value);
this.updateValidity();
}
}
仕組みはシンプルです。Lit の updated() が呼ばれた時点では、Shadow DOM 内のネイティブ <input> にはすでにプロパティ経由で値が渡されており、ブラウザによるサニタイズも完了しています。そこで this.input.value(ブラウザが正規化した後の値)と this.value(カスタム要素が保持している値)を比較し、差異があれば内部フィールド this._value をネイティブ側の値で上書きします。this.value ではなく this._value を直接書き換えることで、Lit のリアクティブな再レンダリングループに入らずに値を修正できます。
<wa-number-input> にも同様のロジックが追加されています。こちらは常に数値型として扱われるため sanitizingTypes による型チェックは不要で、this.input.value !== this.value の比較のみで十分です。また、この修正に合わせて changedProperties.has('defaultValue') のチェックも追加され、HTML 属性経由で無効な初期値が指定された場合も同じサニタイズが適用されるようになりました。
設計判断
ネイティブ <input> の動作をソースオブトルースとして参照する設計が採用されました。
サニタイズロジックをカスタム要素側に独自実装する(たとえば isNaN() チェックで数値を検証するなど)のではなく、ブラウザがすでに正規化した結果を読み取るだけにとどめています。これにより、仕様に準拠したサニタイズ処理をブラウザに委譲でき、型ごとのエッジケース(数値の精度、日付フォーマットのロケール差異など)をカスタム要素が個別に考慮する必要がなくなります。
type プロパティの変更も監視対象に加えている点も注目に値します。たとえば type="text" から type="number" に動的に切り替えた際、それまで保持していた非数値の value が自動的にサニタイズされるようになります。この対称性はネイティブの <input> の挙動とも一致しています。
まとめ
この修正は、数行の条件チェックを追加するだけで「カスタム要素の値プロパティがネイティブ入力と一致する」という基本的な契約を保証したものです。サニタイズ処理をブラウザに委譲するアプローチにより、将来的に型が追加された場合でも sanitizingTypes 配列に型名を追加するだけで対応できる拡張性を確保しています。