この記事は Snow Monkey / unitone Advent Calendar 2024 11日目の記事です。まだまだ空きがたくさんあるので、ぜひお気軽にご参加ください!
先日の記事(line-height の計算式をアップデートしたのでその解説)で、unitone は「流体タイポグラフィ(画面幅に応じて文字サイズが変化する)時に、文字サイズの変化にあわせて line-height
も動的に変化するように」していることに触れました。
「流体タイポグラフィ」というのは画面幅に応じて文字サイズが流動的に変化することですが、文字サイズだけが変化して line-height
が変化しない場合、行間がゆるくなったりきつくなったりしてしまいますよね。なので unitone では文字サイズの変化に応じて line-height
も変化するようにしています。
この機能を実現するためには「今その要素の文字サイズが基準となる文字サイズの何倍なのか」を取得する必要があります。使用できる単位が rem
や px
だけなら話は簡単なのですが、流動的に変化するということは vw
や vh
を使うわけで、そうなると CSS だけで「今その要素の文字サイズが基準となる文字サイズの何倍なのか」を計算することができません。
なので unitone ではこの処理を JavaScript で実装しています。「流体タイポグラフィ」が適用されている要素の DOM を取得して、文字サイズの情報を取得し、基準となる文字サイズの何倍なのかを計算します。
この DOM の取得、フロントは全く問題なく普通に実装できるのですが、WordPress のエディターは React なのでひと工夫が必要でした。今日はそのことについて書きたいと思います(導入なげぇ…)
各ブロックに実装する場合
各ブロック毎に DOM を取得するのはそんなに難しくありません。例えばこんな感じで useRef()
を使えば良いです(わかりやすくするために色々省略しているのでそのままでは動かないです><)。
const { useBlockProps } from '@wordpress/components';
const { useRef, useEffect } from '@wordpress/element';
const ref = useRef();
const blockProps = useBlockProps( { ref } );
// DOM を取得して、その文字サイズが、基準となる文字サイズの何倍なのかを計算する
useEffect( () => {
// ブロック DOM を取得
const target = ref?.current;
const ownerDocument = target?.ownerDocument;
const defaultView = ownerDocument?.defaultView;
// 基準となる文字サイズを指定している要素の DOM を取得
const root = ownerDocument?.querySelector( '.editor-styles-wrapper' );
if ( ! target || ! root ) {
return;
}
// ブロックの文字サイズ(実数値)を取得
const fontSize = parseFloat(
defaultView.getComputedStyle( target ).getPropertyValue( 'font-size' )
);
// 基準となる文字サイズ(実数値)を取得
const baseFontSize = parseFloat(
defaultView.getComputedStyle( root ).getPropertyValue( 'font-size' )
);
const multiple = fontSize / baseFontSize;
// 〜あとは適当に state とかに保存したりして使う〜
}, [] );
ただ、この処理はタイポグラフィ設定を持つ全てのブロック(コアブロック含む)に持たせたいので、各ブロックごとにコードを書くことはできません。ということで、unitone では Block Filters を使って実装しています。
Block Filters を使って実装する
ブロックにも PHP と同じようにフィルターフックが用意されています。今回の場合は「今その要素の文字サイズが基準となる文字サイズの何倍なのか」を計算して DOM にその情報を属性として渡したいので、属性を追加できる editor.BlockListBlock
フィルターを使います。
フックした関数で retrun
する BlockListBlock
は、前述したコードで言う blockProps()
とほぼ同じ(と僕は認識していた)ので、ここに ref
を渡せば同じようにできるんじゃ?と考えました。こんな感じ。
import { createHigherOrderComponent } from '@wordpress/compose';
const { useRef, useEffect } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
const withFluidTypography = createHigherOrderComponent( ( BlockListBlock ) => {
return ( props ) => {
const ref = useRef();
// DOM を取得して、その文字サイズが、基準となる文字サイズの何倍なのかを計算する
useEffect( () => {
// ブロック DOM を取得
const target = ref?.current;
// 以下省略...
}, [] );
return <BlockListBlock { ...props } ref={ ref } style={ 属性を追加 } />;
};
}, 'withFluidTypography' );
addFilter(
'editor.BlockListBlock',
'unitone/fluidTypography/withFluidTypography',
withFluidTypography
);
全く取得できない〜〜!!!(TT)
v15.5.0 の段階ではもう絶対これは無理なやつだなと思って諦めていたのですが、ふと、BlockListBlock
に ref
を渡せなくても、普通に HTML になら渡せるわけだから、空のダミー要素を追加して、そいつに ref
を渡せば良いんじゃね?とひらめきました。
import { createHigherOrderComponent } from '@wordpress/compose';
const { useRef, useEffect } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
const withFluidTypography = createHigherOrderComponent( ( BlockListBlock ) => {
return ( props ) => {
const { clientId } = props;
const ref = useRef();
// DOM を取得して、その文字サイズが、基準となる文字サイズの何倍なのかを計算する
useEffect( () => {
// 空 div の DOM を取得
const dummy = ref?.current;
// ブロック DOM を取得
const target = ownerDocument?.getElementById( `block-${ clientId }` );
// 以下省略...
}, [] );
// ダミー要素は常には必要無いので、いらないときは消えるように条件を設定
const enableFluidTypography = 省略;
return (
<>
<BlockListBlock { ...props } ref={ ref } style={ 属性を追加 } />
( enableFluidTypography && (
<div ref={ ref } style={{ display: 'none' }} />
);
</>
);
};
}, 'withFluidTypography' );
addFilter(
'editor.BlockListBlock',
'unitone/fluidTypography/withFluidTypography',
withFluidTypography
);
これでいけました!
問題点として、DOM が1個本当の DOM の後ろに増えちゃうので、:last-child
みたいな CSS セレクタでスタイリングしている場合はそれが効かなくなっちゃうというのはあります…。
コアのほうで、フック経由でも DOM が取得できるようになっていれば良いのですが…。もっと良い方法をご存じの方がいたら教えてください!
コメントを残す