Mantine 入門5 トップページに動きをつけよう
前回のトップページに動きをつけます。具体的には下記を実施します。
ダークモードの実装
スマホ用、タブレット用、PC用の表示
ダークモード
まずはダークモードです。まずは__app.tsxで以下のように手動でダークモードを設定してみます。
import { AppProps } from 'next/app';
import Head from 'next/head';
import { MantineProvider } from '@mantine/core';
export default function App(props: AppProps) {
const { Component, pageProps } = props;
return (
<>
<Head>
<title>Page title</title>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
</Head>
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
/** Put your mantine theme override here */
colorScheme: 'dark',
}}
>
<Component {...pageProps} />
</MantineProvider>
</>
);
}
それっぽくなりました。次に、どうやってコレを動的に変えるかです。
公式ドキュメントの下記に書いてありました。
まず、__app.tsxを下記で置き換えます。
import { GetServerSidePropsContext } from 'next';
import { useState } from 'react';
import { AppProps } from 'next/app';
import { getCookie, setCookie } from 'cookies-next';
import Head from 'next/head';
import { MantineProvider, ColorScheme, ColorSchemeProvider } from '@mantine/core';
import { NotificationsProvider } from '@mantine/notifications';
export default function App(props: AppProps & { colorScheme: ColorScheme }) {
const { Component, pageProps } = props;
const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme);
const toggleColorScheme = (value?: ColorScheme) => {
const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark');
setColorScheme(nextColorScheme);
setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
};
return (
<>
<Head>
<title>Mantine next example</title>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<link rel="shortcut icon" href="/favicon.svg" />
</Head>
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
<MantineProvider theme={{ colorScheme }} withGlobalStyles withNormalizeCSS>
<NotificationsProvider>
<Component {...pageProps} />
</NotificationsProvider>
</MantineProvider>
</ColorSchemeProvider>
</>
);
}
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
colorScheme: getCookie('mantine-color-scheme', ctx) || 'light',
});
クッキーに、mantine-color-schemeのキーでcolorSchemeを保存し、MantineProviderのthemeに保存してあるcolorSchemeを設定しています。
次に、以下のようなアクションアイコンコンポーネントを作成します。
//components/tips/darkOrSunIcon.tsx
import { ActionIcon, useMantineColorScheme } from "@mantine/core"
import { IconSun,IconMoonStars } from "@tabler/icons"
const DarkOrSunIcon = () =>{
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
return (
<ActionIcon
onClick={() => toggleColorScheme()}
>
{dark ? <IconSun /> : <IconMoonStars />}
</ActionIcon>
);
}
export default DarkOrSunIcon
クリックされると、下記で定義したtoggleColorSchemeを介して、先ほどの__app .tsxのcolorSchemeを変更できるようです。
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
最後に、このアクションアイコンコンポーネントをホームヘッダに設定します。
・・・・
const HomeHeader= ()=>{
const {classes} = useStyles()
return(
<Header p="sm" height={50}>
<SimpleGrid cols={3}>
<div className={classes.burgerArea}>
<Burger opened={false}/>
</div>
<div className={classes.titleArea}>
<Title order={3}>毎日ブログ</Title>
</div>
<div className={classes.iconArea}>
<DarkOrSunIcon/>
</div>
</SimpleGrid>
</Header>
)
}
export default HomeHeader
レスポンシブ
ナビゲーションバー
ナビゲーションバーを画面幅に応じて自動で出したり、選択的にトグルできるようにします。
// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { AppShell} from '@mantine/core'
import HomeHeader from '../components/homeHeader'
import HomeNavbar from '../components/homeNavbar'
import HomeFooter from '../components/homeFooter'
import HomeMain from '../components/homeMain'
import { useState , useEffect} from 'react';
import { useViewportSize } from '@mantine/hooks';
import { useToggle } from '@mantine/hooks';
const Home: NextPage = () => {
const { height, width } = useViewportSize();
const [navbarOpened, toggleNavbarOpened] = useToggle([true, false]);
const onoffNavbar = () =>{toggleNavbarOpened()}
useEffect(()=>{
if(width < 600){
toggleNavbarOpened(false)
}
else{
toggleNavbarOpened(true)
}
//document.title = `${width}${navbarOpened}`
},[width])
return (
<>
<Head><title>Home</title></Head>
<AppShell
header={
<HomeHeader
navbarOpened={navbarOpened}
onoffNavbar={onoffNavbar}
/>}
navbar={<HomeNavbar navbarOpened={navbarOpened}/>}
footer={<HomeFooter/>}
>
<HomeMain/>
</AppShell>
</>
)
}
export default Home
まず、横幅は以下のhookで取得できます。
import { useViewportSize } from '@mantine/hooks';
・・・
const { height, width } = useViewportSize();
次にwidthが変化したときに、コレを検知して600px以上だったら強制的にナブバーON,狭い場合はナブバーOFFとします。
2値のStateは下記で生成すると、自分でONの時OFFにして、OFFの時ONにしてという処理を書かなくていいので便利です。
import { useToggle } from '@mantine/hooks';
・・・
const [navbarOpened, toggleNavbarOpened] = useToggle([true, false]);
const onoffNavbar = () =>{toggleNavbarOpened()}
定石ですが、oggleNavbarOpenedの型は複雑なので、ただの関数onoffNavbarでラップして、子コンポーネントに渡しやすいようにします。
以下のように、navbarOpenedを直接編集しても大丈夫です。
useEffect(()=>{
if(width < 600){
toggleNavbarOpened(false)
}
else{
toggleNavbarOpened(true)
}
//document.title = `${width}${navbarOpened}`
},[width])
コレで、画面幅600px以上だったらON,そうでなければOFFのフラグができました。
コレをNavBarコンポーネントに渡してあげています。
navbar={<HomeNavbar navbarOpened={navbarOpened}/>}
受け取るHomeNavbar側は次のように改造しています。
import { Navbar, ScrollArea, Chip, Title, Divider} from "@mantine/core"
const nav_width={
sm:200,//画面幅がテーマのブレークポイントsmを超える時
base:150//上記以外。デフォルト100%幅
}
export type HomeNavbarProps = {
navbarOpened:boolean
}
const HomeNavbar = ({navbarOpened}:HomeNavbarProps)=>{
if(navbarOpened){
nav_width.base=150
}else{
nav_width.base=1
}
return(
<Navbar hidden={!navbarOpened} p="sm" width={nav_width}>
・・・・
</Navbar>
)
}
export default HomeNavbar
コツはNavbarのhidden属性に渡すだけでなく、幅を1pxにしているところです。コレがなければナブバーが隠れるだけで、baseの幅が残っていました。
次に、バーガーメニューで600px以下の時でもナブバーを表示できるように
します。バーガーメニューはヘッダの子コンポーネントなので、ヘッダを介してONOFFフラグnavbarOpenedとコレをトグルする関数onoffNavbarを渡してあげます。
<AppShell
header={
<HomeHeader
navbarOpened={navbarOpened}
onoffNavbar={onoffNavbar}
/>}
バーガーメニューは以下の用に実装しました。
//components/tips/menuBurger.tsx
import { Burger } from "@mantine/core"
import { createStyles } from '@mantine/core'
const useStyles = createStyles((theme) => ({
burger:{
[theme.fn.largerThan(600)]:{
display:"none"
}
}
}))
export type MenuBurgerProps = {
navbarOpened:boolean
onoffNavbar:()=>void
}
const MenuBurger = ({navbarOpened,onoffNavbar}:MenuBurgerProps) =>{
const {classes} = useStyles()
return(
<>
<Burger className={classes.burger}
opened={navbarOpened}
onClick={()=>{onoffNavbar()}}/>
</>
)
}
export default MenuBurger
ポイントは、Burgerのタグにpropsとして、先ほどのフラグと関数を受け取るところと、下記により600px以下ではメニューを隠しているところです。
[theme.fn.largerThan(600)]:{
display:"none"
}
中継するpropsを中継するヘッダーは下記の通りです。
import { Header, Title , SimpleGrid} from "@mantine/core"
import { createStyles } from '@mantine/core'
import DarkOrSunIcon from "./tips/darkOrSunIcon"
import MenuBurger from "./tips/menuBurger"
import { MenuBurgerProps } from "./tips/menuBurger"
・・・・
const HomeHeader = ({navbarOpened, onoffNavbar}:MenuBurgerProps)=>{
const {classes} = useStyles()
return(
<Header p="sm" height={50}>
<SimpleGrid cols={3}>
<div className={classes.burgerArea}>
<MenuBurger
navbarOpened={navbarOpened}
onoffNavbar={onoffNavbar}/>
</div>
<div className={classes.titleArea}>
<Title order={3}>毎日ブログ</Title>
</div>
<div className={classes.iconArea}>
<DarkOrSunIcon/>
</div>
</SimpleGrid>
</Header>
)
}
export default HomeHeader
カード群表示
現在は、下記の通り、シンプルグリッドを使ってどんな時でも2列でカードを並べていました。
//components/homeMainCards.tsx
import { SimpleGrid } from "@mantine/core"
import BlogCard from "./tips/blogCard"
import type { BlogCardProps } from "./tips/blogCard"
......
const HomeMainCards = () =>{
const list_jsx_Cards = article_headers_obj.map((item,index) => (
<BlogCard
title={item.title}
date={item.date}
abstract={item.abstract}
tags={item.tags}
/>
))
return(
<SimpleGrid m="md" cols={2}>
{list_jsx_Cards}
</SimpleGrid>
)
}
export default HomeMainCards
通常のGridでは、ブレークポイントサイズごとに幅(最大12)を設定できます。smより小さい時は幅12(1列)、sm以上で幅6(2列)、lg以上で幅3(4列)にします。
const HomeMainCards = () =>{
const list_jsx_Cards = article_headers_obj.map((item,index) => (
<Grid.Col sm={6} lg={4}>
<BlogCard
title={item.title}
date={item.date}
abstract={item.abstract}
tags={item.tags}
/>
</Grid.Col>
))
return(
<Grid m="md">
{list_jsx_Cards}
</Grid>
)
}
export default HomeMainCards
あとは、カードのコンテンツによってカードサイズがあまり変化しないようにしたいと思います。カードはタイトル、日付、概要、タグからなりますが、概要のテキストをSpoilerで囲って高さを制限しました。
return (
<Card className={classes.cardArea} shadow="sm" p="lg" radius="md">
<Title order={5}>{title}</Title>
<Group position="right">
<Text size="xs">{date}</Text>
</Group>
<Spoiler maxHeight={40} showLabel="もっと見る" hideLabel="隠す" transitionDuration={0}>
<Text weight={500} size="sm">{abstract}</Text>
</Spoiler>
<SimpleGrid mt="sm" cols={2}>
{list_jsx_Badge}
</SimpleGrid>
</Card>
);
}
コレでだいぶレスポンシブっぽくなりました。