種別: Webサイト

  • マージンの折り重ね(マージンの重複)って知ってた?

    マージンの折り重ね(マージンの重複)って知ってた?

    Gutenbergのブロック開発をしていて、おかしな現象に見舞われました。次の画像をご覧ください。

    これはブロックにニューモフィズムというシャドーをつけたものです。
    上がブロックエディタの表示で、下がフロントエンドの表示です。
    ご覧のとおり下は高さが狭く、ニューモフィズムの浮き出た感じが出し切れていません。
    なぜ、こんな現象が起こるのかをブログにしたいと思います。

    この画像のHTMLとCSSについて

    まず、この画像のHTMLとCSSを示します。
    まず、HTMLです。

    <div>
         <ul>
             <li>情報入力</li>
            <li>確認</li>
            <li>処理完了</li>
        </ul>
    </div>
    

    次にCSS(SCSS)です。

    div{
        ul{
            margin: 1em 2em 1em 2em;
            padding: 1em 2em 1em 2em;
            box-shadow: 5px 5px 5px #ecd4d4,-5px -5px 5px #fcf8f8;
        }
    }
    

    簡単なコードです。div要素でul要素をラップし、ul要素にはmarginとpaddingをつけました。
    その上でbox-shadowをつければ、marginとpaddingの間にシャドーが落ちてくれると思ったわけです。
    しかし、ブロックエディタは思惑どおりでしたが、肝心のフロントエンドは変な表示になってしまいました。

    margin-topとmargin-bottomにはマージンの折り重ね(マージンの重複)という現象がある

    調べてみるとこんなことがわかってきました。
    margin-topとmargin-bottomというのは、上下に接触する要素同士においては、折り重なるという性質があります。
    次のようなコードでは、要素Aの margin-bottom と要素Bの margin-top が折り重ねられ、実際のマージンは30px(大きい方の値)となります。

    <div style="margin-bottom: 20px;">要素A</div>
    <div style="margin-top: 30px;">要素B</div>
    

    この事例は隣接する兄弟要素の場合ですが、この現象は、親要素とその最初または最後の子要素の間でも発生します。

    <div style="margin-bottom: 20px;">
        要素A
        <div style="margin-top: 30px;">要素B</div>
    </div>
    

    この図のように子要素の上側・下側のマージンは親要素の外に「押し出される」ことになり、親要素の縦幅を広げるわけではないのです。
    これが「マージンの折り重ね(マージンの重複)」です。

    親要素に border や padding が付いているとこの現象は起こらない

    では、なぜブロックエディタの方は、期待どおりの動きをしてくれるのかということです。
    実はGutenbergのブロック開発ではブロックエディタ側には、ブロックに1pxの破線のボーダーが付いています。
    これのおかげで子要素のマージンは親要素のボーダーの内側に回るのです。
    コードと図解は次のようになります。

    <div style="
        margin-bottom: 20px;
        border: 1px dotted #f00;
    ">
        要素A
        <div style="margin-top: 30px;">要素B</div>
    </div>
    

    これは親要素にパディングがある場合でも同じです。
    その他にも「マージンの折り重ね」が起きない場合があるので、箇条書きにしてまとめます。

    1. borderやpaddingの追加:親要素に少なくとも1pxの border や padding がある。
    2. overflowプロパティの使用:親要素に overflow: auto または overflow: hidden を設定する。
    3. flexboxやgridの使用:FlexboxやGridレイアウトを使用する。

    幅(margin-left および margin-right)についてはどうなのか

    ちなみに、水平方向のマージン(margin-left および margin-right)には、マージンの折り重ね(マージンの重複)という現象は存在しません。
    そもそも、水平方向のマージンというのは、要素を親要素のどの位置に配置されるかを決定するためのもので、親要素の全体の幅そのものを広げるという効果はありません。
    仮に、水平方向のマージン(margin-left および margin-right)が自らの幅とあわせて、親要素の幅を超えるとオーバーフローを起こします。

    親要素の幅に影響を与えるのは子要素のwidthやpadding, borderなどのプロパティです。子要素の幅やパディング、ボーダーが親要素の利用可能な幅を超える場合、通常、親要素は子要素を収容するために拡張されます。
    ただし、親要素に固定の幅が設定されている、あるいは他のスタイルが適用されている場合はそうならないこともあります。

    まとめ

    CSSの基本中の基本とも言えるmarginプロパティですが、実はこんなに奥が深かったということを今更のように知りました。

    皆さんはいかがでしょう。

    もし、私が経験したような現象でお悩みの方がいれば、是非参考にしていただきたいと思います。

  • セットしたはずのグラデーションが表示されない?!

    セットしたはずのグラデーションが表示されない?!

    まずはPanelColorGradientSettingsを使ってみよう

    ブロック開発でカスタムブロックに色を設定するのは基本中の基本ですよね。
    まず、最初のカスタマイズは色の設定でしょう。
    WordPressはそのための入力用コントロールとしてPanelColorGradientSettingsというコントロールを用意してくれています。
    これが優れもので、中間色はもちろんのこと、グラデーションまでセットできます。
    これを使うようになってグラデーションを採用することが増えました。

    このコントロールを使って、単色でもグラデーションでも選択できるようにするには、次のような手順を踏みます。

    1. 色情報を格納するためにblock.json内にattributesを設定する。
    2. サイドバーにPanelColorGradientSettingsを設置する。
    3. ユーザーが選択した色をbackGroundスタイルにセットする。

    具体的なコードで見てみましょう。
    まず、block.jsonにこんなふうに入れます

    "attributes": {
        "bg_Color": {
            "type": "string",
            "default": "#504237"
        },
        "bg_Gradient": {
            "type": "string",
        }
    }
    

    attributesには初期値も設定できるんです。ですから、このように設定すると、最初にブロックがマウントされてattributesが呼び出されるとbg_Colorには#504237がセットされた状態になるのです。便利なので重宝します。

    つぎにPanelColorGradientSettingsの設置です。
    edit.jsのreturn文にInspectorControlsを用意してその中に次のコードを入力してやります。

    <PanelColorGradientSettings
        title={__("Background Color Setting","text-domain")}
        settings={[
            {
                colorValue: bg_Color,
                gradientValue: bg_Gradient,
    
                label: __("Choice color or gradient","text-domain"),
                onColorChange: (newValue) => {
                    setAttributes({ bg_Color: newValue });
                },
                onGradientChange: (newValue) => {
                    setAttributes({ bg_Gradient: newValue });
                },
            }
        ]}
    />
    

    これで取得したプロパティ値をDOM要素のstyle属性としてセットすればあっという間にインラインスタイルが出来上がります。
    次のような感じです。

    const bgColor = bg_Color || bg_Gradient;
    return(
        <div style={{ backGround: bgColor }}></div>
    )
    

    なんとも小気味よくセットできるのです。

    グラデーションが表示されない

    しかし、ふと見るとこんな現象が起こっています。

    お判りでしょうか。サイドバーには確かに選択したグラデーションが表示されているのに、レンダリングされたブロックは単色です。
    この色はデフォルトで設定した色です。
    なぜ、こんなことが起こるのか全く分かりません。

    そしてさらにsave.jsに

    <div style={{ background: bgColor }}></div>
    

    などとするとBlock validation failedというエラーを起こします。これはエディタでブロックが保存された状態と、それをレンダリングしようとする状態とで一致しない場合に発生します。リカバリを試みることはできますが、毎回そんなことをしているわけにはいかないし、実際リカバリもできもせん。
    途方に暮れてしまいました。

    原因の解明

    何とか原因が究明できましたので、ここでそれを解説します。

    PanelColorGradientSettingsは単色を選択するとその色のコードを、グラデーションを選択するとlinear-gradientまたはradial-gradientを返します。そして選択しなかった方はundefinedを返すのです。
    つまり、単色が選択されると、bg_Colorには色コードがセットされますが、グラデーションが選択されるとundefinedがセットされます。
    この仕組みが元凶でした。
    もう少し具体的に説明します。
    グラデーションを選択したとします。
    セットしたときはbg_Colorにundefinedがセットされ、bg_Gradientには、linear-gradientまたはradial-gradientがセットされます。
    そして、

    const bgColor = bg_Color || bg_Gradient;
    

    が実行されれば、無事にbgColorにはグラデーションのスタイルがセットされ、ブロックにはグラデーションがレンダリングされます。
    しかし、その状態でブロックが保存され、次にそのブロックがマウントされるとどうなるでしょうか。
    ここで、あの重宝していた初期値の設定が仇になります。
    bg_Colorはundefinedのままでいてくれません。attributesで設定した初期値である#504237がセットされるのです。そしてconst bgColor = bg_Color || bg_Gradient;が実行されるとbgColorには bg_Colorがセットされてしまって bg_Gradientは無視されてしまいます。
    これが、奇妙な現象の原因でした。

    対策を講じたコード

    そしてようやく結論です。

    <PanelColorGradientSettings
        title={__("Background Color Setting")}
        settings={[
            {
                colorValue: bg_Color,
                gradientValue: bg_Gradient,
    
                label: __("Choice color or gradient"),
                onColorChange: (newValue) => {
                    setAttributes({ bg_Color: newValue === undefined ? '' : newValue });
                },
                onGradientChange: (newValue) => {
                    setAttributes({ bg_Gradient: newValue });
                },
            }
        ]}
    />
    

    おわかりでしょうか。onColorChangeのコールバック関数のsetAttributesでセットする値をnewValueがundefinedの場合、つまり、選択されなかった場合は空文字にするのです。undefinedでなければ、初期値のセットは行われません。
    そして空文字はfalsy values(falseと評価される値)なのでconst bgColor = bg_Color || bg_Gradient;ではbg_Gradientが選択されるというわけです。

    前にattributesに色を設定するときは、初期値の設定には注意しないといけないというようなことを聞いた記憶があるのですが、その時はその意味も理解せずスルーしていました。
    しかし、こんなしっぺ返しを食らうとは思ってもみませんでした。

  • TextControlに日本語入力ができなくなった!?

    TextControlに日本語入力ができなくなった!?

    どんな状態になったか

    いつものようにブロック開発をしていました。何気なく以前作ったブロックに文字入れをしようと思ってTextControlに日本語を入れようと思うと次の画像のような現象が起きました。

    この画像でおわかりでしょうか?
    この画像はブロックエディタのサイドバーです。
    母音はローマ字変換されているけど子音は母音の入力を待たず入力が確定しているという状態です。これまで見たことがない怪現象です。
    IMEの設定がおかしくなったかと思い確認しましたが特に問題はありません。そもそも他のTextControlではこんな現象は起きていません。
    よくみると漢字変換もできない状態です。つまり、一文字入力するごとにEnterキーを押したような状態です。
    TextControlに何か変なPropsでも渡したかなと思って確認しても特に何もありません。
    こんな時はChatGPTかと思って現象を説明しましたしたがReactの非同期処理の問題というような回答でイマイチ的を得ていませんでした。

    TextControlのレンダリングコード

    TextControlをレンダリングしているコードは次のようになっています。

    <TextControl
        label="コピーテキスト"
        labelPosition="top"
        value={optionStyle.copy_content}
        onChange={(newValue) => {
            setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));
        }}
    />
    

    上記のコードのonChange={(newValue) => {setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));でこのブロックコンポーネントが持つoptionStyleオブジェクト内のcopy_content オブジェクトの値を書き換えています。
    そして、

    const [localOptionStyle, setLocalOptionStyle] = useState(optionStyle);
    
    // localOptionStyle の変更があるたびに setAttributes を呼び出す
    useEffect(() => {
        setAttributes({ optionStyle: localOptionStyle });
    }, [localOptionStyle]);
    

    としてあって、setLocalOptionStyleでlocalOptionStyleに変更があれば、optionStyleというブロックの属性が書き換わるという仕組みです。
    特別特殊なことは何もしてないし、何か問題が起こるなどとは全く思っていませんでした。
    ところが、このコードには重大な欠陥があるのです。おわかりでしょうか?

    TextControlのonChangeは一文字入力ごとに発火する

    ヒントはこの表題の中にあります。
    答えに至るポイントは

    value={optionStyle.copy_content}
    

    にありました。
    どうしても

    onChange={(newValue) => {
        setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));
    }}
    

    の方に目が行きがちでChatGPTもonChangeでなくonBlurで処理すれば、入力の確定を遅らせることができるなんていう説明をしていました。
    しかし、そこではないのです。

    TextControlに表示されている文字列はvalueに渡された値です。そして、次のようなコードがより一般的だと思います。

    <TextControl
        value={copyInputValue}
        onChange={(newValue) => {
            setCopyInputValue(newValue);
        }}
    />
    

    このコードと問題のコードの違いがおわかりでしょうか。
    そう、一般的なコードはvaluenewValueが何も加工されずに渡っています。ところが、問題のコードはvalue{ optionStyle.copy_content }というnewValueを加工したものを表示させています。
    そうするとTextControlは入力途中で未返還の状態を維持することができず、確定した文字列を表示してしまうのです。
    「TextControlのonChangeは一文字入力ごとに発火する」ということを意識していれば、そのたびにsetLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));が働いて一文字一文字確定させていくんだというイメージが湧いたのではないでしょうか。
    たとえば次のようなコードならわかりやすいでしょう。

    <TextControl
        value={copyInputValue}
        onChange={(newValue) => {
            setCopyInputValue(`${newValue}.`);
        }}
    />
    

    こんなコードを書くことはないでしょうが、テンプレートリテラルでnewValueに’.’をつけるような加工をしています。これがTextControl内にレンダリングされたら、その後は日本語変換はできなくなるだろうなということは想像がつくと思います。

    それでどうやって問題を解消するか

    この問題を解消するコードを示します。

    //TextControlの表示用変数
    const [copyInputValue, setCopyInputValue] = useState(optionStyle.copy_content);
    
    <TextControl
        label="コピーテキスト"
        labelPosition="top"
        value={copyInputValue}
        onChange={(newValue) => {
            setCopyInputValue(newValue);
            setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));
        }}
    />
    

    このコードではTextControlの表示用として状態変数を一つ用意して、それをvalueに渡しています。そうすることでTextControlの表示として加工していないnewValueを渡すことができます。setLocalOptionStyleはこれまでどおり行えばよいのです。ブロックにレンダリングされるのはそれによって書き換えられたoptionStyle.copy_contentです。それ自身は正解で、何もonBlurのイベント発生を待つ必要はありません。それを待っているとブロックへのレンダリングが遅れ、TextControlの入力内容がリアルタイムにブロックの表示に反映されなくなります。

    まとめ

    TextControlは非常に一般的で基本的なUIだと思っていましたが、他の入力コントロールと違い入力途中の状態を表示するという特殊性があります。そのため入力コントロールの表示とブロックのレンダリングが必ずしも一致しないということを意識しないといけないのかということに気付きました。もちろん、ほとんど場合は一致するので気が付きにくいです。日本語入力でなく、英数字を入力していればこんな現象は起こらず欠陥には気づきません。
    そういう意味でなかなか、良い勉強になったと思いました。
    参考にしていただけると光栄です。