LoginSignup
2
2

More than 1 year has passed since last update.

React ページ内遷移をドロワーで実装する【備忘録】

Last updated at Posted at 2022-11-19

記事を書いた経緯

開発中にページ内遷移をドロワーで行いたいと思い、色々と調べてみたのですが
記事がなく、自作してしまったのでその備忘録になります🙌

また、MUIやNext等の導入は既に行なっている体で話を進めますので
導入から行う方は以下の記事を参考にすると良いです!
(自分が書いたものですが、大方使うものは全て書いてあります👍)

動作画面

早速ですが、実装した画面を見てみましょう。
こんな感じです!
(画質とデザインはご勘弁を、、、🙏)
download.gif

手順

  1. Drawerを作る
  2. DrawerMenuを作る
  3. 使いたいページでuseRefしまくる
  4. propsの形式でDrawerMenuに渡す
  5. ドロワーの項目ごとでスクロールする関数を作成する

Drawerを作る

まず、ドロワーを使いたいので、作ります。ドロワーだけ作成する手順は

  1. ドロワーAPIを拝借
  2. ドロワーの状態を管理
    だけになります!
    公式の内容だと全方向にドロワーが出る感じでコード量が多かったのですが
    実際にやってみるとそうでもないです✨)

コード

header/index.tsx
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で状態管理しているのでisOpensetIsOpenを渡してあげれば
簡単に状態管理できますね!
また、ドロワーの方向は左、左側からドロワーが出てくる形になります!

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>
  );

今回必要になりそうな(使いたい)部分は緑色にしています!

改造後

DrawerMenu.tsx
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しまくる

index.tsx
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も先ほどのものから変更しましょう。

DrawerMenu.tsx
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もついでに渡しておきます。

ドロワーの項目ごとでスクロールする関数を作成する

最後に、関数を作成します。実は難しくないので
サクッと読めますよ〜〜✊

DrawerMenu.tsx
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は要素の一番上にピッタリ来られると
少し見にくかったので調整としてつけた感じなので
ここはお好みで調整しましょう!

今回の設定では渡す前の情報と事前に作成したリストのインデックスが同じなので
ピッタリできた感じです!

感想

今回は調べる過程が長くて少し大変でしたが
一応実装できたので、よかったです🙌

「もっとこうしたら良くなるよ」、「それ、他のライブラリだと簡単にできるよ」
などなどありましたら、ぜひコメントお願いします🙇
最後まで見ていただき、ありがとうございました!

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2