記事を書いた経緯
開発中にページ内遷移をドロワーで行いたいと思い、色々と調べてみたのですが
記事がなく、自作してしまったのでその備忘録になります🙌
また、MUIやNext等の導入は既に行なっている体で話を進めますので
導入から行う方は以下の記事を参考にすると良いです!
(自分が書いたものですが、大方使うものは全て書いてあります👍)
動作画面
早速ですが、実装した画面を見てみましょう。
こんな感じです!
(画質とデザインはご勘弁を、、、🙏)
手順
- Drawerを作る
- DrawerMenuを作る
- 使いたいページでuseRefしまくる
- propsの形式でDrawerMenuに渡す
- ドロワーの項目ごとでスクロールする関数を作成する
Drawerを作る
まず、ドロワーを使いたいので、作ります。ドロワーだけ作成する手順は
- ドロワーAPIを拝借
- ドロワーの状態を管理
だけになります!
(公式の内容だと全方向にドロワーが出る感じでコード量が多かったのですが
実際にやってみるとそうでもないです✨)
コード
import {
AppBar,
Container,
Drawer,
IconButton,
Toolbar,
Typography,
} from '@mui/material'
import MenuIcon from '@mui/icons-material/Menu'
import DateDisplay from './DateDisplay'
import { useState } from 'react'
import { DrawerMenu } from '~/components/layout/header/DrawerMenu'
type Props = {
title: string
scroll?: string[]
}
const Header = (props: Props) => {
+ const [isOpen, setIsOpen] = useState<boolean>(false)
return (
<>
<AppBar position='fixed'>
<Toolbar>
<IconButton
size='large'
edge='start'
color='inherit'
aria-label='menu'
onClick={() => setIsOpen(true)}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
+ <Drawer anchor='left' open={isOpen} onClose={() => setIsOpen(false)}>
+ </Drawer>
<Container>
<Typography variant='h6' component='div'>
{props.title}
</Typography>
</Container>
<Container className='text-right mr-2'>
<DateDisplay />
</Container>
</Toolbar>
</AppBar>
</>
)
}
export default Header
今回追加した部分は緑色になっています!
解説
MUIのドロワーはanchor
どこにドロワーを出すか、
onClose
閉じる際に実行する関数、opne
状態を覚えておくもの
この3つを渡すだけで使えます🙌
今回は、useStateで状態管理しているのでisOpen
とsetIsOpen
を渡してあげれば
簡単に状態管理できますね!
また、ドロワーの方向は左、左側からドロワーが出てくる形になります!
DrawerMenuを作る
ここでは、公式が作っていたリストを独自のものに少し改造します!
公式のドロワーメニュー(リスト)
const list = (anchor: Anchor) => (
+ <Box
+ sx={{ width: anchor === 'top' || anchor === 'bottom' ? 'auto' : 250 }}
+ role="presentation"
+ onClick={toggleDrawer(anchor, false)}
+ onKeyDown={toggleDrawer(anchor, false)}
+ >
+ <List>
+ {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
+ <ListItem key={text} disablePadding>
+ <ListItemButton>
+ <ListItemIcon>
+ {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
+ </ListItemIcon>
+ <ListItemText primary={text} />
+ </ListItemButton>
+ </ListItem>
+ ))}
+ </List>
<Divider />
<List>
{['All mail', 'Trash', 'Spam'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);
今回必要になりそうな(使いたい)部分は緑色にしています!
改造後
import {
Box,
List,
ListItem,
ListItemButton,
ListItemText,
} from '@mui/material'
const DrawerMenu: React.FC<Props> = (props) => {
const menuList = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
return (
<Box
role='presentation'
onClick={() => {}}
onKeyDown={() => {}}
>
<List>
{menuList.map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemText primary={`${index + 1}. ` + text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
)
}
公式のものだと、アイコンとかいらないものが少しあったので
もう少し簡単に改造しました!
使いたいページでuseRefしまくる
import { Container } from '@mui/material'
import { useEffect, useRef, useState } from 'react'
import Header from '~/components/layout/header'
import Consult from '~/components/report/consult'
import FirstForm from '~/components/report/firstForm'
import Note from '~/components/report/note'
import { Register } from '~/components/report/register'
import { Transfer } from '~/components/report/transfer'
export const Report = () => {
const [top, setTop] = useState<string[]>()
const firstFormRef = useRef<HTMLDivElement>(null)
const consultRef = useRef<HTMLDivElement>(null)
const noteRef = useRef<HTMLDivElement>(null)
const registerRef = useRef<HTMLDivElement>(null)
const transferRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setTop([
String(firstFormRef.current!.getBoundingClientRect()['top']),
String(consultRef.current!.getBoundingClientRect()['top']),
String(noteRef.current!.getBoundingClientRect()['top']),
String(registerRef.current!.getBoundingClientRect()['top']),
String(transferRef.current!.getBoundingClientRect()['top']),
])
}, [firstFormRef, consultRef, noteRef, registerRef, transferRef])
return (
<>
<Header title='Sample' scroll={top} />
<Container component='div' className='min-h-screen mt-28'>
<Container className='mt-10 border rounded' ref={firstFormRef}>
< />
</Container>
<Container className='mt-10 border rounded' ref={consultRef}>
<Consult />
</Container>
<Container className='mt-10 border rounded' ref={noteRef}>
<Note />
</Container>
<Container className='mt-10 border rounded' ref={registerRef}>
<Register />
</Container>
<Container className='mt-10 border rounded' ref={transferRef}>
<Transfer />
</Container>
</Container>
</>
)
}
これが最良かどうかだとすごく怪しいですが、今回のページ内遷移では
移動する要素のTopの情報がほしいのでref
を使っています。
また、Propsで渡す際には一気に渡したいので
配列で状態を管理する変数を作っておきます。
getBoundingClientRect
↑が突然出てきて???かもしれませんが、こちらの関数(メソッド)は
HTMLの要素の大きさなどを取得するものです!
今回スクロールするにあたってサイズなどの情報が必要なので取得しています🌸
propsの形式でDrawerMenuに渡す
<Header title='Sample' scroll={top} />
の形式で渡すので
DrawerMenu
も先ほどのものから変更しましょう。
import {
Box,
List,
ListItem,
ListItemButton,
ListItemText,
} from '@mui/material'
+ import { Dispatch, SetStateAction } from 'react'
+ type Props = {
+ scroll?: string[]
+ setIsOpen: Dispatch<SetStateAction<boolean>>
+ }
export const DrawerMenu: React.FC<Props> = (props) => {
const menuList = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
return (
<Box
role='presentation'
+ onClick={() => props.setIsOpen(false)}
+ onKeyDown={() => props.setIsOpen(false)}
>
<List>
{menuList.map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemText primary={`${index + 1}. ` + text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
)
}
Propsで渡せるように変更を加えました。
また、メニュー側からドロワーを閉じることができるように
ドロワーの状態を変更するsetIsOpen
もついでに渡しておきます。
ドロワーの項目ごとでスクロールする関数を作成する
最後に、関数を作成します。実は難しくないので
サクッと読めますよ〜〜✊
import {
Box,
List,
ListItem,
ListItemButton,
ListItemText,
} from '@mui/material'
import { Dispatch, SetStateAction } from 'react'
type Props = {
scroll?: string[]
setIsOpen: Dispatch<SetStateAction<boolean>>
}
export const DrawerMenu: React.FC<Props> = (props) => {
const menuList = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
+ const onClickItem = (idx: number) => {
+ const scrollTop =
+ props.scroll !== undefined ? Number(props.scroll[idx]) - 80 : 0
+ window.scrollTo({ top: scrollTop, behavior: 'smooth' })
+ }
return (
<Box
role='presentation'
onClick={() => props.setIsOpen(false)}
onKeyDown={() => props.setIsOpen(false)}
>
<List>
{menuList.map((text, index) => (
<ListItem key={text} disablePadding>
+ <ListItemButton onClick={() => onClickItem(index)}>
<ListItemText primary={`${index + 1}. ` + text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
)
}
関数の解説
今回は要素の一番上であるtop
の情報を利用してスクロールするので
scrollToを利用します。
今回はオプションを細かく指定する形式で、
滑らかに指定した位置まで移動するように設定します。
const onClickItem = (idx: number) => {
const scrollTop =
props.scroll !== undefined ? Number(props.scroll[idx]) - 80 : 0
window.scrollTo({ top: scrollTop, behavior: 'smooth' })
}
また、謎の-80
は要素の一番上にピッタリ来られると
少し見にくかったので調整としてつけた感じなので
ここはお好みで調整しましょう!
今回の設定では渡す前の情報と事前に作成したリストのインデックスが同じなので
ピッタリできた感じです!
感想
今回は調べる過程が長くて少し大変でしたが
一応実装できたので、よかったです🙌
「もっとこうしたら良くなるよ」、「それ、他のライブラリだと簡単にできるよ」
などなどありましたら、ぜひコメントお願いします🙇
最後まで見ていただき、ありがとうございました!