react-dates を使った DatePicker のレスポンシブ対応をやる(DateRangePicker 編)
はじめに
テントの中から失礼します、CX事業本部のてんとタカハシです!
React を使用した Web アプリの案件で、react-dates というライブラリを使用した DatePicker のレスポンシブ対応を行う機会がありました。
私が担当している案件では、UI のフレームワークとして Material-UI を使用しているのですが、Material-UI の DatePicker(v3系)には、日付の範囲指定が可能なコンポーネントが用意されていないため、代わりに react-dates を使用しています。
今回は Material-UI と react-dates を使った DatePicker のレスポンシブ対応について記事にしようと思います。react-dates には、DateRangePicker
という日付の範囲指定が可能な DatePicker と TextField がセットになったコンポーネントがありますので、今回はこのコンポーネントに絞って解説していきます。
下記のリポジトリに、全体のソースコードを置いているので、併せて参考にして頂ければと思います。
Github - react-dates-mobile-friendly
react-dates とは
react-dates とは、Airbnb が開発している React 用の DatePicker ライブラリです。日付の単体指定はもちろんのこと、日付の範囲指定が可能なコンポーネントが用意されていたり、カスタマイズが豊富であることから、多様なケースに対応ができる便利なライブラリです。
こちらに、コンポーネント別のデモが用意されているので、どんな感じのライブラリなのか、事前に確認することができます。
環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.6 BuildVersion: 19G2021 $ node --version v12.18.2 $ yarn --version 1.22.4 $ yarn list --depth=0 @material-ui/[email protected] [email protected] [email protected] [email protected]
基本的な使い方
インストール
react-dates の内部で使われている moment も一緒にインストールする必要があります。
$ yarn add react-dates moment $ yarn add --dev @types/react-dates
実装
日付の範囲指定が可能な DatePicker と TextField がセットになった DateRangePicker
コンポーネントを使って、シンプルに実装する場合は、下記の通りになります。
import React, { useState } from 'react'; import moment from 'moment'; import { DateRangePicker } from 'react-dates'; import 'moment/locale/ja'; // 日本語ローカライズ import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; const MyDateRangePicker: React.FC = () => { const [startDate, setStartDate] = useState<moment.Moment | null>(null); const [endDate, setEndDate] = useState<moment.Moment | null>(null); const [focusedInput, setFocusedInput] = useState< 'startDate' | 'endDate' | null >(null); return ( <DateRangePicker startDate={startDate} startDateId="startDateId" endDate={endDate} endDateId="endDateId" focusedInput={focusedInput} onFocusChange={setFocusedInput} onDatesChange={(selectedDates) => { setStartDate(selectedDates.startDate); setEndDate(selectedDates.endDate); }} /> ); }; export default MyDateRangePicker;
下記の通り、まず TextField が表示されている状態になります。
TextField をクリックすると、Date Picker が表示されるようになります。日付の範囲指定も良い感じにできます。
しかし、この状態でスマホ表示にすると、DatePicker が画面から見切れてしまい、使い勝手が悪くなってしまいます。スマホ表示にも対応できるようにしていきましょう。
スマホ表示を整える
全画面表示 & 縦表示にする
DateRangePicker
の Props である withPortal
を true
にすると、DatePicker が全画面表示のようになります。また、orientation
を vertical
にすると、Date Picker が縦表示になります。
<DateRangePicker startDate={startDate} startDateId="startDateId" endDate={endDate} endDateId="endDateId" focusedInput={focusedInput} onFocusChange={setFocusedInput} onDatesChange={(selectedDates) => { setStartDate(selectedDates.startDate); setEndDate(selectedDates.endDate); }} withPortal={true} // ★ orientation="vertical" // ★ />
ちょっと良い感じになりましたが、Material-UI の AppBar があると、上に被さってしまっています。これを修正するには、react-dates の Styles を上書きする必要があります。
Styles を上書きする
GitHub - react-dates#overriding-styles に記載があるように、Styles 上書き用の CSS ファイルを用意して、それをインポートする必要があります。要は、react-dates 自体に Style のカスタマイズは用意されていないということですね。自分で、Chrome DevTools などを使って、DatePicker のクラス名を特定する必要があります。
CSS ファイルの名前は何でもよいのですが、ここでは react-dates-custom.css
とします。これをインポートします。
import { DateRangePicker } from 'react-dates'; import 'moment/locale/ja'; import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; import './style/react-dates-custom.css'; // ★
react-dates-custom.css
の中身は下記の通りです。DatePicker を Material-UI の AppBar より上に表示させます。
.DateRangePicker_picker__portal { z-index: 10000; }
良い感じになりました。しかし、まだ問題点が残っています。このままだと日付を選択しない限り、DatePicker を閉じることができず、使いづらいです。
閉じるボタンを追加する
なら、ボタンを追加すればよし。DateRangePicker の renderCalendarInfo
に Material-UI の IconButton を渡してあげます。
<DateRangePicker ... withPortal={true} orientation="vertical" renderCalendarInfo={() => ( <IconButton aria-label="close" className={classes.close}> <ClearIcon /> </IconButton> )} />
Styles はこんな感じ。
const useStyles = makeStyles(() => createStyles({ close: { position: 'absolute', top: '5px', right: '5px', }, }) );
すると、画面右上にボタンが表示されます。ボタンをクリックして DatePicker を閉じることができます。
レスポンシブ対応
これまでの実装を踏まえて、DateRangePicker
コンポーネントをレスポンシブ化していきます。
この辺は、Material-UI の Hidden コンポーネントの出番ですね。スマホの場合と、そうでない場合で表示を切り替えるようにします。
実装は下記の通りになります。
import React, { useState } from 'react'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import Hidden from '@material-ui/core/Hidden'; import IconButton from '@material-ui/core/IconButton'; import ClearIcon from '@material-ui/icons/Clear'; import moment from 'moment'; import { DateRangePicker } from 'react-dates'; import 'moment/locale/ja'; import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; import './style/react-dates-custom.css'; const useStyles = makeStyles(() => createStyles({ close: { position: 'absolute', top: '5px', right: '5px', }, }) ); const MyDateRangePicker: React.FC = () => { const classes = useStyles(); const [startDate, setStartDate] = useState<moment.Moment | null>(null); const [endDate, setEndDate] = useState<moment.Moment | null>(null); const [focusedInput, setFocusedInput] = useState< 'startDate' | 'endDate' | null >(null); const dateRangePicker = (isMobile?: boolean) => ( <DateRangePicker startDate={startDate} startDateId="startDateId" endDate={endDate} endDateId="endDateId" focusedInput={focusedInput} onFocusChange={setFocusedInput} onDatesChange={(selectedDates) => { setStartDate(selectedDates.startDate); setEndDate(selectedDates.endDate); }} withPortal={isMobile} orientation={isMobile ? 'vertical' : 'horizontal'} renderCalendarInfo={() => isMobile ? ( <IconButton aria-label="close" className={classes.close}> <ClearIcon /> </IconButton> ) : ( <></> ) } /> ); return ( <> <Hidden xsDown implementation="js"> // ★ タブレットや PC の場合は表示する {dateRangePicker()} </Hidden> <Hidden smUp implementation="js"> // ★ スマホの場合は表示する {dateRangePicker(true)} </Hidden> </> ); }; export default MyDateRangePicker;
表示が切り替わる様子は、下記のリポジトリの README に gif を載せているので、そちらをご参照ください。
Github - react-dates-mobile-friendly
おわりに
今回は Material-UI と react-dates の DateRangePicker
コンポーネントを使ったレスポンシブ対応について解説しましたが、いかがだったでしょうか。
次回は、Date Picker を単体で持つDayPickerRangeController
コンポーネントのレスポンシブ対応について記事にしようと思っています。こっちはもう少しカスタムが多めになります。
今回は以上になります。最後まで読んで頂きありがとうございました!