Open1

@floating-ui/reactでDropdown Menuをよしなに挿入してくれるやつを作る

mikana0918@InterfaceXmikana0918@InterfaceX

@floating-ui/react自体はとても高機能でよくできたライブラリなのですが、あまりに公式ドキュメントが優しくなかったので書いておきます。Popper.jsからアップデートに苦戦している人に届け。

ドロップダウンが作れたらありとあらゆるフローティング要素のフックは似たような感じの設定を変えるだけで作れるはず。

Hooksをつくる

ドロップダウンを前提としてつくってしまった。

import {
  autoUpdate,
  flip,
  offset,
  shift,
  useClick,
  useDismiss,
  useFloating,
  useId,
  useInteractions,
  useRole,
} from '@floating-ui/react'
import { useState } from 'react'

export const useFloatingDropdownMenu = () => {
  const [isOpen, setIsOpen] = useState(false)
  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip({ fallbackAxisSideDirection: 'end' }), shift()],
    whileElementsMounted: autoUpdate,
  })
  const click = useClick(context)
  const dismiss = useDismiss(context)
  const role = useRole(context)
  const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role])
  const id = useId()

  return {
    refs,
    floatingStyles,
    context,
    getReferenceProps,
    getFloatingProps,
    id,
    isOpen,
  }
}

こういう普通のドロップダウンメニューの時

export default function SomeComponent() {
    // 中略
    const {
    refs,
    floatingStyles,
    getFloatingProps,
    getReferenceProps,
    isOpen,
    id,
    context,
    } = useFloatingDropdownMenu()
    
    return (
    // 中略
       <button
        ref={refs.setReference}
        {...getReferenceProps()}
        className="rounded-md p-4 text-white shadow-md hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
      >
        <span className="text-32">
          <RiAccountCircleFill />
        </span>
      </button>
      {isOpen && (
        <FloatingFocusManager context={context} modal={false}>
          <div
            className="w-160 rounded-md bg-white/10 p-4 text-white backdrop-blur-md"
            ref={refs.setFloating}
            style={floatingStyles}
            aria-labelledby={headingId}
            {...getFloatingProps()}
          >
            <div className="text-white-900 p-4 text-sm dark:text-white">
              <small>{t('Nickname')}</small>
              <div>{userOnDB.data.user.nickname}</div>
            </div>
            <div className="h-[1px] flex-1 bg-white/20" />
            <ul className="text-white-900 cursor-pointer py-4 text-sm dark:text-gray-200">
              <li onClick={handleClickSettings}>
                <span className="block px-4 py-2 hover:bg-gray-900/20">{t('Settings')}</span>
              </li>
            </ul>
            <ul className="text-white-900 cursor-pointer py-4 text-sm dark:text-gray-200">
              <li onClick={handleClickXPRewardCenter}>
                <span className="block px-4 py-2 hover:bg-gray-900/20">{t('XP Reward Center')}</span>
              </li>
            </ul>
            <div className="h-[1px] flex-1 bg-white/20" />
            <div className="cursor-pointer py-2">
              <a
                href="/api/auth/logout"
                className="text-white-700 block px-4 py-2 text-sm hover:bg-gray-900/20"
                onClick={handleLogout}
              >
                {t('Sign out')}
              </a>
            </div>
          </div>
        </FloatingFocusManager>
      )}
)

テーブルのアクションメニューみたいな複数要素


同じhookをループさせるがコンポーネントに細かく分けて、何度もレンダリングされるようにする必要がある。幸いなことに先のhooksでゴリっと書ける

// テーブルならtrに相当する横一列のコンポーネントを作ってしまう
function Tr(props: { user: UserDto }) {
  const {
    refs,
    floatingStyles,
    getFloatingProps,
    getReferenceProps,
    isOpen: isActionMenuOpen,
    id,
    context,
  } = useFloatingDropdownMenu()

  return (
    <tr className="border-b bg-white dark:border-gray-700 dark:bg-gray-800">
      <td className="px-6 py-4">{props.user.fullname}</td>
      <td className="px-6 py-4">{props.user.email}</td>
      <td className="px-6 py-4">{$formatDate({ date: props.user.createdAt, format: 'YYYY年MM月DD日_HH:mm' })}</td>
      <td className="flex px-6 py-4">
        <div ref={refs.setReference} {...getReferenceProps()}>
          <BsThreeDots className="hover:cursor-pointer" />
        </div>
        {isActionMenuOpen && (
          <FloatingFocusManager context={context} modal={false}>
            <div
              className="w-160 rounded-md border border-gray-50 bg-black/60 p-4 text-white backdrop-blur-md"
              ref={refs.setFloating}
              style={floatingStyles}
              aria-labelledby={id}
              {...getFloatingProps()}
            >
              <div className="cursor-pointer py-2">
                <div className="text-white-700 block px-4 py-2 text-sm hover:bg-gray-900/20">退会</div>
              </div>
            </div>
          </FloatingFocusManager>
        )}
      </td>
    </tr>
  )
}

// 中略: 使いたいコンポーネント上(クライアントコード)
 <tbody>
    {userData.users.map((user, key) => (
      <Tr key={key} user={user} />
    ))}
  </tbody>

こうするとそれぞれのtrのちゃんと真下に出してくれて便利!