内容に応じてサイズが可変する <textarea> を素敵に実装する

## 概要
内容に応じてサイズが可変する `textarea` を、できるだけ手間をかけず、スマートな実装を試みます。
しかも、ネイティブのフォームが持っている利点をそのまま活かして、堅牢でアクセシブルな設計を目指します。

## 標準 `textarea` の難点
HTML の `textarea` 要素は基本的に高さが固定されていて使い勝手が悪いです。３行分くらいしか領域がなくて、長い文章を打つのがとにかく苦痛なんていうこともザラです。

最近のブラウザ実装では、多少気を利かせてくれているのか、テキストエリアの領域をドラッグで拡大・縮小できます。

![textarea をマウスドラッグでリサイズしているところの動画。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43689/c462fd99-cfc8-8272-57b8-ed571a8c8f21.gif)

ただ私は思うのです。めんどくさいし、最初っから、入力するテキスト量に応じて自動的に伸び縮みしてくれればいいのに……と。スクロールバーなんて、１ページにひとつあればじゅうぶんなんですよ。

## 実装方法
難しいことはありませんが、HTML と CSS、JS が協調して動作します。

### HTML
```html
<label for="FlexTextarea">伸縮するテキストエリア</label>
<div class="FlexTextarea">
  <div class="FlexTextarea__dummy" aria-hidden="true"></div>
  <textarea id="FlexTextarea" class="FlexTextarea__textarea"></textarea>
</div>
```

### CSS
```css
.FlexTextarea {
  position: relative;
  font-size: 1rem;
  line-height: 1.8;
}

.FlexTextarea__dummy {
  overflow: hidden;
  visibility: hidden;
  box-sizing: border-box;
  padding: 5px 15px;
  min-height: 120px;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: break-word;
  border: 1px solid;
}

.FlexTextarea__textarea {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  overflow: hidden;
  box-sizing: border-box;
  padding: 5px 15px;
  width: 100%;
  height: 100%;
  background-color: transparent;
  border: 1px solid #b6c3c6;
  border-radius: 4px;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  resize: none;
}

.FlexTextarea__textarea:focus {
  box-shadow: 0 0 0 4px rgba(35, 167, 195, 0.3);
  outline: 0;
}
```

### JavaScript
```javascript
function flexTextarea(el) {
  const dummy = el.querySelector('.FlexTextarea__dummy')
  el.querySelector('.FlexTextarea__textarea').addEventListener('input', e => {
    dummy.textContent = e.target.value + '\u200b'
  })
}

document.querySelectorAll('.FlexTextarea').forEach(flexTextarea)
```

コードの大半は CSS ですし、JavaScript もなんかしてる？　ってくらい簡素なものです。ですがしっかり動きます。

## 動作サンプル
[サイズ可変の textarea の動作サンプル（CodePen）](https://codepen.io/tsmd/pen/wvBGGWB)

## 解説
コードこそ簡素なものですが、とりあげて解説したい箇所はたくさんあります。

### 動作原理の概要
ざっくりと説明します。`textarea` 要素は親要素の大きさに追随して大きさが決まるようになっています。`position: absolute` で絶対配置をし、同時に `width` と `height` を `100%` を指定することで、親要素の大きさと同一にしています。

```css
.Textarea__textarea {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
```

そして親要素がテキストの量に応じた大きさになるように、入力されたテキストを `.Textarea__dummy` にそのまま流し込みます。

```javascript
textarea.addEventListener('input', e => {
  dummy.textContent = e.target.value
})
```

すると、高さを指定していない `.Textarea__dummy` はテキスト量に応じて自動的に大きさが決まり、連動して `textarea` の大きさも決まるという按配です。

![textarea がテキスト内容に応じて拡大している様子。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43689/ac6828cc-01e1-0412-5e85-88bc0dd95b6d.gif)

[原理解説用の 3D 表示の FlexTextarea](https://codepen.io/tsmd/pen/wvBzvNe)

基本はこれだけです。続いて、細かい部分を解説していきます。

### 最小・最大の高さを指定する
何もテキストが入力されていないときの最小の高さと、たくさん入力されたときの最大の高さを設定できます。設定しないことも可能で、`min-height` を指定しなければ１行分の高さになり、`max-height` を指定しなければ、内容に応じてどこまででも大きくなります。個人的には `min-height` の指定のみで良いだろうと思います。

```css
.FlexTextarea__dummy {
  min-height: 120px;
  max-height: 480px;
}
```

### 入力された改行を改行として表示する
テキストエリアの値をそのまま `.Textarea__dummy` に流し込むと、改行が半角スペースに変化してしまいます。これは DOM において、改行文字はホワイトスペースとして等価に扱われてしまうためです。とても有名な話ですが、HTML で改行をするには `<br>` タグを使う必要があります。

ですが、改行文字を改行として反映するための CSS プロパティがあります。

```css
.FlexTextarea__dummy {
  white-space: pre-wrap;
}
```

こうしておけば、次のような単純なテキスト代入でも、改行文字が改行として表示されるようになります。

```javascript
textarea.addEventListener('input', e => {
  dummy.textContent = e.target.value
})
```

「改行文字を `<br>` に文字列置換して、`innerHTML` に代入する」ようなアプローチは、セキュリティ脆弱性を孕みやすいので、ぜったいにやめてね！！

### 末尾の改行を無視されないようにする
`white-space: pre-wrap` はとても便利なプロパティですが、落とし穴が１点あります。テキスト末尾の改行が表示に現れないのです。たとえばテキストエリアに「TEST＜改行＞＜改行＞＜改行＞」と入力したときに `dummy` に期待する高さは４行ぶんの高さですが、３行分の高さにしかなりません。

そこで、ゼロ幅スペース（U+200B）を末尾に埋め込むことで、末尾の改行も高さに含まれるようにしています。ゼロ幅なので、テキストのフローに影響することはありません。

```javascript
textarea.addEventListener('input', e => {
  dummy.textContent = e.target.value + '\u200b'
})
```

### `textarea` と `dummy` の細かな挙動を統一する
ここが最も難所です。

テキストエリアに設定される大きさは、完全に `dummy` の表示に依存しています。そのため、「何文字で改行するか」「改行ごとにどれくらい大きさが変わるか」「禁則処理ルール」などの点を同一にしていかなくてはいけません。条件を同一にするためのファクターは以下のとおりです。

1. フォント
2. 文字サイズ
3. 行の高さ
4. 文字間隔
5. 罫線の幅
6. padding の大きさ
7. 改行できない文字が連続したときの取扱い

これらを解決するために、以下のコードが必要になります。

```css
.FlexTextarea__dummy {
  box-sizing: border-box;
  padding: 5px 15px;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: break-word;
  border: 1px solid;
}

.FlexTextarea__textarea {
  box-sizing: border-box;
  padding: 5px 15px;
  border: 1px solid #b6c3c6;
  font: inherit;
  letter-spacing: inherit;
}
```

テキストエリアにはたいてい、`border` や `padding` の設定が必要になるでしょう。それにあわせて `dummy` にも同じ大きさで `border` と `padding` を指定しています。また、ボックスの大きさ計算の基準をそろえるために `box-sizing: border-box` も必要です。

文字周りをそろえるために、`font: inherit`, `letter-spacing: inherit` を指定します。これで、フォント、ウエイト、行の高さが揃います。`letter-spacing: inherit` も必要です。

テキストエリアは「aaaaaaaaaaaa……」と入力されたとしても、矩形を突き出ず、必ず改行される仕様になっています。`dummy` もそれにあわせるため、 `overflow-wrap` と、後方互換のための `word-wrap` プロパティも設定します。

### 手動リサイズは行わないようにする
テキストエリアが自動的にリサイズされるようになったので、右下に表示されているリサイズハンドルは不要になりました。

```css
.FlexTextarea__textarea {
  resize: none;
}
```

### フォーカスリングを表示する
伸縮可能かどうかに関係はありませんが、テキストエリアがフォーカスを受け取ったときに、ちゃんとそれとわかるインジケーターを表示してあげましょう。たいていはデフォルトの `outline` で良いでしょう。余裕があれば独自のインジケーターを表示させるとカッコいいでしょう。

```css
.FlexTextarea__textarea:focus {
  box-shadow: 0 0 0 4px rgba(35, 167, 195, 0.3);
  outline: none; /* box-shadow を代わりに使っているから outline は不要 */
}
```

### スクリーンリーダー等の支援技術に配慮する
`.FlexTextarea__dummy` には、テキストエリアに入力された文字がそのまま流し込まれています。同要素には `visibility: hidden` プロパティが設定されているとはいえ、念のため HTML にも、`aria-hidden` 属性を使って、この要素がコンテンツとは無関係であることを示しておきましょう。

```html
<div class="FlexTextarea__dummy" aria-hidden="true"></div>
```

## 完成
このようにして出来上がった FlexTextarea コンポーネントを皆さんお使いください。一番上に書いたものが完成系のコードです。