Skip to content

floating-ui を使った toast を作った

2023-04-30


floating-ui を使った toast を作った。

前提

floating-ui は浮動要素を扱うためのライブラリで、比較的低レイヤーな API とそれのラッパーで構成されている。
自分は React のラッパーしか触っていないが Vue や React Native のラッパーもある。React に限った話でいうと上記の API を提供する hooks やコンポーネントが提供されていて、豊富なオプションで浮動要素を扱うことができる。a11y 周りであったり、アニメーション周りであったり、tab focus であったり、ユーザーの操作に対してどう振る舞うかなどを柔軟に定義することができる。

floating-ui は浮動要素を扱うものだが現状だと anchor となる要素に対しての相対的な位置に浮動要素を配置することしかできない。(つまり今回の toast や他だと snackbar のような viewport に対する相対的な位置に配置することはできない(一部のものは対応されているが))
それについての discussion も立っている。(floating-ui#2191

モチベーション

単純に作ってみたかった。

ついでにと言っては変な表現だが、自社のデザインシステム(ingred-ui)に入れたかったという動機もある。
ingred-ui では浮動要素を扱うためのライブラリとして popperjs を使用しているが、それが少し前に floating-ui にリブランディングされるということになった。(migration guide
そのため、floating-ui を ingred-ui に入れることは決定した上で、toast をこれで作ってみようと思い手を動かしてみた。

補足 1

現状の ingred-ui にも toast はあるが、react-toast-notifications という随分前に開発の終わっているライブラリを clone して使っている。
そのため、floating-ui に移行することで ingred-ui における toast の実装を統一することができると踏んで実装した。

補足 2

実は floating-ui 本家に先にプルリクエストを作ったが(feat(react): add toast component #2279)現状の API では考慮する点が多かったので一旦保留になっている。
初めてそのリポジトリに作るプルリクエストがいきなりゼロベースの feature request だったのはちょっと無謀だったので反省している。

使い方

一番簡単な使用方法は以下。Provider で囲って toast 関数で表示したい toast を queue に入れる。

import { ToastProvider, useToast } from "@takurinton/toast";

const Component = () => {
  const { toast } = useToast();

  return (
    <button
      onClick={() =>
        // toast を queue に入れる
        toast({
          placement: "top",
          text: "Hello",
        })
      }
    >
      show toast
    </button>
  );
};

function App() {
  return (
    <ToastProvider>
      <Component />
    </ToastProvider>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

下でも紹介するが、例えば独自の toast を表示したかったら toast 関数の render というプロパティにコールバックを渡すことで実現できる。

<button
  onClick={() =>
    toast({
      placement: "top",
      render: () => {
        <div>custom toast here!!</div>;
      },
    })
  }
>
  show custom toast
</button>
1
2
3
4
5
6
7
8
9
10
11
12

機能

まだ開発中であり考慮できてない点はそれなりにあると自覚してるが現時点で使える機能について書く。

useToast

useToast は toast を表示するための hooks である。
useToast は以下の 4 つのプロパティを返す。

  • toast
  • toasts
  • close
  • closeAll

詳しくは下で説明する。

toast

toast を追加するための関数になっている。
これを呼ぶことで queue に toast が追加され、表示される。 指定できるプロパティは以下の通り。

placement

toast の位置を指定することができる。デフォルト値は buttom
指定できる値は

  • top
  • top-start
  • top-end
  • bottom
  • bottom-start
  • bottom-end

の 6 つである。

toast を viewport の上部中央に表示したい場合は以下のように指定することができる。

toast({
  placement: "top",
});
1
2
3

deley

toast が自動的に閉じるまでの時間をミリ秒で指定することができる。デフォルト値は 5000。
toast が表示されてから 500ms 経過後に自動で閉じるようにしたい場合は以下のように指定することができる。

toast({
  deley: 500,
});
1
2
3

autoClose

toast が自動的に閉じるかどうかを指定することができる。デフォルト値は true。
toast が自動で閉じないようにしたい場合は以下のように指定することができる。

toast({
  autoClose: false,
});
1
2
3

requestClose

true にすると toast を閉じることができる。
これは toast 関数を呼ぶときではなく toasts を操作する際に使用する想定。

transition

toast が表示される際のアニメーションを指定することができる。
ここは floating-ui の useTransitionStyle で指定することができる値を直接入れることができる。
デフォルトは以下のようになっている。

{
  duration: 300,
  initial: () => ({
    opacity: 0,
    maxHeight: 0,
  }),
  open: ({ side }) => ({
    opacity: 1,
    transform: {
      top: "translateY(-0.5rem)",
      right: "translateX(0.5rem)",
      bottom: "translateY(0.5rem)",
      left: "translateX(-0.5rem)",
    }[side],
  }),
  close: ({ side }) => ({
    opacity: 0,
    maxHeight: 0,
    transform: {
      top: "translateY(0.5rem)",
      right: "translateX(-0.5rem)",
      bottom: "translateY(-0.5rem)",
      left: "translateX(0.5rem)",
    }[side],
  }),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

例えば、toast が表示される際に 10 秒かけて表示したい場合は以下のように指定することができる。

toast({
  transition: {
    duration: 10000,
  },
});
1
2
3
4
5

text

toast に表示するテキストを指定することができる。render 関数が指定されている場合は text は無視される。
toast に表示するテキストを Hello にしたい場合は以下のように指定することができる。

toast({
  text: "Hello",
});
1
2
3

background

toast の背景色を指定することができる。デフォルト値は rgb(37 99 235)
toast の背景色を #ff0000 にしたい場合は以下のように指定することができる。

toast({
  background: "#ff0000",
});
1
2
3

closeable

toast の右に閉じるボタンを表示するかどうかを指定することができる。デフォルト値は false。
toast の右に閉じるボタンを表示したい場合は以下のように指定することができる。

toast({
  closeable: true,
});
1
2
3

render

toast をカスタマイズしたい時に使用することができる callback。
render 関数を指定すると、その関数の戻り値が表示される。

toast({
  render: () => <div>hello</div>,
});
1
2
3

また、render 関数は idonClose を受け取ることができる。
id は toast の id で、onClose は toast を閉じるための関数である。
toast のテキストを toast の id にして、toast を閉じるボタンを表示したい場合は以下のように書くことができる。

toast({
  render: ({ id, onClose }) => (
    <div style={{ display: "flex" }}>
      {id}
      <button onClick={onClose}>close</button>
    </div>
  ),
});
1
2
3
4
5
6
7
8

toasts

表示している toast を array で返す。

close

特定の toast を閉じるための関数。
引数に id をとる。id は render 関数または toasts から取得できる。
例えば、placement が top になっている toast のみを消したい場合、以下のようなコードを書くことができる。

const { toasts, close } = useToast();

const handleRemoveTopToast = () => {
  for (const toast of toasts) {
    if (toast.placement === "top") {
      close(toast.id);
    }
  }
};
1
2
3
4
5
6
7
8
9

closeAll

全ての toast を閉じるための関数。
引数等はなく、単純に呼ぶことで queue を空にする。

ToastProvider

ToastProvider は toast を表示するための context を提供する。
ToastProvider を使用することで、useToast を使用することができるようになる。
ToastProvider には toast 関数で渡すことができるプロパティを渡すことができる。これは共通化する時に便利で、例えば全ての toast を top に表示したい場合は以下のように書くことができる。

import { ToastProvider, useToast } from "@takurinton/toast";

const Component = () => {
  const { toast } = useToast();

  return (
    <button
      onClick={() =>
        toast({
          // ToastProvider で指定しているので placement を指定しなくても top に配置される
          text: "top",
        })
      }
    >
      show toast
    </button>
  );
};

function App() {
  return (
    // placement を指定することができる
    <ToastProvider placement="top">
      <Component />
    </ToastProvider>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

感想

floating-ui で toast を作ってみたが、位置指定以外は floating-ui の a11y や keyboard event の恩恵を受けられるのは良かった。また、floating-ui の useTransitionStyle で toast のアニメーションを指定できるのも良かった。
まだ未完成なのでもう少しブラッシュアップする必要はあり、さらに API の変更もそれなりにありそうと思いつつ、とりあえず公開してみた。
誰か使いたいみたいな人がいたら npm publish もしようかなと思っている。(会社でしか使わないならそれ用に作り直す)

他のライブラリ

参考にしたライブラリがいくつかある。
インターフェイスも内部実装もそれぞれ特性が違って面白かった。

react-toastify や sonner は Provider ではない形で toast を提供していて面白かった。
chakra-ui は Provider を提供しているが、<ChakraProvider /> に toast の記述を含めることができるようになっていた。
toast のためだけに Provider を作って呼ばせるのは一般的ではないのかもしれないとも感じた(ひとまず今回は Provider を作ったが...。)

また、react-toastify や sonner、react-hot-toast の toast 関数は引数を 2 つとっていて、第一引数にカスタムコンポーネントを、第二引数に詳細なオプションを指定できるようだった。
render 関数でカスタムコンポーネントを指定する方法は chakra-ui のものを参考にしている。