はじめに
tailwindcssフレームワークをベースにしたUIコンポーネントのPrelineを使用してモーダル表示を実装したのですが、表示されなかった原因とその解決策をまとめました。
これ解決に2日以上、かかりました。
問題
ボタン押下してもモーダルが表示されない。
★モーダル呼び出し側のコード
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-overlay
とaria-expanded
を監視し、ボタン押下の状態によってモーダルの開閉を制御する
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で初期化を行う必要があります。
※公式ドキュメントのインストール手順に明記されています
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の初期化をすることで解決することができました✨
...
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
などの説明はこちら