ReactでPopper.jsを使ってみる
Popper.js
popper.jsはツールチップやドロップダウンといったポップアップ要素の表示位置をいい感じにしてくれるライブラリです。
Reactの多くのUIフレームワークがこれに依存していて、独自のラッパーを作っています。
-
react-bootstrap (react-overlays)
-
material-ui
-
chakra-ui
公式のラッパーでも充分な感じです。
popper.jsが提供しているのはあくまで表示位置に関連する機能であり、Toolitp等の具体的なUIに関してはtippy.jsというのが紹介されています。(もちろんpopper.jsに依存)
Reactのラッパーもありますが割とスター数も少なくマイナーな感じですね。
多くの人は基本的にUIフレームワークを使うと思うので当然といえば当然ですね。
Cool
単に表示位置を計算するだけにしてはコード量が多いなと思って調べていたら、
スクロールやズーム等のUIの変化に追随してコンテンツの表示位置が更新されたり、スクロール要素の中でもいい感じにする等の配慮がされてました。ライフライクルやmodifilerによる拡張も備えていてシンプルながら結構Coolな感じです。
- 下にスクロールすると反転する例Demo
- スクロールして矢印の位置が追随しているDemo
APIもシンプルでいい感じです。
import { createPopper } from '@popperjs/core';
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
// Pass the button, the tooltip, and some options, and Popper will do the
// magic positioning for you:
const instance = createPopper(button, tooltip, {
placement: 'right',
});
// instance.destroy()
これを使うとtooltipの座標をbuttonの右側にいい感じにしてくれます。
また、createPopperはインスタンスを返します。インスタンスをdestroyするまで、popperは生き続けます。(内部でイベントリスナーが登録されており、スクロールやズーム等のUIに変更があっても座標をいい感じにしてくれる)
Reactで使う場合はuseEffect
の中でインスタンスを生成したり戻り地の関数の中で破棄したりして使うんだなとなんとなく分かります。
Modifier
ライフサイクルもあるので、これを使ってカスタマイズができそうです。
chakra-uiをの実装も参考になりそうです。
Reactで使ってみる。
Reactで使えるようなhookがあるので、これを使います。
yarn add react-popper @popperjs/core
Simple Example
overlayしたい要素とそれの基準となる要素のrefをuserPopperに渡すと、popperが導出した
styleとattribute(data-popper-*)を返してくれます。
import React, { useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import styled from 'styled-components';
export const Example = () => {
const [show, setShow] = useState(false);
const referenceRef = useRef<HTMLButtonElement | null>(null);
const popperRef = useRef<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(
referenceRef.current,
popperRef.current,
{
placement: 'bottom',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 16],
},
},
],
}
);
return (
<div>
<button onClick={() => setShow(true)} ref={referenceRef}>
open
</button>
<div ref={popperRef} style={styles.popper} {...attributes.popper}>
{show && <OverlayContent>content</OverlayContent>}
</div>
</div>
);
};
const OverlayContent = styled.div`
width: 400px;
height: 300px;
padding: 16px;
background: white;
border: 1px solid #eee;
border-radius: 6px;
box-shadow: 0px 4px 7px rgba(0, 0, 0, 0.25);
`;
これは座標を計算しているだけなので、overlayコンテンツがReact.Portalであっても正しく動きます。
シンプルで使いやすいですね。
with custom hooks
custom hookを使ってクリックやescapeで閉じるようにした例。
import React, { useState, useEffect, useRef, RefObject } from 'react';
import { usePopper } from 'react-popper';
import styled from 'styled-components';
const defaultEvents = ['mousedown', 'touchstart'];
export const useClickAway = <E extends Event = Event>(
ref: RefObject<HTMLElement | null>,
onClickAway: (event: E) => void,
events: string[] = defaultEvents
) => {
const savedCallback = useRef(onClickAway);
useEffect(() => {
savedCallback.current = onClickAway;
}, [onClickAway]);
useEffect(() => {
const handler = (event: any) => {
const { current: el } = ref;
if (el && !el.contains(event.target)) {
savedCallback.current(event);
}
};
events.forEach(eventName => document.addEventListener(eventName, handler));
return () => {
events.forEach(eventName =>
document.removeEventListener(eventName, handler)
);
};
}, [events, ref]);
};
const useDisclosure = (initialValue: boolean) => {
const [isOpen, setIsOpen] = useState(initialValue);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
return {
isOpen,
open,
close,
};
};
export function useKeypress(key: string, handler: () => void) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const onKeyup = (event: KeyboardEvent) => {
if (event.key === key) {
savedHandler.current();
}
};
document.addEventListener('keyup', onKeyup);
return () => document.removeEventListener('keyup', onKeyup);
}, []);
}
export const Example = () => {
const referenceRef = useRef<HTMLButtonElement | null>(null);
const popperRef = useRef<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(
referenceRef.current,
popperRef.current,
{
placement: 'bottom',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 16],
},
},
],
}
);
const { isOpen, open, close } = useDisclosure(false);
useClickAway(popperRef, close);
useKeypress('Escape', close);
return (
<div>
<button onClick={open} ref={referenceRef}>
open
</button>
<div ref={popperRef} style={styles.popper} {...attributes.popper}>
{isOpen && <OverlayContent>content</OverlayContent>}
</div>
</div>
);
};
const OverlayContent = styled.div`
width: 400px;
height: 300px;
padding: 16px;
background: white;
border: 1px solid #eee;
border-radius: 6px;
box-shadow: 0px 4px 7px rgba(0, 0, 0, 0.25);
`;
基本的にpopperを直接使うことはないと思いますが、UIライブラリを読んでるとPopperを使ってるので気になって触ってみました。シンプルなAPIで拡張性も高く、HeadlessなUtilltyなので多くの箇所で使われているんだなと納得しました。
Discussion
floating-uiを使ってデモを作ってみました。
demo code.
demo site.
簡単ですが、以上です。