Reactで右クリックした場所にメニューを表示してみたい。

# Reactで右クリックメニュー
先日書いた[React+Railsで付箋っぽいアプリを作る](https://qiita.com/hanaitama-kaz/items/5b68b9c665f75020bcd4)中で、右クリックメニューを作ってみたいなぁ、と、調べた内容と実装方法を書いていきたいと思います。

## oncontextmenuのイベントハンドラを作れば良い
[JavaScriptのドキュメント](https://developer.mozilla.org/ja/docs/Archive/Mozilla/XUL/PopupGuide/ContextMenus)によれば、oncontextmenuイベントをハンドルすることで、独自のメニューが作れそうです。

## 実装方針
ここでは、以下の方針で実装してみることにしました。

1. 右クリックした場所にメニューを表示する。
1. メニューの要素は、Reactのコンポーネントとして作ってみる。
1. 環境は、[React+Railsで付箋っぽいアプリを作る](https://qiita.com/hanaitama-kaz/items/5b68b9c665f75020bcd4)の環境をそのまま使いましたが、多分Reactの動く環境なら、どこでも大丈夫。

# 実装だー
作るのは、以下の3つです。
1. メニューを表示するコンポーネント(menu.js)
1. 右クリックを受け付ける親コンポーネント(parent.js)
1. スタイルシート(parent.css)

## メニューを表示するコンポーネント
適当なメニューと"close"が選べるポップアップを表示させます。

```jsx:menu.js
import React from 'react';
import PropTypes from 'prop-types';

class Menu extends React.Component {
  // コンストラクタ
  constructor(props){
    super(props);        // おまじないですね。

    // メニューを表示するdiv要素を参照するための変数です。
    this.menuElm = null;

    // イベントハンドラのバインド
    this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
    this.onMenuItemClick = this.onMenuItemClick.bind(this);
    this.onKeyUp = this.onKeyUp.bind(this);

    // メニューに表示するアイテムの配列です。(適当すぎてやばい。)
    this.messages = ["You", "are", "incredible"];

  }

  // メニュー要素を表示(visibility="visible")します。
  // 親要素から、呼び出されるメソッドです。
  show(clientX, clientY){
    // 以下のようにstyle.top,style.leftを指定することで、好きな場所にメニューを表示できます。
    this.menuElm.style.top = clientY + "px";
    this.menuElm.style.left = clientX + "px";
    this.menuElm.style.visibility = "visible";

    // 表示したらフォーカスを割り当てます。
    // (これで、keyイベントを受け付けてくれるようになります。)
    this.menuElm.focus();
  }

  // メニューを閉じ(visibility="hidden"に変更し)ます。
  close(){
    this.menuElm.style.visibility = "hidden";
  }

  // "close"クリック時のイベントハンドラ
  onCloseButtonClick() {
    // close()を呼び出します。
    this.close();
  }

  // メニューアイテムクリック時のイベントハンドラ
  onMenuItemClick(event) {
    // 自分自身を閉じて
    this.close();

    // 親要素から渡されたコールバック関数を呼び出します。
    // 引数には、アイテムに表示されているテキストを渡します。
    this.props.onMenuItemClick(event.target.innerHTML);
  }

  // エスケープキーで閉じるためのイベントハンドラです。
  onKeyUp(event) {
    event.preventDefault();
    // 文字列で比較できるとは思いませんでした。。
    if ("Escape" == event.key) {
      this.close();
    }
  }

  // レンダラー
  render(){
    return (
      <React.Fragment>
        { /* refで要素を参照することで、styleの変更ができるようになります。 */ }
        <div className="MenuBox" ref={(node) => this.menuElm = node} onKeyUp={this.onKeyUp} tabIndex="0" >
          {
            this.messages.map((message) => 
              <div className="MenuItem" onClick={this.onMenuItemClick} key={ message }>{ message }</div> 
            )
          }
          <div className="MenuItem" onClick={this.onCloseButtonClick}>Close</div>
        </div>
      </React.Fragment>
    );
  }
}

Menu.propTypes = {
  onMenuItemClick: PropTypes.func
};

export default Menu;
```

## 右クリックを受け付ける親コンポーネント
自身の領域が右クリックされたら、メニューを表示させます。
さらに、メニューで選択された内容に応じて、自身の表示内容を変化させてみました。

```jsx:parent.js
import React from 'react';
import Menu from './menu';

class ParentPage extends React.Component {
  // コンストラクタ
  constructor(props){
    super(props);

    // stateを初期化
    this.state = {message: "Please Click Anywhere you like."}

    // メニュー要素への参照を初期化（後ほどレンダラーの中でrefを割り当てます。）
    this.menu = null;

    // イベントハンドラのバインド
    this.onContextMenu = this.onContextMenu.bind(this);
    this.onMenuItemClick = this.onMenuItemClick.bind(this);
  }

  // 右クリックイベントハンドラ
  onContextMenu(event) {
    // preventDefault()を忘れると、普通の右クリックメニューが表示されますよ。
    event.preventDefault();

    // メニュー要素の"show()"メソッドを呼び出します。
    // 引数にはマウスポインタの位置情報を渡してあげます。
    this.menu.show(event.clientX, event.clientY);
  }

  // 右クリックメニューでメニューが選択された際にコールバックしてもらうメソッドです。
  // 選択されたメニューの内容(innnerHTML)をstateに設定しています。
  // (これにより、画面左上のメッセージが切り替わるはず。)
  onMenuItemClick(message) {
    this.setState({message: message});
  }

  // レンダラー
  render(){
    return (
      <React.Fragment>
        { /* 自身の右クリックイベントハンドラをonContextMenu=で指定 */ }
        <div className="ParentBox" onContextMenu={this.onContextMenu} >
         { this.state.message }
         { /* コンポーネントもrefで参照できるので、子要素のメソッドを呼び出すことが可能になります。 */ }
         <Menu onMenuItemClick={this.onMenuItemClick} ref={(node) => this.menu = node} />
        </div>
      </React.Fragment>
    );
  }
}

```

## スタイルシート
ブラウザのUIは、結局のところスタイルシートで「それっぽく見せている」だけなんですね。
何かの本にこんなことが書いてありました。
「コンピュータ使って実現しているものっていうのは、『そのように見える』だけのハリボテに過ぎない」
まさしくその通りだなぁ、と、改めて思うのでした。

```scss:parent.scss
// 親要素のスタイル
div.ParentBox {
  font-weight: bold;
  position: relative;
  width: 100vw;
  height: 100vh;
  background-color: #FFFFFF;
  border: 1px solid #000000;
}

// メニュー要素のスタイル
// position: absoluteにしないと、指定した場所に表示できないのでご注意を。
div.MenuBox {
  position: absolute;
  margin: 0px;
  padding: 5px;
  font-size: 10px;
  font-weight: thin;
  width: 100px;
  background-color: #888888;
  color: #000000;
  visibility: hidden;
  border-radius: 5px;
}

// focusをあてたときに周りが光らないようにしました。(気分の問題)
div.MenuBox:focus {
  outline: none;
}

// 各メニューにマウスポインタが乗っかった時のスタイルです。
div.MenuItem:hover {
  cursor: pointer;
  background-color: #E0E0FF;
}

```

# 実験
殺風景ですが、クリックしたところにメニューが表示される感じが実現できたはずです。
メニューを選択すると、メニューに表示されていた文字列が、そのまま親ページの右肩に表示されると思います。
Escapeボタンでも、"Close"のリンクでもメニューを消すことができます。
<img width="308" alt="スクリーンショット 2020-05-26 20.44.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244595/71a43eb4-f675-9cb8-d9cd-810ace94232f.png">

# 補足
今回の内容については、DBが関係ないので、Railsの環境は正直不要で、Reactの環境だけあれば実験できます。
npmとwebpackだけで動作させた際のwebpack.config.jsの内容と、ちょっとしたソースの修正内容を記載しておきます。

## ディレクトリ構成
特段特別なものはありません。
publicにindex.htmlを置いて、src配下にスクリプトとスタイルシートを置きました。

```bash:shell
app
├── node_modules(配下は割愛)
├── package-lock.json
├── package.json
├── public
│   └── index.html
├── src
│   ├── menu.js
│   ├── parent.js
│   └── parent.scss
└── webpack.config.js
```

## webpack.config.js

```javascript:app/webpack.config.js
module.exports = {
  mode: "development",
  entry: {
    app: "./src/parent.js"
  },
  output: {
    path: __dirname + '/public/js',
    filename: "[name].js"
  },
  devServer: {
    contentBase: __dirname + '/public',
    port: 8080,
    publicPath: '/js/'
  },
  devtool: "#inline-source-map",
  module: {
    rules: [{
      test: /\.js$/,
      enforce: "pre",
      exclude: /node_modules/,
      loader: "eslint-loader"
    },{
      test: /\.css$/,
      loader: ["style-loader","css-loader"]
    },{
      test: /\.scss$/,
      loader: ["style-loader","css-loader","sass-loader"]
    },{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }]
  }
};

```

## public/index.html
ブラウザからアクセスするためのトップページです。

```html:public/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1,shrink-to-fit=no" />
    <meta http-equiv="X-UA-Compatible" content="IE=Edge, chrome=1" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript" src="js/app.js" charset="utf-8"></script>
  </body>
</html>
```

## src/parent.js
最後に、メインエントリポイントになるparent.jsの修正部分を書いておきます。
明示的にimportしないとスタイルが読み込まれなかったので、その対処と、
ReactDom.renderの呼び出しを追加しました。

```jsx:src/parent.js
// 1,2はファイルの先頭(import文の並び)に追加
// 1. ReactDomの追加
import ReactDom from 'react-dom';

// 2. スタイルの参照を追加
import './parent.scss';

// ： (中略)

// 3はファイルの最後に追加
// 3. レンダラーの呼び出し
ReactDom.render(
  <ParentPage />,
  document.getElementById('root')
);

```