ContentEditable を用いた Auto Expandable(?)なテキストエリアの実装

ContentEditableとは

HTML 要素を編集可能にする attribute *1*2HTML5 で実装された。
基本的な機能はほぼすべての主要なブラウザで実装されている。 *3

どんなときに使うか?

複数行の入力欄を実装するときにまず思いつくのは textarea *4 だと思う。
しかし標準の textarea では特定の文字の色を変えたり、WYSIWYG*5 エディタのようなリッチなテキストエリアを実装することができない。
ContentEditable を用いることによってリッチなテキストエリアを実現することができる。

Twitter のツイートの入力欄にも使われている。

twitter-draft-editor
Twitter のツイート欄

今回のTopic

一般的に textarea は縦方向に対して auto expandable(適当に名付けた。autoresize?)ではない。
表示される高さ(row count)が固定されており、行数が増えた場合は scrollable になる。
行数が変わったときに要素の高さも変えたい場合、不便である。

以下のような解決手法があるが、実装が面倒であったり自然な挙動にならない場合がある。

  • JavaScript を用いて仮想的にコンテンツを描画して高さを取得し、動的に調整する
  • JavaScript を用いて 改行 を検知し、 row count を変更する

そこで、適当な HTML element に対して contenteditable を付与することによって擬似的に auto expandable な textarea を実現できる。

実際に以下のコードで試してみる。

<p classname="Editor" role="textbox" contenteditable="true">
  Hello World
</p>

contenteditable-demo
contenteditable のデモ

React で使ってみる

普段 React を使って開発しているので React で使うときの注意点をいくつか書いておく。
contenteditable で変更を行った値は React で管理していないため、正しく store していないと "変更した結果が保存されないよ!" という旨の warning がでる。*6 suppressContentEditableWarning={true} を指定すると warning は消える。

contenteditable をつけたからと言って onChange が実装されているわけではないので、onChange は動かない。
そこで onBlur + ref などを用いてフォーカスが外れた際などに保存する必要がある。 onKeyDown は動くが、マウスを使って貼り付けを行った場合などは onKeyDown event は発行されないので注意する必要がある。

(追記 2021/05/25) input event が発行されるのでそちらを使ったほうが良さそう。*7

その他

  • フォーカスしたときに ブラウザが outline を勝手につけた場合は outline: 0px solid transparent;*8 を指定することによって outline を消せる
  • role="textbox" を指定することによってブラウザに入力可能要素であることを伝えられる*9

Example

サンプルコード

/* styles.css */
.App {
  font-family: sans-serif;
}

.Editor {
  padding: 16px;
  border-radius: 6px;
  border: solid 1px rgba(0, 0, 0, 0.12);
  background-color: rgba(0, 0, 0, 0.03);
  outline: 0px solid transparent;
}

.Editor:active,
.Editor:focus {
  background-color: rgba(0, 0, 0, 0.06);
  outline: 0px solid transparent;
}
/* App.jsx */
import { useState, useRef } from "react";
import "./styles.css";

export default function App() {
  const [value, setValue] = useState("Hello, World!");
  const ref = useRef(null);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
        <p
          className="Editor"
          ref={ref}
          role="textbox"
          contentEditable={true}
          suppressContentEditableWarning={true}
          onBlur={() => {
            setValue(ref.current.innerText);
          }}
        >
          {value}
        </p>
        <p>{value}</p>
    </div>
  );
}

最後に

ContentEditable を使って Auto Expandable なテキストエリアを実現する方法について書いてみたが、実際のプロダクトではメンテナンス性や安定性を考えると third-party のライブラリを使ったほうが良い。