ContentEditable を用いた Auto Expandable(?)なテキストエリアの実装
ContentEditableとは
HTML 要素を編集可能にする attribute *1*2 で HTML5 で実装された。
基本的な機能はほぼすべての主要なブラウザで実装されている。 *3
どんなときに使うか?
複数行の入力欄を実装するときにまず思いつくのは textarea *4 だと思う。
しかし標準の textarea では特定の文字の色を変えたり、WYSIWYG*5 エディタのようなリッチなテキストエリアを実装することができない。
ContentEditable を用いることによってリッチなテキストエリアを実現することができる。
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>
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 のライブラリを使ったほうが良い。
*1:https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content
*2:https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable
*3:https://caniuse.com/?search=contenteditable
*4:この記事中では "textarea" は HTML 要素を指し、 "テキストエリア" は複数行の入力欄を指す
*5:最近なんの略か覚えたのでスペルで迷わなくなった
*6:https://stackoverflow.com/questions/49639144/why-does-react-warn-against-an-contenteditable-component-having-children-managed
*7:https://stackoverflow.com/questions/1391278/contenteditable-change-events
*8:https://www.w3schools.com/howto/howto_css_contenteditable_border.asp
*9:https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/textbox_role