masalibの日記

システム開発、運用と猫の写真ブログです

React Material UI入門2 メニューバー

Google先生みたいなログイン画面が作りたかった。 それには「Material UI」を使えばいいので Material UIについて勉強しました

前回の続きになります

masalib.hatenablog.com

Material UIとは

GoogleのMaterialデザインをベースに開発された、UIコンポーネントライブラリです。 Google先生のサイトを使った事ある人ならすぐに受け入れてくるのがいい点です。

メニューバーみたいなもの

メニューバーとは はてなブログはてなブックマークを使って事がある人なら見たことがあるブラウザの上の部分に表示されているものです

f:id:masalib:20201121152105p:plain

この部分を作りたいと思います。

メニューバー作成1

超ー簡単です。google先生サンプルソースを用意してくれています。

App Bar React component - Material-UI

ソース

クリックすると展開されます(長文なので注意)

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

const useStyles = makeStyles((theme) => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },
}));

export default function ButtonAppBar() {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Toolbar>
          <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="menu">
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" className={classes.title}>
            News
          </Typography>
          <Button color="inherit">Login</Button>
        </Toolbar>
      </AppBar>
    </div>
  );
}

結果

f:id:masalib:20201121153250p:plain

さすが!!Google先生!!

メニューバー作成2

さすがにシンプルすぎるのではてなみたいに検索とか表示します。

ソース

クリックすると展開されます(長文なので注意)

import React from 'react';
import { fade, makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import InputBase from '@material-ui/core/InputBase';
import Badge from '@material-ui/core/Badge';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import AccountCircle from '@material-ui/icons/AccountCircle';
import MailIcon from '@material-ui/icons/Mail';
import NotificationsIcon from '@material-ui/icons/Notifications';
import MoreIcon from '@material-ui/icons/MoreVert';

const useStyles = makeStyles((theme) => ({
  grow: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    display: 'none',
    [theme.breakpoints.up('sm')]: {
      display: 'block',
    },
  },
  search: {
    position: 'relative',
    borderRadius: theme.shape.borderRadius,
    backgroundColor: fade(theme.palette.common.white, 0.15),
    '&:hover': {
      backgroundColor: fade(theme.palette.common.white, 0.25),
    },
    marginRight: theme.spacing(2),
    marginLeft: 0,
    width: '100%',
    [theme.breakpoints.up('sm')]: {
      marginLeft: theme.spacing(3),
      width: 'auto',
    },
  },
  searchIcon: {
    padding: theme.spacing(0, 2),
    height: '100%',
    position: 'absolute',
    pointerEvents: 'none',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  inputRoot: {
    color: 'inherit',
  },
  inputInput: {
    padding: theme.spacing(1, 1, 1, 0),
    // vertical padding + font size from searchIcon
    paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
    transition: theme.transitions.create('width'),
    width: '100%',
    [theme.breakpoints.up('md')]: {
      width: '20ch',
    },
  },
  sectionDesktop: {
    display: 'none',
    [theme.breakpoints.up('md')]: {
      display: 'flex',
    },
  },
  sectionMobile: {
    display: 'flex',
    [theme.breakpoints.up('md')]: {
      display: 'none',
    },
  },
}));

export default function PrimarySearchAppBar() {
  const classes = useStyles();
  const [anchorEl, setAnchorEl] = React.useState(null);
  const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null);

  const isMenuOpen = Boolean(anchorEl);
  const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);

  const handleProfileMenuOpen = (event) => {
    setAnchorEl(event.currentTarget);
  };

  const handleMobileMenuClose = () => {
    setMobileMoreAnchorEl(null);
  };

  const handleMenuClose = () => {
    setAnchorEl(null);
    handleMobileMenuClose();
  };

  const handleMobileMenuOpen = (event) => {
    setMobileMoreAnchorEl(event.currentTarget);
  };

  const menuId = 'primary-search-account-menu';
  const renderMenu = (
    <Menu
      anchorEl={anchorEl}
      anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
      id={menuId}
      keepMounted
      transformOrigin={{ vertical: 'top', horizontal: 'right' }}
      open={isMenuOpen}
      onClose={handleMenuClose}
    >
      <MenuItem onClick={handleMenuClose}>Profile</MenuItem>
      <MenuItem onClick={handleMenuClose}>My account</MenuItem>
    </Menu>
  );

  const mobileMenuId = 'primary-search-account-menu-mobile';
  const renderMobileMenu = (
    <Menu
      anchorEl={mobileMoreAnchorEl}
      anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
      id={mobileMenuId}
      keepMounted
      transformOrigin={{ vertical: 'top', horizontal: 'right' }}
      open={isMobileMenuOpen}
      onClose={handleMobileMenuClose}
    >
      <MenuItem>
        <IconButton aria-label="show 4 new mails" color="inherit">
          <Badge badgeContent={4} color="secondary">
            <MailIcon />
          </Badge>
        </IconButton>
        <p>Messages</p>
      </MenuItem>
      <MenuItem>
        <IconButton aria-label="show 11 new notifications" color="inherit">
          <Badge badgeContent={11} color="secondary">
            <NotificationsIcon />
          </Badge>
        </IconButton>
        <p>Notifications</p>
      </MenuItem>
      <MenuItem onClick={handleProfileMenuOpen}>
        <IconButton
          aria-label="account of current user"
          aria-controls="primary-search-account-menu"
          aria-haspopup="true"
          color="inherit"
        >
          <AccountCircle />
        </IconButton>
        <p>Profile</p>
      </MenuItem>
    </Menu>
  );

  return (
    <div className={classes.grow}>
      <AppBar position="static">
        <Toolbar>
          <IconButton
            edge="start"
            className={classes.menuButton}
            color="inherit"
            aria-label="open drawer"
          >
            <MenuIcon />
          </IconButton>
          <Typography className={classes.title} variant="h6" noWrap>
            Material-UI
          </Typography>
          <div className={classes.search}>
            <div className={classes.searchIcon}>
              <SearchIcon />
            </div>
            <InputBase
              placeholder="Search…"
              classes={{
                root: classes.inputRoot,
                input: classes.inputInput,
              }}
              inputProps={{ 'aria-label': 'search' }}
            />
          </div>
          <div className={classes.grow} />
          <div className={classes.sectionDesktop}>
            <IconButton aria-label="show 4 new mails" color="inherit">
              <Badge badgeContent={4} color="secondary">
                <MailIcon />
              </Badge>
            </IconButton>
            <IconButton aria-label="show 17 new notifications" color="inherit">
              <Badge badgeContent={17} color="secondary">
                <NotificationsIcon />
              </Badge>
            </IconButton>
            <IconButton
              edge="end"
              aria-label="account of current user"
              aria-controls={menuId}
              aria-haspopup="true"
              onClick={handleProfileMenuOpen}
              color="inherit"
            >
              <AccountCircle />
            </IconButton>
          </div>
          <div className={classes.sectionMobile}>
            <IconButton
              aria-label="show more"
              aria-controls={mobileMenuId}
              aria-haspopup="true"
              onClick={handleMobileMenuOpen}
              color="inherit"
            >
              <MoreIcon />
            </IconButton>
          </div>
        </Toolbar>
      </AppBar>
      {renderMobileMenu}
      {renderMenu}
    </div>
  );
}

結果

f:id:masalib:20201121155324p:plainf:id:masalib:20201121155330p:plain
メニューバー

さすが!!Google先生!!すごすぎ

ソース解説

長文なのでコピペしただけだと理解できていないので未来の自分用(笑)に解説します

レスポンシブに対応

メニューバーはスマフォで表示すると横のピクセルが足りません。そこで スマフォとディスクトップで分ける必要があります。俗にいうレスポンシブ対応です。 サンプルのスマフォとディスクトップを切り替えているのは横の幅が960px以上がディスクトップで960px未満がスマフォの判定でした。

   [theme.breakpoints.up('md')]: {
      display: 'flex',
    }

もし960px以外での判定をしたい場合はこのmd部分を変えれば変更できます

  • xs、極小:0px
  • sm、小さい:600px
  • md、 medium:960px
  • lg、大:1280px
  • xl、特大:1920px

ディスクトップとモバイルでスタイルを作って breakpointsで切り替える事でレスポンシブをおこなっています

  sectionDesktop: {
    display: 'none',
    [theme.breakpoints.up('md')]: {
      display: 'flex',
    },
  },
  sectionMobile: {
    display: 'flex',
    [theme.breakpoints.up('md')]: {
      display: 'none',
    },
  },

実際のHTMLのレンダリングする部分でClassNameでsectionDesktopとsectionMobileを指定する事で 切り替えています

<div className={classes.sectionDesktop}>
  <IconButton aria-label="show 4 new mails" color="inherit">
    <Badge badgeContent={4} color="secondary">
      <MailIcon />
    </Badge>
  </IconButton>
  <IconButton aria-label="show 17 new notifications" color="inherit">
    <Badge badgeContent={17} color="secondary">
      <NotificationsIcon />
    </Badge>
  </IconButton>
  <IconButton
    edge="end"
    aria-label="account of current user"
    aria-controls={menuId}
    aria-haspopup="true"
    onClick={handleProfileMenuOpen}
    color="inherit"
  >
    <AccountCircle />
  </IconButton>
</div>
<div className={classes.sectionMobile}>
  <IconButton
    aria-label="show more"
    aria-controls={mobileMenuId}
    aria-haspopup="true"
    onClick={handleMobileMenuOpen}
    color="inherit"
  >
    <MoreIcon />
  </IconButton>
</div>

モーダル表示

スマフォにすると足りないメニューバーをクリックすると表示させるモーダル表示をさせる必要あります

  <IconButton
    aria-label="show more"
    aria-controls={mobileMenuId}
    aria-haspopup="true"
    onClick={handleMobileMenuOpen}
    color="inherit"
  >
    <MoreIcon />
  </IconButton>

このonClick={handleMobileMenuOpen}でモーダルを表示させる関数を実行しています

  const handleMobileMenuOpen = (event) => {
    console.log("handleMobileMenuOpen");
    setMobileMoreAnchorEl(event.currentTarget);
  };

setMobileMoreAnchorElでMobileMoreAnchorElの値をfalseからTrueに切り替えています

 const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);

isMobileMenuOpenがtrueになる事でモーダルをメニューを表示させています (open={isMobileMenuOpen}の部分が注目ポイント)

 const renderMobileMenu = (
    <Menu
      anchorEl={mobileMoreAnchorEl}
      anchorOrigin={{ vertical: "top", horizontal: "right" }}
      id={mobileMenuId}
      keepMounted
      transformOrigin={{ vertical: "top", horizontal: "right" }}
      open={isMobileMenuOpen}
      onClose={handleMobileMenuClose}
    >
      <MenuItem>
        <IconButton aria-label="show 4 new mails" color="inherit">
          <Badge badgeContent={5} color="secondary">
            <MailIcon />
          </Badge>
        </IconButton>
        <p>Messages</p>
      </MenuItem>
      <MenuItem>
        <IconButton aria-label="show 11 new notifications" color="inherit">
          <Badge badgeContent={11} color="secondary">
            <NotificationsIcon />
          </Badge>
        </IconButton>
        <p>Notifications</p>
      </MenuItem>
      <MenuItem onClick={handleProfileMenuOpen}>
        <IconButton
          aria-label="account of current user"
          aria-controls="primary-search-account-menu"
          aria-haspopup="true"
          color="inherit"
        >
          <AccountCircle />
        </IconButton>
        <p>Profile</p>
      </MenuItem>
    </Menu>
  );

メニューが開いた状態でメニュー以外の部分をクリックするとCloseのイベントが発生します。そこで handleMobileMenuCloseが起動します

  const handleMobileMenuClose = () => {
    setMobileMoreAnchorEl(null);
    console.log("handleMobileMenuClose");
  };

その部分でMobileMoreAnchorElの値がTrueからfalseに変更される事で非表示にしています。

説明していない事での補足

MenuにanchorElという指定があります

 <Menu
      anchorEl={anchorEl}

この部分の指定を忘れてるとトンデモない所に表示されるもよう・・・

qiita.com

感想

まだできていないけど、ログイン情報や通知の状態を渡す事で内容を変更できるようにしたい