Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Floating UIのすすめ: 特徴と使い方を紹介

こんにちは。2023年にSansanに新卒として入社し、Eightでエンジニアをしている徳永です。人生初ブログとなるのでドキドキしながら書きました。

ユーザーがボタンなど特定の要素をクリックまたはタップしたときに表示される、小さなウィンドウやボックスの UI を Popover と言います。最近、Eight が自前で実装・運用している UI ライブラリに Popover を追加する機会があったのですが、自前で実装すると配置制御周りで考慮することが多く、複雑になってしまいました。
配置制御周りを管理してくれるライブラリかつ自分たちのプロダクトで使いやすそうなものを探していたところFloating UIというJavaScriptのライブラリに出会いました。

今回は、Floating UIがとても使いやすく便利だったので、その特徴と使い方について紹介していきたいと思います。

目次
Floating UIの特徴
Floating UIの使い方
実際に使ってみて
おわりに

Floating UIの特徴

Floating UIとは

Floating UIはツールチップやポップオーバー、ドロップダウンなどコンテンツを妨げることなく、UI上に浮かぶ”フローティング要素”の実装に役立つJavaScriptのライブラリです。

フローティング要素は基本的にボタンやテキストといった基準となる要素(この記事ではFloating UIのドキュメントに合わせてアンカー要素と記載します)がクリックされたことを検知し、アンカー要素の隣に表示されます。Floating UIを使用することで配置制御やユーザーの操作に対した制御を簡単に実装できるようになります。
このセクションではFloatingUIの特徴を3つ紹介します。

柔軟な配置制御

Floating UIは、フローティング要素の配置を設定することができます。
アンカー要素に対してフローティング要素をどこに配置するか、スクロールした際や画面からはみ出す場合の挙動などを細かく調整できます。これにより開発者はデザイン要件に合わせた実装を行うことができます。
ボタンを押して出てきたメニューが画面からはみ出して見えにくい・操作しにくいという経験はありませんか?Floating UIはそのような場合にどのような挙動をするかを設定することができます。
例えば、下記のような感じに画面の外にフローティング要素がはみ出した時にアンカー要素の下に移動する等を簡単に設定できます。

軽量

Floating UIは、高度にモジュール化されたアーキテクチャを採用しており、ツリーシェイク(tree shaking)することができます。これにより、利用されていないコードを容易に除外でき、結果として軽量な実行環境にすることができます。
私が業務で利用した時は、配置制御のみを利用したかったためユーザーインタラクティブな機能は提供されていない@floating-ui/react-domを採用しています。

複数フレームワークに対応

Floating UIはVanillaだけでなく、ReactやVueにも対応しています。
また、コードベースでTypeScriptが利用されているため、しっかりとした型付けがされています。

Floating UIの使い方

Floating UIの使い方を説明するにあたって、Popover (ポップオーバー)というコンポーネントを実装していきます。Popoverとは特定の要素をクリックまたはホバーした際にコンテキストメニューや詳細情報などが現れるUIパターンのことです。今回は業務で利用しているReactを使って実装していきます。

機能要件

今回のデモ実装ではミニマムに作りたいため、下記の要件を満たすことを条件として実装していきます。
表示位置の制御 アンカー要素(基準となる要素)の上部に配置します。
アンカー要素や画面の端とは衝突を避け、動的に位置を変えるようにします。
挙動の設定 アンカー要素がクリックされた時はPopoverを表示、非表示するようにします。
アンカー要素やPopover以外がクリックされた時は閉じるようにします。
アクセシビリティへの考慮 スクリーン・リーダーがアクセスできるように、関連する役割とARIA属性を設定します。
わかりやすいように完成しているものをお見せすると下記のGIFのようになります。

ではやっていきましょう!

インストール

お好きなパッケージマネージャーを利用してインストールしてください。
この記事では公式ドキュメントで記載されているnpmでのインストールを記載しています。

npm install @floating-ui/react

また、位置制御のみを行いたい場合は、@floating-ui/reactではなく@floating-ui/react-domを利用するとバンドルサイズを小さくすることができます。

枠組みの実装

Floating UIを利用する前にPopoverの表示・非表示を制御するボタンとPopoverの枠組みを作成します。

export default function Popover() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen((prev) => !prev)}>アンカー要素(ボタン)</button>

      {isOpen && (
        <div>
        フローティング要素(Popover)
        </div>
      )}
    </>
  );
}

実装された画面は以下のようなイメージです。

フローティング要素の配置を制御する

Floating UIが提供しているhooksを利用し、実際にフローティング要素の配置を制御します。配置制御はuseFloatingにて行います。そこで利用されているプロパティは下記になります。

  • middleware: フローティング要素の配置や挙動をカスタマイズしたいときに利用します。middlewareは配列となっており、今回の設定ではアンカー要素から10pxマージンをあけ、スクロールされても配置を調整して視界に入るようにしています。
  • whileElementsMounted: アンカー要素とフローティング要素がマウントされたときに呼びだされ、アンマウントされたときに呼び出されるクリーンアップ関数を返します。今回はフローティング要素の配置を自動更新するためのautoUpdateを設定しています。
  • placement: アンカー要素に対するフローティング要素の配置を設定できます。
  • refs: アンカー要素とフローティング要素の参照を設定します。setReferenceはアンカー要素、setFloatingはフローティング要素のrefとして渡してください。
  • floatingStyles: フローティング要素に適用するスタイルです。
import React, { useState } from 'react';
import {
  useFloating,
  autoUpdate,
  offset,
  flip,
  shift,
} from '@floating-ui/react';

export default function Popover() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles } = useFloating({
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
    placement: 'top',
  });

  return (
    <>
      <button
        ref={refs.setReference}
        onClick={() => setIsOpen((prev) => !prev)}
      >
        アンカー要素(ボタン)
      </button>

      {isOpen && (
        <div
          ref={refs.setFloating}
          style={floatingStyles}
        >
        フローティング要素()
        </div>
      )}
    </>
  );
}

ここまでの実装で配置を制御することができます。
画面の外にスクロールしても、フローティング要素が画面に残ることが確認できると思います。

インタラクティブな動きを実装する

配置の制御はできたのでフローティング要素外をクリックされたときにフローティング要素を非表示にする挙動の実装を行います。

先ほど利用していたuseFloatingの引数に、openとonOpenChangeを追加して、返り値としてcontextを受け取るようにしてください。

const { refs, floatingStyles, context } = useFloating({
  open: isOpen,
  onOpenChange: setIsOpen,
  middleware: [offset(10), flip(), shift()],
  whileElementsMounted: autoUpdate,
  placement: 'top',
});

次に下記のhooksを追加します。

  • useDissmiss: useFloatingから返されるcontextを利用し、ユーザーがフローティング要素外をクリックしたときやescキーを押したときにフローティング要素を閉じるよう設定できます。
  • useInteractions: インタラクティブなイベントハンドラーを制御してくれます。これによって複数の挙動を追加することができます。今回は設定していないですが、ホバーやフォーカスといったイベントも制御してくれます。
  • getReferenceProps, getFloatingProps: useInteractionsによって合成されたイベントハンドラー等をまとめています。これらにはonClickなどが含まれています。
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([
  dismiss,
]);

先ほどのhooksから返されるgetReferenceProps, getFloatingPropsをフローティング要素とアンカー要素に展開します。

  return (
    <>
      <button
        ref={refs.setReference}
        onClick={() => setIsOpen((prev) => !prev)}
        {...getReferenceProps()}
      >
        アンカー要素(ボタン)
      </button>

      {isOpen && (
        <div
          ref={refs.setFloating}
          style={floatingStyles}
          {...getFloatingProps()}
        >
        フローティング要素(Popover)
        </div>
      )}
    </>
  );

これでユーザーがフローティング要素の外部をクリックしたときやescキーを押したときにフローティング要素を非表示にできるようになりました。

アクセシビリティを考慮する

アクセシビリティを考慮するために、role属性とARIA属性を設定します。
先ほどと同じ手順でuseRoleを追加し、返り値のroleをuseInteractionsの引数の配列に追加します。

  • useRole: 指定されたroleを元にARIA属性やrole属性を返します。今回実装しているものはノンモーダルなダイアログに近しい特性を持っているため、roleにdialogを設定しています。
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'dialog' });

const { getReferenceProps, getFloatingProps } = useInteractions([
  dismiss,
  role,
]);

今回roleにdialogを追加することで、下記のARIA属性が追加されるようになります。これらによりスクリーンリーダーにポップアップが存在していることを示すことができます。

  • aria-controls: アンカー要素がPopoverコンポーネントを制御していることを示しています。
  • aria-expanded: 要素の開閉が状態を表すWAI-ARIAの属性です。Popoverコンポーネントの開閉によって値が変化します。
  • aria-haspopup: ポップアップ要素が存在していることを示しています。

以上で要件すべてを満たすことができました。
以下が全体のコードになります。

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

export default function Popover() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
    placement: 'top',
  });

  const dismiss = useDismiss(context);

  const role = useRole(context, { role: 'dialog' });

  const { getReferenceProps, getFloatingProps } = useInteractions([
    dismiss,
    role,
  ]);

  return (
    <>
      <button
        ref={refs.setReference}
        onClick={() => setIsOpen((prev) => !prev)}
        {...getReferenceProps()}
      >
        アンカー要素(ボタン)
      </button>

      {isOpen && (
        <div
          ref={refs.setFloating}
          style={floatingStyles}
          {...getFloatingProps()}
        >
        フローティング
        </div>
      )}
    </>
  );
}

実際に使ってみて

個人的な感想になりますが、とても直感的で使いやすいと感じました。
ミドルウェアでカスタマイズできる幅が広く、設定する時に選択肢を多く持てることがとてもありがたかったです。
ただし、利用する際はどこまでをFloating UIに任せるかどうかは考える必要があるかもしれません。例えば、Popoverの開閉をFloating UIが用意してくれているhooksで制御する場合、ユーザーインタラクティブな挙動なのでそれが含まれるパッケージを利用しないといけません。自前のものでも十分な場合もあるので、何が必要で何がいらないのかを利用する側で考慮する必要がありそうです。

おわりに

今回はFloating UIの特徴や使い方について書かせてもらいました。
皆さんにとっても有益な情報であったことを願ってます。
また、Eight ではエンジニアを募集中です。 ご興味があれば、エントリーお待ちしております!

open.talentio.com

© Sansan, Inc.