yuw27b’s blog

技術メモと雑記

ReactでModal windowを実装する

汎用コンポーネントも存在しますが、自作のミニマル実装コードです。

ひとまずコード(スタイルなどは簡略化しています)

import React from 'react';
import PropTypes from 'prop-types';

class ModalWindow extends React.Component {
  constructor(props) {
    super(props);
    this.eventListener = this._handleEscKey.bind(this);
    this.state = {};
  }

  componentDidMount() {
    document.body.classList.add('openModal');
    document.addEventListener('keydown', this.eventListener);
  }

  componentWillUnmount() {
    document.body.classList.remove('openModal');
    document.removeEventListener('keydown', this.eventListener);
  }

  _handleEscKey(e) {
    const keyCode = parseInt(e.keyCode, 10);
    if (keyCode !== 27) {
      return false;
    }
    this.props.onClose();
  }

  render() {
    const { children, onClose } = this.props;
    return (
      <div className="modalWin_wrapper" onClick={(e) => e.target === e.currentTarget ? onClose() : null}>
        <div className="modalWin_inner">
          <button className="modalWin_close" onClick={() => onClose()}>
            &times;
          </button>
          {children}
        </div>
      </div>
    );
  }
}

ModalWindow.propTypes = {
  children: PropTypes.node.isRequired,
  onClose: PropTypes.func.isRequired
}

export default ModalWindow;
.modalWin_wrapper {
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, .5);
  z-index: 10000;
}

.modalWin_inner {
  position: relative;
  width: 1000px;
  height: 100%;
  margin: 0 auto;
  padding: 0 0 20px;
  background: #FFF;
  overflow: scroll;
  box-sizing: border-box;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, .5);
}

.modalWin_close {
  display: block;
  position: absolute;
  width: 40px;
  height: 40px;
  top: 0;
  right: 0;
  border: 0;
  background: #666;
  font-size: 40px;
  line-height: 1;
  color: #FFF;
  text-align: center;
  outline: none;
}

body.openModal {
  overflow: hidden;
}

使うときは、

<ModalWindow onClose={this.onClose.bind(this)}>
  <div>Content</div>
</ModalWindow>

のようにする。


最低限、

  • モーダルウィンドウの外側(ここではmodalWin_wrapperクラスのdiv要素)をクリックしたら閉じる
  • Escキーを押したら閉じる
  • モーダルウィンドウ内にも閉じるボタン(ここでは.modalWin_closeクラスのbutton要素)
  • 閉じる時にActionの発行をしたいので、閉じるための関数は外部から渡す

を実装しています。

Escキーが押されたことを検知するにはイベントリスナーの登録が必要なので、componentDidMount()内でdocument.addEventListener('keydown', this.eventListener);しています。そしてcomponentWillUnmount()で削除します。削除する時に同じ関数を引数にする必要があるので、関数はコンストラクターで定義してしまっています。

もう一点、モーダルウィンドウを開いている状態でスクロールをすると、後ろに隠れているコンテンツもスクロールしてしまいます。これではあまり使い勝手がよろしくないので、モーダルウィンドウが開いている間はbody要素にoverflow: hiddenを設定しています。
body要素はReactのマウントポイントの外側にあるため、直接DOMツリーからアクセスしています。この部分:document.body.classList.add('openModal');

videoタグのダウンロードオプション

HTMLのvideoタグで動画を表示すると、Chromeで右下のメニューをクリックすると「ダウンロード」という項目があります。(他のブラウザでは確認できませんでした。)
これは、controlslist属性に"nodownload"というパラメータを指定すると非表示になります。

controlslist属性には他にも様々な値が指定できますが、HTML標準にはなってないようです。
<video>: 動画埋め込み要素 - HTML: HyperText Markup Language | MDN
[HTMLMediaElement] Add a controlsList/controlslist attribute. by avayvod · Pull Request #2426 · whatwg/html · GitHub
(MDNのページには実験段階を示すアイコンがついているし、WHATWGの提案はマージされていない様子。)

<video
  class="mediaWrapper_video"
  src="video.mp4"
  type="video/mp4"
  controls
  controlslist="nodownload"
>
</video>

これでChromeでも「ダウンロード」というメニューは表示されなくなりますが、動画の上で右クリックするとコンテキストメニューに「名前をつけて動画を保存」という項目は表示されたままです。できれば保存されたくない、という要望があった場合、やはり右クリックも無効化するしかなさそうです。

document.querySelector('.mediaWrapper').addEventListener('contextmenu', function(e) {
  e.preventDefault();
  return false;
});

ユーザビリティの面からは、コンテキストメニューの非表示はあまりやりたくないので、せめて動画の部分(ここではmediaWrapperクラス)だけを無効化。)


もちろんソースコード上では動画のURLが見えていてダウンロードできてしまうので、厳密にやるのであればストリーミング配信するしかないですが、「なるべくなら保存されたくない」というような場合は、現状はこれがベターでしょうか。

GETで見られたくないものを送信するときはヘッダーに入れる

HTTPSで通信していても、GETのクエリパラメータは暗号化されないので、見られたくないものを送信するときは、POSTで送るか、HTTPのリクエストヘッダーに入れるようにしています。

クエリパラメータはURLの一部なので、https://example.com?page=1みたいな平文が公衆Wi-Fiの通信に乗ったり、中間サーバのログに残ったりする可能性があります。ページ番号であればそれでも構いませんが、例えばログインリクエストの場合にはhttps://example.com?user=xxxxx&password=yyyyyのような情報を送ることになるので、これは見られては困ります。

単純にPOSTリクエストのリクエストボディに入れても良いのですが、リクエスト先がRESTfulなAPIで、セマンティクス的にGETがふさわしい場合(サーバのリソースを書き換えないリクエスト)には、HTTPのカスタムヘッダーを使うようにしています。

例:
https://example.com/api/login のようなURLのエンドポイントにGETリクエストを送るときに、
MyApp-User-Authorization:{ユーザー認証のための文字列}
のようなカスタムヘッダーを追加します。

「ユーザー認証のための文字列」には、「ログイン名:パスワード」をbase64エンコードしたものを入れて、サーバ側でデコード・パースして認証を行います。

このやり方は、cybozuさんのAPIを勝手に参考にさせていただいたものです:
kintone REST APIの共通仕様 – cybozu developer network

上記のAPI仕様書によると、cybozuさんのカスタムヘッダーは「X-Cybozu-Authorization」という名前ですが、カスタムなヘッダーの場合に接頭辞を「X-」とする、という習慣は、現在は非推奨とのことなので、シンプルにプロダクトのサービス名から始めるようにしています。

「X-」接頭辞についての参考資料:
HTTP ヘッダー - HTTP | MDN
RFC 6648 - Deprecating the "X-" Prefix and Similar Constructs in Application Protocols

PHPがセッションIDを発行しなくなった

突然PHPsession_id()が空白を返すようになってしまった

もちろん本当に「突然」なのではなく、何かいじってしまったからですが。

PHPのセッション情報は、デフォルトではファイルに保存される

ので、そのファイルの読み書きができなければセッションIDも発行されません。

phpinfo();

でセッションの項目を確認。
session.save_handlerfilesの場合、
session.save_pathに設定されているディレクトリに読み書きの権限があるかを確認。



なぜそんなことになったのかの推測(自分メモ)

対象ディレクトリのユーザーがroot、グループがapacheになっていた。
当該サーバではApacheではなくNginxを使っているので、グループがapacheなのはおかしいようにも思える。
おかしくなる直前に、PHPの追加モジュールを入れたりしていたので、そこでパーミッションをいじってしまったか、グループがapacheに変わってしまったのかもしれない。

letter-spacingとtext-align: center(とcalc())

letter-spacingとテキストの中央揃えを併用すると、左右の空白がずれて見えるのでpaddingで調整することがよくある。

例えばこんな見た目のボタンを作るのに、
f:id:yuw27b:20201105213339p:plain

padding: 5px;
font-size: 16px;
letter-spacing: 1em;
text-align: center;
/*〜略〜*/

とする。
(全角スペースで表現されたデザイン画を受け取ることもあるけど、HTMLに「検 索」と入れるのはなしですよね。ぱっと思いつくところだとアクセシビリティの問題とか。)

そうすると、letter-spacingは各文字の後ろに入るので、右側の余白が大きく見えてしまう。
f:id:yuw27b:20201105213414p:plain

これをpaddingで調整するのに、

padding: 5px 5px 5px 21px; /* 5 + 16 = 21 */

でも良いのだが、ここ数年はモダンブラウザであれば「calc()」が問題なく使えるので*1

padding: 5px 5px 5px calc(5px + 1em);

としておくと、コードの意図がより分かりやすくなって余白の調整もやりやすい。
f:id:yuw27b:20201105213433p:plain