`wa-textarea`の`resize="auto"`が非表示状態から復元後も高さが折りたたまれたままになるバグを修正
<wa-textarea resize="auto">が初期状態で非表示のコンテナ内に配置されていた場合、表示後も高さが正しく計算されないバグが修正されました。ResizeObserverの監視対象と監視タイミングの見直しにより、幅変化および非表示→表示遷移の両方で自動リサイズが正しく機能するようになります。
背景
リグレッションにより、display: noneを持つ親要素内の<wa-textarea resize="auto">が、表示後も高さ0のままになる問題が報告されました(#2347)。
問題の根本原因は、非表示状態では内部の<textarea>のscrollHeightが0になるため、その時点で計算された高さがそのまま固定されてしまう点にあります。コンポーネントが最初のupdated()でResizeObserverを一度だけ設定し、その後resize属性が変更されてもオブザーバーを再生成しない実装になっていたため、自動リサイズのトリガーが失われていました。
また、同じ実装上の問題として、resize="vertical"などのマニュアルモードからresize="auto"に切り替えた場合も、古いオブザーバーが内部の<textarea>を監視し続けるため、幅変化による高さの再計算が機能しないリグレッションも存在していました。
技術的な変更
updateResizeObserver()メソッドが大幅に改修され、autoモードと手動リサイズモードで監視対象と処理内容を分岐するようになりました。
変更前:
// The resize observer is only needed for manual resize modes (vertical, horizontal, both)
// to sync the base wrapper dimensions with the textarea.
const needsObserver = this.resize !== 'none' && this.resize !== 'auto';
if (needsObserver && !this.resizeObserver && this.input) {
this.resizeObserver = new ResizeObserver(() => this.setTextareaDimensions());
this.resizeObserver.observe(this.input);
} else if (!needsObserver && this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = undefined;
}
変更後:
private lastObservedWidth = 0;
// ...
const needsObserver = this.resize !== 'none';
// Always tear down first.
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = undefined;
}
if (needsObserver && this.input) {
if (this.resize === 'auto') {
this.resizeObserver = new ResizeObserver(entries => {
const width = entries[0]?.contentRect.width ?? 0;
if (width !== this.lastObservedWidth) {
this.lastObservedWidth = width;
requestAnimationFrame(() => this.setTextareaDimensions());
}
});
this.resizeObserver.observe(this);
} else {
this.resizeObserver = new ResizeObserver(() => this.setTextareaDimensions());
this.resizeObserver.observe(this.input);
}
}
変更は3点に整理できます。
-
監視対象の変更:
autoモードでは内部の<textarea>ではなくホスト要素(this)を監視するようにした。非表示状態から表示状態への遷移(display: noneの解除)でコンテナの幅変化が発生するため、ホスト要素を監視することでこのイベントを捕捉できる。 -
幅変化のみへの絞り込み:
autoモードのコールバックではlastObservedWidthと比較して幅が変化した場合のみsetTextareaDimensions()を呼び出す。高さの変化は無視することで、自身の高さ変更がオブザーバーを再帰的にトリガーするループを防いでいる。 -
requestAnimationFrameによる遅延実行:setTextareaDimensions()の呼び出しを次のフレームに遅延させることで、ResizeObserverのコールバック内で同期的にレイアウトを変更することによる「ResizeObserver loop completed」警告を回避している。
さらに、updateResizeObserver()の冒頭で既存のオブザーバーを無条件に破棄してから再生成する構造に変わりました。これにより、resize属性の変更時に古いオブザーバーが残存する問題が解消されます。
設計判断
autoモードと手動モードで監視対象を分離する設計が採用されました。
手動リサイズモード(vertical・horizontal・both)では、ユーザーが内部の<textarea>をドラッグしてサイズを変更した際にホストのラッパー要素へ寸法を同期する必要があります。このため内部の<textarea>を監視することが適切です。一方、autoモードではユーザーによるドラッグは発生せず、幅の変化に応じた高さの再計算が目的です。監視対象を異なる要素にすることで、それぞれのモードで必要なイベントだけを正確に捕捉できます。
また、常にオブザーバーを破棄してから再生成する構造は、モード切り替え時に古いオブザーバーが誤った要素を監視し続けるリグレッションを構造的に排除します。「まず壊してから正しく作り直す」パターンにより、条件分岐の複雑さを減らしながら正確性を確保しています。
まとめ
本修正は、autoモードにおけるResizeObserverの監視対象を内部要素からホスト要素へ変更し、非表示→表示遷移を幅変化として捕捉できるようにした変更です。高さ変化の無視とrequestAnimationFrameによる遅延実行を組み合わせることで、再帰ループや「ResizeObserver loop」警告を回避しつつ、初期非表示のコンテナ内でも自動リサイズが正しく動作するようになります。