2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactでtaliwindcssとPrelineを使ったモーダル表示が上手くいかなかったけど解決した

Posted at

はじめに

tailwindcssフレームワークをベースにしたUIコンポーネントのPrelineを使用してモーダル表示を実装したのですが、表示されなかった原因とその解決策をまとめました。
これ解決に2日以上、かかりました。

問題

ボタン押下してもモーダルが表示されない。

★モーダル呼び出し側のコード

Card.tsx
import { useLocation } from "react-router";
import { UserCardClass } from "../class/userCardClass";
import { FC, use, useCallback, useEffect, useState } from "react";
import { getCardImage, getUserCard } from "../api/supabaseFunctions";
import { styles } from "../styles";
import { YoutubeIconModal } from "../components/molecules/YoutubeIconModal";
import { MusicIconModal } from "../components/molecules/MusicIconModal";
import { useUserId } from "../contexts/UserIdContext";
import { set } from "react-hook-form";

type Props = {
  cardInfo: UserCardClass;
};
export const Card: FC<Props> = (props) => {
  const { cardInfo } = props;

  const location = useLocation();
  const [loading, setLoading] = useState<boolean>(true);
  const [userCard, setUserCard] = useState<UserCardClass>();
  const [memberImage, setMemberImage] = useState<string | ArrayBuffer | null>(
    null,
  );

  useEffect(() => {
    setUserCard(cardInfo);
    // 画像の表示
    console.log("Card image URL:", cardInfo.image);
    const data = getCardImage(cardInfo.image);
    setMemberImage(data.publicUrl);
    setLoading(false);
  }, [location.pathname]);

  if (loading) {
    console.log("Card is loading...");
    return <div>Loading...</div>;
  }
  return (
    <div className="h-115 w-auto justify-self-center">
...
            <YoutubeIconModal
              title="推し動画"
              linkText={userCard?.movie}
              modalId={userCard?.user_id + "-movie"}
            />
            <MusicIconModal
              title="推し動画"
              linkText={userCard?.music}
              modalId={userCard?.user_id + "-music"}
            />
...
    </div>
  );
};

このように、画面にカードを表示してYoutubeのアイコンボタン、音楽アイコンボタンを押下するとPreline UIのモーダルを表示し、ユーザーが共有したいYoutube, 音楽(MV)のURLを表示するというコンポーネントを作成しようとしてました。

★Youtubeアイコンモーダル側のコード

実際にtailwindcssとPreline UIのモーダルはこのように呼び出しています。
Preline UIのモーダルの使い方は下記です。

  • モーダルを表示する役割の button に属性として追加したdata-hs-overlay の値と表示するモーダルの data-hs-overlay を一致させる
  • Preline UIが data-hs-overlayaria-expanded を監視し、ボタン押下の状態によってモーダルの開閉を制御する
YoutubeIconModal.tsx
import { FC, memo } from "react";
import { Modal } from "../atom/Modal";

type Props = {
  title: string;
  linkText?: string;
  modalId: string; // modalIdは必須としました
};

export const YoutubeIconModal: FC<Props> = memo((props) => {
  const { title, linkText = "", modalId } = props;

  return (
    <>
      <button
        className="absolute right-7/8 bottom-11/12 size-5 items-center justify-start rounded-full text-red-500 focus:outline-hidden disabled:pointer-events-none disabled:opacity-50"
        aria-haspopup="dialog"
        aria-expanded="false" // Prelineがこれを制御するため、固定でOK
        aria-controls={modalId} // modalIdを使用
        data-hs-overlay={`#${modalId}`} // modalIdを使用
        onClick={() =>
          console.log(
            "DEBUG: YouTube button clicked, Preline should handle this.",
          )
        }
      >
        <svg
          className="h-6 w-6 text-red-500"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z" />
          <polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02" />
        </svg>
      </button>
      <Modal title={title} linkText={linkText} modalId={`${modalId}`} />
    </>
  );
});

そしてPreline UIはこのようにApp.tsxで初期化を行う必要があります。
※公式ドキュメントのインストール手順に明記されています

App.tsx
import { useEffect } from "react";
import "./App.css";
import { BrowserRouter, useLocation } from "react-router";
import { Router } from "./router/Router";

async function loadPreline() {
  return import("preline/dist/index.js");
}

function App() {
  const location = useLocation();
  useEffect(() => {
    const initPreline = async () => {
      console.log("DEBUG: Initializing Preline...");
      await loadPreline();

      if (
        window.HSStaticMethods &&
        typeof window.HSStaticMethods.autoInit === "function"
      ) {
        window.HSStaticMethods.autoInit();
      }
    };

    initPreline();
  }, [location.pathname]);

  return <Router />;
}

export default App;

Reactアプリケーションでは、ルート変更(location.pathnameの変更)などによって新しいコンポーネントがマウントされたり、既存のコンポーネントが更新されたりします。

この初期化がないとPreline UIは上手く動作しません。
Preline UIは「ルート変更(location.pathnameの変更)」時にDOM構造をスキャンして、JavaScriptに通知します。

そして見つかった要素に対して、Preline UIは必要なイベントリスナー(例: ボタンのクリックイベント)や、モーダルの表示/非表示を制御するための内部ロジックをアタッチします。

というわけで、初期化が必ず必要になってくるというところまで理解しました。
でも App.tsx で初期化されているから間違いなくモーダルは表示されるはず??と思ったのですが、上記コードでは表示されませんでした。

困り疲れていたところ、頭を冷やして少し考え直しました。

初期化は必ずされている(うまくいっている)

初期化されるタイミングはルート変更時

じゃあ、このモーダルが表示されるタイミングはいつだっけ??

初期化されているが、タイミングが悪いから上手くいっていないのかな?とふと思いました。
↑これが解決に繋がりました!!

解決方法

結論、ローディング中にPreline UIの初期化が走っていたと考えられます!!

ルート変更がされた時にはローディング中(...Loading)がDOMに表示されているので、Preline UIのjavascriptにモーダルを検知させることは出来ていませんでした。

下記のように、ローディング中が終了後に再度Preline UIの初期化をすることで解決することができました✨

Card.tsx
...

type Props = {
  cardInfo: UserCardClass;
};
export const Card: FC<Props> = (props) => {
  const { cardInfo } = props;

  const location = useLocation();
  const [loading, setLoading] = useState<boolean>(true);
  const [userCard, setUserCard] = useState<UserCardClass>();
  const [memberImage, setMemberImage] = useState<string | ArrayBuffer | null>(
    null,
  );

  useEffect(() => {
    setUserCard(cardInfo);
    // 画像の表示
    console.log("Card image URL:", cardInfo.image);
    const data = getCardImage(cardInfo.image);
    setMemberImage(data.publicUrl);
    setLoading(false);
  }, [location.pathname]);

  useEffect(() => {
    // Prelineの再初期化
    if (
      window.HSStaticMethods &&
      typeof window.HSStaticMethods.autoInit === "function"
    ) {
      window.HSStaticMethods.autoInit();
      console.log(`DEBUG: Re-initialized Preline UI for new elements.`);
    } else {
      console.log(`DEBUG: HSStaticMethods or autoInit not found.`);
    }
  }, [loading]);

  if (loading) {
    console.log("Card is loading...");
    return <div>Loading...</div>;
  }
  return (
    <div className="h-115 w-auto justify-self-center">
...
            <YoutubeIconModal
              title="推し動画"
              linkText={userCard?.movie}
              modalId={userCard?.user_id + "-movie"}
            />
            <MusicIconModal
              title="推し動画"
              linkText={userCard?.music}
              modalId={userCard?.user_id + "-music"}
            />
...
    </div>
  );
};

おわりに

今回の調査にあたって、Prelineの公式サイトやGithubのソースコードをきちんと確認したおかげで解決することができました。
今後は、公式ドキュメントを読み漁ってからフレームワークやライブラリを使用することを誓います。

参考

data-hs-overlay などの説明はこちら

2
1
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?