2020/04/11 追記
Safari
もバージョン13.1
でResizeObserver
に対応するみたいです。
これにより、主要なブラウザはResizeObserver
に対応することとなります。
まだSafari
の13.1
はベータリリース状態ではあるものの、
今後はResizeObserver
を使うのが主流になっていきそうですね。
(ターゲットユーザー・動作環境との兼ね合いは考える必要がありますが)。
動機
Webフロントエンド開発で、たまに要素のリサイズを検知したいことがあります。
たとえば以下のようなレイアウトがあったとします。
ここで、左のメニューが閉じる場合に右側の要素(背景グレーの領域)でリサイズが発生するとします。
通常、レスポンシブなスタイルを適応していれば特に問題は無いと思いますが、都合により右側の領域の寸法をJavaScriptで取得して扱いたい場合があります(私はありました)。
そうなると右側領域の要素でresize
イベントを扱いたくなりますが、
Element
オブジェクトではresize
イベントを使うことはできません。
// 右側領域要素を取得
const rightPane = document.getElementById('right-pane');
// ※ そんなものはないため呼ばれない
rightPane.addEventListener('resize', () => {
console.log('要素のリサイズです');
});
// ※ これはある
window.addEventListener('resize', () => {
console.log('windowのリサイズです');
});
解決の手口を掴もうと足掻いていたところ、以下のような記事を見つけました。
Cross-Browser, Event-based, Element Resize Detection - Back Alley Coder
なるほど。
JavaScriptでやるならば上記記事を参考にする、もしくは上記記事の手法を実装したライブラリなどを使うのが良さそうです。
今回は上記記事の内容を参考にReactで要素のリサイズを検出してみたりしましたので、備忘録としてまとめておきます。
ゴールと方針
ゴールは、「Reactを用いてブラウザサイズが変わらない場合の要素のリサイズを検出する」ことです。取る方針は以下のとおりです。
<object>
のような、内部にWindow
オブジェクトを持ちうる要素を、検出対象となる要素の内側に配置します。
この時、対象の要素のスタイルでposition: relative
とし、<object>
はposition: absolute
で幅いっぱいに広がるようにします。
あとは<object>
内部のWindow
オブジェクトにリサイズイベントのコールバックを設定してあげます。
実装 - 非表示のobject要素を使う
サンプルの実装は以下になります。
interface Props {
onResize: (event: Event) => void;
};
const ElementResizeListener: React.FC<Props> = ({ onResize }) => {
const rafRef = useRef(0);
const objectRef: RefObject<HTMLObjectElement> = useRef(null);
const onResizeRef = useRef(onResize);
onResizeRef.current = onResize;
const _onResize = useCallback((e: Event) => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
onResizeRef.current(e);
});
}, []);
const onLoad = useCallback(() => {
const obj = objectRef.current;
if (obj && obj.contentDocument && obj.contentDocument.defaultView) {
// defaultViewはDocumentに関連付けられているWindowオブジェクトを返す
// object要素のWindowオブジェクトに対し、リサイズイベントを登録する
obj.contentDocument.defaultView.addEventListener('resize', _onResize);
}
}, []);
useEffect(() => {
// クリーンアップ処理
return () => {
const obj = objectRef.current;
if (obj && obj.contentDocument && obj.contentDocument.defaultView) {
obj.contentDocument.defaultView.removeEventListener('resize', _onResize);
}
}
}, []);
return (
<object
onLoad={onLoad}
ref={objectRef} tabIndex={-1}
type={'text/html'}
data={'about:blank'}
title={''}
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
pointerEvents: 'none',
zIndex: -1,
opacity: 0,
}}
/>
)
}
export default ElementResizeListener;
この例では<object>
のresize
イベントコールバックをprops
から渡せるような形にしていますが、<object>
の寸法をstate
に持たせても良いかと思います。
他の方法
1. resizeイベント
Window: resize event - Web APIs | MDN
Windowオブジェクトでのみ発火しうるイベントです。
Window
オブジェクトのリサイズを検知するだけで十分な場合にはこれで十分ですが、
(たとえば、左メニューは固定で右側領域のサイズを取得したい場合)要素同士でのサイズ干渉がありそれを検知したい場合にはwindow.onresize
に頼ることはできません。
普通に考えてWindow
以外の要素オブジェクトでもresize
イベントがあって然るべきな気もするのですが、もともとが「viewport
のwidth
またはheight
に変化があった際に、Window
オブジェクトでresize
イベントを発火する」というニュアンスみたいです。
https://www.w3.org/TR/cssom-view-1/#resizing-viewports
また、W3Cの定義でviewport
のwidth
またはheight
が変わる場合の例として、
e.g. as a result of the user resizing the browser Window, or changing the page zoom scale factor, or an iframe element’s dimensions are changed
としています。
おおまかに和訳すると
- ユーザーがブラウザウィンドウの大きさを変えたとき
- ページの表示倍率を変えたとき
iframe
要素の寸法が変わった時
とされています。
(例を挙げているだけなのでこれらで全てというわけではない可能性があります)。
今回紹介する方法では3.も関係してくるので頭の片隅で覚えておきたいところです。
2. イベントを用いず、定期的にサイズを取得する
setInterval()
など用いて対象となる要素のサイズを都度取得するイメージです。要素のサイズ変更を即座に反映しなくても良い場合にはこれでも良いかもしれません。
ただし、サイズに変更がない場合にも監視のための処理が走りますから、取得頻度を上げた場合にはパフォーマンスに影響が出るかもしれません。
3. ResizeObserver
ResizeObserver - Web API | MDN
あるじゃないですか!
しかし、残念ながらクロスブラウザ対応の点からはまだまだ厳しそうです。
MDNを見た所、本記事執筆時点での対応ブラウザはChromeやOperaなど一部のブラウザのみで、SafariやFirefox、Edgeでは未対応です。
[2020/04/11] 主要なブラウザでの実装はほぼ済んでいます。必要に応じて各種ブラウザの対応状況を確認しつつResizeObserverを使うのが良いと思います。
本格的にブラウザの実装が進めばこの記事に書いている技を使うまでもなくなるはずです。
ResizeObserver
についてはこの記事では仔細を語りません。存在を仄めかす程度に留めておきます。
まとめ
<object>
などの要素を用いてWindow
オブジェクトのresize
イベントを扱うのは盲点でした。
例の記事に感謝です。
ResizeObserver
が扱えれば不要になる手段かもしれませんが、しばらくはこの方法を使うことになりそうです。
ソースコードはGithubに上げてあるのでもしよければ参考にしてみてください。
GitHub