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/core@4.11.0
react-dates@21.8.0
react@16.13.1
typescript@3.7.5

基本的な使い方

インストール

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 である withPortaltrue にすると、DatePicker が全画面表示のようになります。また、orientationvertical にすると、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 コンポーネントのレスポンシブ対応について記事にしようと思っています。こっちはもう少しカスタムが多めになります。

今回は以上になります。最後まで読んで頂きありがとうございました!