ReactでDropdownをつくる。
すごい今更なんだけど、転職用にアウトプットをしていきたい。
来週くらいまでにに記事として投稿したい。
普通のドロップダウンを作る。
完成形
<Dropdown>
<Dropdown.Toggle>Open</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.MenuItem> A </Dropdown.MenuItem/>
<Dropdown.MenuItem> B </Dropdown.MenuItem/>
<Dropdown.MenuItem> C </Dropdown.MenuItem/>
<Dropdown.MenuItem> D </Dropdown.MenuItem/>
<Dropdown.Menu/>
</Dropdown>
目次的には
- Overlay Hook
- アクセシビリティ
- CSS Dropdown
- Portal
- useMemo, useCallback等
- テスト
useOutsideClick
画面をクリックしてドロップダウンを閉じるようにするためには、
こうゆうHookをつくるのと思う。
実装自体もそんな難しくないし、ライブラリの実装も似たりよったりである。
import { RefObject, useEffect, useRef } from 'react';
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]);
};
- https://usehooks.com/useOnClickOutside/
- https://chakra-ui.com/docs/hooks/use-outside-click
- https://github.com/streamich/react-use/blob/90e72a5340460816e2159b2c461254661b00e1d3/src/useClickAway.ts
補足があるとしたら、モバイル端末を意識しようくらいだろうか。
(mouseDownを指定する場合は、touchDownをしていするみたいな)
まぁ素直にclickリスナーを指定すればいいと思うが。
作ったhookを使って最小限で作るとこんな感じだろうか。
export const Dropdown = () => {
const [show, setShow] = useState(false);
const ref = useRef(null);
useClickAway(ref, () => {
setShow(false);
});
return (
<div>
<div onClick={() => setShow(true)}>open</div>
{show && <div ref={ref}>content</div>}
</div>
);
};
これで要件であろう
- open をクリックしたら メニューが表示される。
- 画面の外をクリックしたらメニューが非表示になる。
がクリアできる。
スタイルを当てていく。
基本的にはWrapperにposition:relativeを指定し、メニューはposition:absoluteで座標を指定していけば良い。
あと、z-indexの指定も必要。
ただ、あくまでCSSなので、親要素でOverflow Hiddenとか指定されてる切れたりする。
ググって日本語の記事を読んでもいいし、少し信頼できそうなリンクを張っておく。
ただ、基本的に上記の実装で事足りるはずである。
React.Portalを使ったやり方はめんどくさいのでやらない。
座標の計算とか面倒なので、実際に実装する場合はpopper.jsを使うのが良さそう。
(まぁそもそもDropdownなんて作らないと思うが)
とりあえず、それっぽいDropdownを作ったのであとは完成形のインターフェースに持っていく。
まず、最初のインターフェースだが
これはこうするしかない。
export const Dropdown = () => {}
Dropdown.Menu = () => {}
そして、もう一つのインターフェースを実現するためには、contextを使う。
export const Dropdown = ({ children }: any) => {
const [show, setShow] = useState(false);
return (
<div>
<Context.Provider value={{ show, setShow }}>{children}</Context.Provider>
</div>
);
};
const Context = React.createContext({
show: false,
setShow: (v: boolean) => {},
});
const useDropdownContext = () => {
return useContext(Context);
};
export const DropdownToggle = ({ children }: any) => {
const { setShow } = useDropdownContext();
console.log('toggle rendered');
return (
<div
onClick={() => {
setShow(true);
console.log('ghoe');
}}
>
{children}
</div>
);
};
export const DropdownMenu = ({ children }: any) => {
const { show, setShow } = useDropdownContext();
const ref = useRef(null);
useClickAway(ref, () => {
setShow(false);
});
return <>{show && <div ref={ref}>{children}</div>}</>;
};
export const DropdownMenuItem = ({ children }: any) => {
return <MenuItemWrapper> {children}</MenuItemWrapper>;
};
contextを使うことによって、不要な描画が発生するので最低限の制御を行う。
アクセシビリティへの配慮。
dropdownが表示中のときは、aria-labelを指定する。
roleも指定する。
Button
- aria-expanded
- aria-controls
- aria-haspopup
Menu
- role
- menu
- aria-orientation
MenuItem
- tabIndex
- role=menuitem
- dataindex
Chakra
実はドキュメントに書かれてたり。
キーボード
escで閉じるのみ実装。
キーイベントをいい感じに扱えるhookをつくる。
const useKeyboardEvent = () => {
}
上下左右で下のメニューを移動できたりする。
テスト。
まぁ別に書く必要ないけど
アクセシビリティのテスト、
機能面のテストをやるイメージ。
これは、まぁコード読む程度で良いでしょ。