この記事は第2のドワンゴ Advent Calendar 2018 25日目の記事です。
ドワンゴでニコニコ生放送のWebフロントエンジニアをやっています、 @misuken です。
- 14日目の React + TypeScript における ViewComponent の美しい合成技術
- 18日目の 超簡単に数値を 1,234 1.2万 12.3万 123万 1,234万 1.23億 ... に整形する方法
- 23日目の AtomicDesign の atom より小さな世界の扉を開く
- 24日目の LayeredAtomicDesign でコンポーネントの粒度問題を解決する
に続いての投稿です。
今回のお題
前回の記事で紹介した LayeredAtomicDesign を用いて、実際にデザインカンプからコンポーネント設計と仮実装を行ってみるというものです。
コードは React を想定していますが、設計部分はvueでもなんでも参考になるのではと思います。
今回も設計する対象とさせて頂くのは Qiita さんの記事投稿ページです。
デザインカンプという想定のスクショ
今回設計するのはこちらです。
プレースホルダーっぽく表示されている部分は文章量などの関係で設計の対象外としますが、
ヘッダーを除いた記事を書いて投稿する部分全体のコンポーネント設計を行っていきたいと思います。
それでは、早速始めてみましょう。
デザインカンプを見た所感
第一印象としてはこんな感じ。
- タイトルフィールドは投稿状態があるくらいで簡単そう
- タグフィールドは入力欄を置くだけの印象
- 記事を書く部分はヘッダーっぽいところの分類に少し気を遣いそう
- プレビュー部分はヘッダーっぽいところの
[<][>]
ボタンを押すと表示場所などが変化するので、結構設計がややこしそう - 投稿ボタンは ComboButton を使えそう
コンポーネントの境界やコンポーネント名を固めていく
簡単そうなところは良いとして、難しそうなところを整理していきます。
まず、記事を書く部分とプレビュー部分は、左と右で2つのコンポーネントがあることがわかります。
左側は本文の 編集エリア で、右側が 本文のプレビューを表示するエリア という感じでしょうか。
ざっと名前を付けると 編集パネル と プレビューパネル。
しかし、編集パネル と プレビューパネル だと、何を編集するのか?何をプレビューするのか?がわかりません。
視覚的にも会話的にも何の文脈も無く 編集パネル や プレビューパネル と言われても、何を指しているか正確に認識することはできないでしょう。
名前を考えつつ、定期的に頭に入ったコンテキストを空にして間違っていないか確認することが大切です。
ちゃんとその物自体の意味がわかるところまで名前を調整していく必要があります。
記事本文の編集パネル、記事本文のプレビューパネル。
ArticleTextEditorPanel
ArticleTextPreviewPanel
Editor と Preview の釣り合いが取れていないので Editor と Viewer に調整します。
ArticleTextEditorPanel 記事本文編集パネル
ArticleTextViewerPanel 記事本文閲覧パネル
これでだいぶ良くなりました。
エディタとビューアはパネルなのか
あと気になるのは、パネルなのかどうか。
パネル ではなくて エディタ自体、ビューア自体 なのかどうか。
一旦、エディタのスコープを見直してみます。
例えば、画像挿入ボタン、絵文字挿入ボタン はエディタのスコープに含まれるかどうか。
ボタンを押すことで、エディタの内容を書き換えるように、ボタンは一連の編集における流れの一部を担っていると言えます。
つまり、画像挿入ボタン、絵文字挿入ボタン はエディタのスコープに含まれていると言えます。
ヘルプボタン も 良い記事を書くには のリンクも、本文を編集するときに有益な情報を提供するものですし、
左上の 本文 はエディタの内容を指すラベルなので、この範囲までがエディタのスコープに含まれていると言えます。
エディタのスコープが決まったところで、ビューアのほうも見てみましょう。
ビューアには スライドモード という チェックボックス があります。
記事本文を複数の視覚表現、つまり ビュー を切り替えるための操作で、これはエディタの 挿入ボタン と同等であると言えます。
プレビュー の文言は ビュー の内容に対するラベルです。
残るは エディタ や ビューア のサイズを切り替えるボタン。
これは何でしょうか。
まず、これが何をどうするボタンなのかを考えます。
例えば エディタ や ビューア または パネル を閉じたり開いたりするボタンといった具合にです。
ただ、閉じるという行為には大抵×印のボタンが使われますし、 エディタ を閉じているのか、 ビューア を開いているのか、表現が曖昧な感じになることに気付きます。
こういうときに効果的な視点は、このサービスが提供しているヘルプページの説明文を自分で書いたと仮定して、ユーザーがそれを読んだときにどのように行動するかを想像してみることです。
例えば、
「〜かくかくしかじか〜したい場合は、エディタを閉じて下さい」
「〜かくかくしかじか〜したい場合は、ビューアを閉じて下さい」
と説明文が書かれていたとして、そのサイトに初めて来たネットリテラシーのある程度低い人がどのように行動するか。
自分の頭の中のコンテキストをクリアして説明に導かれるままに操作するイメージでシミュレーションしてみましょう。
「エディタを閉じて下さい」 は人によってはブラウザのタブごと閉じるでしょう。
「ビューアを閉じて下さい」 は、ビューアを認識できたとしても右上の×ボタンを連想して探そうとするでしょう。
このように、わずかにでも表現にブレがあると、人は意図しない行動をとってしまいます。
予めそのアクションがパネルの開閉であるというコンテキストを持ってもらえていれば伝わるかもしれませんが、同じコンテキストを予め万人に持ってもらうことは不可能です。
では、どのようにすればよいのか。
まず言えることは、この行き違いに繋がる歪は 閉じる開く という表現が合っていないという仮説です。
そもそも 閉じる開く という表現が適切ではない物に、それを当てはめて考えても最適な答えは出ません。
ここで、また視点を変えましょう。
目の前にユーザーさんがいるとして、その人に「あのボタンを押そうと思う言葉を投げかけて下さい」と言われたらどのように声をかけますか?
つまりユーザーが何か目的を持って、それを実行に移そうとしたとき、適切に実行できる言葉が正解と言えるのではないかということです。
自分なら「エディタの領域を広げて下さい」と「ビューアの領域を縮めて下さい」と声をかけます。
少なくとも、ブラウザのタブを閉じたり、右上の×ボタンを連想する人はいないはずです。
しかし、実際はあのボタンを押す人よりも、真ん中の境界をドラッグするのではないかと考えます。
ボタンは押してもらえないわけですが、声掛けが適切ではなかったのかというと疑問が残ります。
なぜなら、目的を達成する上で、あのボタンを押すよりも、真ん中の境界をドラッグできたほうが解りやすく合理的ということを示しているとも考えられるからです。
たしかに、ドラッグで境界を移動できれば、ボタンを押すことで実現してもらおうとしていた目的は達成できるわけです。
単純に今はそういう機能が存在しないだけで、境界上でマウスカーソルが ⇔
に変化したのなら、領域のサイズを変えられることに気付く人も増えるでしょう。
必ずしもそうではないかもしれませんが、それが正しいとして話を進めると、
あのボタンの動作は、対象を閉じたり開いたりするものではなく、対象を広げたり縮めたりするものだったということになります。
パネルの正体とは
ボタンがエディタやビューアの領域を広げたり縮めたりするものだということがわかったところで、再びパネルなのかどうか、パネルではなくてエディタ自体、ビューア自体なのかどうかの話しに戻ります。
パネルは開いたり閉じたりするものですが、エディタやビューアは広げたり縮めたりするものだったことがわかっています。
パネルもリサイズするような概念が無いわけではありませんが、今回の広げたり縮めたりする操作はどちらかというと領域という概念に対する広げるや縮めるなので、パネルという概念とは少しズレていることがわかります。
では、この領域という概念は何なのか?
実は、この領域を操作するUI、5年〜10年ほど前に触っていた方は多いのではないでしょうか?
それは メーラー です。
今ではすっかり Gmail の時代になりましたが、Outlook Express や Thunderbird や Becky! といったメーラーではよく使われていたあれです。
そのUIが何と呼ばれていたか、 ペイン(pane) です。
ペインとは、IT用語としては、ウィンドウを複数の表示領域に分割して使用する表示方式における、分割された個々の表示領域のことである。 「ペイン」(pane)は、英語で「枠」や「区画」などの意味を持つ語である。 ウィンドウペイン(windowpane)といえば窓ガラスを指す。
これでだいぶスッキリしました。
エディタやビューアはパネルではなく単体で独立して存在していて、それらがペイン(表示領域)の中に配置されているわけです。
そのペイン(表示領域)をボタンを押して広げたり縮めたりすることで、エディタやビューアのサイズも切り替わるという構造。
現状、機能としては存在しませんが、真ん中の境界をドラッグすることでペイン(表示領域)を任意の大きさにできると便利そう。
という感じです。
あのボタンは何
そろそろ、エディタとビューアはパネルなのか の話の中で出てきた あのボタン [<][>]
にもちゃんと名前を付けなくてはいけません。
アイコンボタンに名前をつけるときは、ホバーしたときにツールチップが出ることを想像してみましょう。
ペインという言葉は馴染みが無い可能性があるので、ユーザーにはペインではなく領域という言葉を使っていきます。
ボタンはビューア側に配置されているので、責務的にビューアを操作対象としたボタンにします。1
領域を操作するボタンであることから、純粋にまとめるとこうなります。
[<]
ビューアの領域を広げる
[>]
ビューアの領域を縮める
ボタンを押す前にツールチップを読んだ場合の想像と、実際に操作したときの結果は一致していそうです。
しかし、縮めた結果、領域が無くなるので 縮める よりも 隠す のほうがより表現が正しそうなので調整します。
[<]
ビューアの領域を広げる
[>]
ビューアの領域を隠す
次に、ビューアの領域を縮めた後の状態ではどうでしょうか。
[<<]
エディタを隠す
[<]
エディタを縮める
隠したビューアを再度表示することから、 ビューアを表示する でも良さそうですが、 [<<]
と [<]
のどちらのボタンもビューアを表示する操作になるので、そう簡単にはいきません。
表示する という表現で揃える場合は以下の3つの文言をボタンの状態に合わせて使い分けることが望ましいでしょう。
ボタン操作の効果 | ツールチップの文言 |
---|---|
押すとエディタのみの表示に切り替わる | エディタのみ表示する |
押すとビューアのみの表示に切り替わる | ビューアのみ表示する |
押すとエディタとビューアが半分ずつの表示に切り替わる | エディタとビューアを分割して表示する |
広げる、縮める、隠す の表現と、表示する の表現を比べると、説明としては後者のほうが優れていそうです。
なぜかというと、エディタを縮める理由はビューアを使いたいからであって、ビューアを使うためにエディタを縮めるというように一回頭の中で変換しなければならないからです。
また、エディタのみが表示されている状態から 縮める を押したとき、エディタが縮まることはわかるものの、縮まった後に空く領域がどうなるかも暗黙的です。
ビューアが隠れたから、それが表示されるであろうという予測は立ちますが、ボタンを押す前に本当にそうなるかの確信は持てないはずです。
そのため、ツールチップの文言としては 表示する という表現を使うほうが良いでしょう。
ちなみに、ツールチップを出す出さないに限らず、文言をボタンの aria-label
属性に指定することをオススメします。
aria-label
属性は CSS の ::before
や ::after
の疑似要素と content
プロパティ内で attr
関数を組み合わせて使うことにより、ツールチップ風の表示を実現できるため、アクセシビリティ的にも視覚的にも柔軟な表現が可能で、とても便利だからです。
.foo-button::before {
content: attr(aria-label);
}
aria-labelでツールチップを出す方法の詳細は AtomicDesign の atom より小さな世界の扉を開く の記事に書いてあります。
ボタンの名前
ボタンの表現が先にわかったところで、ボタンの名前を決めます。
ボタンの名前は [何を][どのように][どうする][もの] と付けます。
LayeredAtomicDesign で言うならばこうなるはずです。
意味 | 定義 |
---|---|
何を | ドメイン名 |
どのように | 概念 |
どうする | アクション名 |
もの | 機能名 |
ツールチップの文言の何を表示するかという視点で名前を付けると次のようになります。
ツールチップの文言 | ボタンの名前 | [何を][どのように][どうする][もの] |
---|---|---|
エディタのみ表示する | editor-only-show-button | [エディタを][単一に][表示する][ボタン] |
ビューアのみ表示する | viewer-only-show-button | [ビューアを][単一に][表示する][ボタン] |
エディタとビューアを分割して表示する | split-show-button | [(エディタとビューアを)][分割して][表示する][ボタン] |
ちょっと3つ目のボタンの名前が暗黙的になっている感じがしますが、先に進みます。
ここでボタンのアイコンとボタンの名前の関係を見てみましょう。
[<] viewer-only-show-button
[>] editor-only-show-button
[<<] viewer-only-show-button
[<] split-show-button
[>>] editor-only-show-button
[>] split-show-button
アイコンとボタンに関係性が無いことがわかります。
このボタンはあくまでペインを操作するためのボタンなので、ボタンの名前としては pane-xxx-button
のほうが適しているように思えます。
そして、アイコンの示す内容を見てみると、左 [<]
と右 [>]
、さらに強 [<<]
弱 [<]
という文脈が存在します。
この文脈を踏まえた上で、ペインを操作対象とするボタンという意味と掛け合わせてそのまま解釈すると、
ペインの境界を左右に移動する という意味になります。
エディタのみ表示する といった絶対的な表現に対して、
ペインの境界を左右に移動する というのは相対的な表現です。
相対的な表現のほうは、境界を左右にドラッグして領域を変化させる操作ともマッチしています。
たしかに、同じ操作を違う手段で実現しているわけですから、本来は同じになるほうが自然です。
さらに [<]
や [>]
を押した時に25%ずつ領域を増減させるといった仕様変更が起こっても互換性があります。
では ペインの境界を左右に移動する という役割でボタンに名前を付けてみます。
[<] pane-separator-left-side-move-button
[>] pane-separator-right-side-move-button
[<<] pane-separator-left-end-move-button
[>>] pane-separator-right-end-move-button
アイコンとの関係性はピッタリです。
何を | どのように | どうする | もの |
---|---|---|---|
ペインの境界を | 左側に | 移動する | ボタン |
pane-separator | left-side | move | button |
ペインの境界を | 左端に | 移動する | ボタン |
pane-separator | left-end | move | button |
だいぶ長ったらしいですが、正確な役割がボタンの名前になりました。
ボタンの名前とツールチップの中に表示される文言が違うのは、役割と表現の違いと言えます。
例えば、何かの説明文があったとして、名前は description
になりますが、その説明をどう表現するかは自由です。
説明文という役割は普遍でも、説明する相手によって伝わりやすい最適な表現は変わります。
さて、このペインの境界移動ボタンですが、バラバラに1つずつボタンが存在しているというのは少々違和感を感じます。
そこで、まとめてみると実は次のようなコントローラの一部を表示していると考えることができます。
[<][<<][>>][>]
これが、 [<][>]
だったり、 [>>][>]
だったり、 [<][<<]
だったり、部分的に表示されているだけであるというわけです。
PainSeparatorController というコンポーネントを作り、そこにボタンを4つ並べると、色々なところで使えそうです。
それも、今回の場合は垂直分割のペインが対象なので、 VerticalPainSeparatorController で、水平分割用の HorizontalPainSeparatorController も存在することも見えてきます。
さらに PainSeparatorController の中にボタンを置く場合、すでに操作対象はスコープで PainSeparator と確定していることから、
ボタンの名前は left-side-move-button や left-end-move-button で良いことになります。
じっくり色々な角度から分析した結果、一気に世界が整理されていきます。
このような考察は非常に重要であると言えます。
組み立ててみる
ここまで分析できれば、曖昧な場所も少なくなるので、組み立てていくことができそうです。
実際に組み立てるときは、大きな単位からディレクトリ構造を作っていきます。
記事エディタの構造を作る
部位 | 英語名 | 日本語名 |
---|---|---|
赤 | ArticleEditor | 記事エディタ |
橙 | TitleField | タイトルフィールド |
緑 | TagField | タグフィールド |
青 | TextMarkdownEditor | 本文マークダウンエディタ |
紫 | ActionController | アクションコントローラ |
TextMarkdownEditor の Text は 本文 の意味の Text です。
テキストエディタのテキストではない点に注意してください。
**LayeredAtomicDesign**でディレクトリ構造を作るとこのようになります。
/src
/app
/templates
/article-post-page - 記事投稿ページ
/ArticlePostPage.tsx
/atoms
/molecules
/organisms
+------------------------------------------ 記事エディタ
| /article-editor
| /ArticleEditor.tsx
| /title-field
| /TitleField.tsx
| /title_field.scss
| /tag-field
| /TagField.tsx
| +-------------------------------------- 本文マークダウンエディタ
| | /text-markdown-editor
| | /TextMarkdownEditor.tsx
| +--------------------------------------
| /action-controller
| /ActionController.tsx
+------------------------------------------
※ scss ファイルは説明に使うもの以外省略しています
※ 基本的に .tsx の隣に .scss を作り、子要素を composes し、それを require して使います
※ 状態は全て aria-* や data-* に反映し、デザイナーさんは .scss 内だけ触れば仕事が完結するようにするのがベストです
タイトルフィールドの記事投稿ステータスは、 src/app/organisms/article-post-status
にある物を使います。
タイトルフィールドのテキストボックスは、このフィールドに依存した特別な大きさ(文字サイズも含め)なので、 src/lib/atoms/text-box
のコンポーネントに、title_field.scss
のセレクタを適用すれば良いでしょう。
タグフィールドのテキストボックスはサイト内の一般的なテキストボックスに見えるので、 src/app/atoms/text-box
にある物を使えばよいでしょう。
アクションコントローラには src/app/organisms/article-post-combo-button
を使います。
本文マークダウンエディタの構造を作る
部位 | 英語名 | 日本語名 |
---|---|---|
赤 | TextEditor | 本文エディタ |
橙 | TextViewer | 本文ビューア |
緑 | Header | ヘッダー |
青 | Editor | エディタ |
紫 | Viewer | ビューア |
ヘッダーは本文エディタ、本文ビューアそれぞれに所属するのでコンポーネント名は同じですが、内容と実態は別々です。
/src
〜 省略 〜
/text-markdown-editor
/TextMarkdownEditor.tsx
/text-editer
/TextEditor.tsx
/header
/Header.tsx
/text-viewer
/TextViewer.tsx
/header
/Header.tsx
/viewer
/Viewer.tsx
コンポーネントを組み始める
そろそろ構造の末端が見えてきたので、実際にコンポーネントを組みながら進めます。
本文エディタのヘッダーを作る
部位 | 英語名 | 日本語名 |
---|---|---|
赤 | TabMenu | タブメニュー |
橙 | AnchorMenu | アンカーメニュー |
緑 | ToolBox | ツールボックス |
青 | VerticalPainSeparatorController | 垂直領域境界コントローラ |
本文 がタブっぽい見た目にしてあることから、Tabとします。
Tabは複数並ぶ可能性が考えられるので、それをTabMenuで包括します。
その隣にはアンカーが一つ置いてありますが、TabMenuとの対称性でAnchorMenuとします。
ツールボックスにはボタンが並ぶだけで、垂直境界コントローラは状態によって出たり消えたりします。
<Header>
<TabMenu>
<Tab>本文</Tab>
</TabMenu>
<AnchorMenu>
<a><i>■</i></a>
</AnchorMenu>
<ToolBox />
<VerticalPainSeparatorController />
</Header>
つまりこういう感じになります。
<Header>
<TabMenu />
<AnchorMenu />
<ToolBox />
<VerticalPainSeparatorController />
</Header>
/src
〜 省略 〜
/text-markdown-editor
/text-editer
/header
/Header.tsx
/tab-menu
/TabMenu.tsx
/anchor-menu
/AnchorMenu.tsx
/tool-box
/ToolBox.tsx
本文ビューアのヘッダーを作る
部位 | 英語名 | 日本語名 |
---|---|---|
赤 | TabMenu | タブメニュー |
緑 | ToolBox | ツールボックス |
青 | VerticalPainSeparatorController | 垂直領域境界コントローラ |
ビューアのほうは プレビュー がタブっぽい見た目ではないものの、役割としてはエディタの 本文 と同様であること、スライドモードが ToolBox の所属に相当することから、役割を頼りに同じ構造に割り当てます。(AnchorMenuは使わないので存在しません)
<Header>
<VerticalPainSeparatorController />
<TabMenu />
<ToolBox />
</Header>
/src
〜 省略 〜
/text-markdown-editor
/text-viewer
/header
/Header.tsx
/tab-menu
/TabMenu.tsx
/tool-box
/ToolBox.tsx
本文マークダウンエディタを作る
まずは本文エディタを作ります。
<TextEditor>
<Header />
<TextArea />
</TextEditor>
次に本文ビューアを作ります。
<TextViewer>
<Header />
<Viewer />
</TextViewer>
それらをペインの中に収めます。
Pane と PaneGroup と PaneSeparator は汎用的に提供されている物を使うので、このあたりのディレクトリ構成には含まれないこととします。
アプリケーション共通のペイン系は src/app/molecules/pane-group
あたりに置かれることでしょう。
汎用的なライブラリとしてのペイン系は src/lib/molecules/pane-group
あたりに置かれます。
<TextMarkdownEditor>
<PaneGroup>
<Pane>
<TextEditor />
</Pane>
<PaneSeparator />
<Pane>
<TextViewer />
</Pane>
</PaneGroup>
</TextMarkdownEditor>
依存関係を正しく守るため、TextEditor
と TextViewer
は配置する側の text-markdown-editor/text_markdown_editor.scss
から親要素のサイズに追従するように設定します。
これで、ペインのサイズを変えれば、エディタやビューアもサイズが変わるようになります。
.text-editor {
composes: text-editor from "./text-editor/text_editor.scss";
// ここで縦横が親要素のサイズに追従するように設定する
}
.text-viewer {
composes: text-viewer from "./text-viewer/text_viewer.scss";
// ここで縦横が親要素のサイズに追従するように設定する
}
ペインのサイズ変更周りの処理は、汎用的な機能としてペイン内に閉じて実装してあるべきです。
VerticalPainSeparatorController
のハンドラとの関連付け、またはidの紐付け程度で連携するところまでインタフェースを洗練させておくのが良いでしょう。
記事エディタを作る
ここまで作ってきた部品を並べるだけで仮実装は完成です。
<ArticleEditor>
<TitleField>
<TextBox />
<ArticlePostStatus />
</TitleField>
<TagField>
<TextBox />
</TagField>
<TextMarkdownEditor />
<ActionController />
</ArticleEditor>
塗り絵で言うところの枠は出来上がったので、あとは実際にpropsを流し込んだり、className
を適用したり、塗りの作業を行うのみ。
**LayeredAtomicDesign**における、コンポーネント固有の子コンポーネントは配下に収めるに従ったため、
ディレクトリ階層が深く感じるかもしれませんが、作りは非常にシンプルで単純です。
/src
/app
/templates
/article-post-page
/ArticlePostPage.tsx
/atoms
/molecules
/organisms
+------------------------------------------ 記事エディタ
| /article-editor
| /ArticleEditor.tsx
| /title-field
| /TitleField.tsx
| /tag-field
| /TagField.tsx
| +-------------------------------------- 本文マークダウンエディタ
| | /text-markdown-editor
| | /TextMarkdownEditor.tsx
| | /text-editer
| | /TextEditor.tsx
| | /header
| | /Header.tsx
| | /tab-menu
| | /TabMenu.tsx
| | /anchor-menu
| | /AnchorMenu.tsx
| | /tool-box
| | /ToolBox.tsx
| | /text-viewer
| | /TextViewer.tsx
| | /header
| | /Header.tsx
| | /tab-menu
| | /TabMenu.tsx
| | /tool-box
| | /ToolBox.tsx
| | /viewer
| | /Viewer.tsx
| +--------------------------------------
| /action-controller
| /ActionController.tsx
+------------------------------------------
デザインカンプとディレクトリ構造を見ただけで全体像がイメージしやすいメリットもあります。
もし、深いディレクトリ階層を避けたいのであれば、適当な部分で src/app/organisms
下に切り出すというのも手ではあります。
ただ、使う場所が限られているものを細かくバラバラにすると、関心の対象がバラバラになって却ってわかりにくくなることがあるので注意しましょう。
補足
スタイルをどのように当てるか気になる方へ
途中でも少し説明しましたが、もう少し詳しく書いておきましょう。
基本的に src/app
ディレクトリ内では、各.tsxファイルに対して1枚の.scssを配置して、コンポーネントが管轄するHTML要素と子コンポーネントのセレクターを用意します。
CSS Modules の composes
を子コンポーネントセレクターに書いて、親子関係を繋ぐところまではエンジニアの作業です。
ここまでやっておくと、ルートのコンポーネントの.scssから、末端のコンポーネントの.scssまで composes: *** from "***";
のパスの部分を飛んでたどり着くことができるようになります。
あと、状態の変化は全て aria-*
と data-*
に反映するようにして、クラス名の付替えを行わないようにしましょう。
これにより、クラス名は最初の描画時から常に固定になります。
ここまでできれば、デザイナーさんが.scssファイルを触るだけで全体のスタイルを当てることができます。
デザイナーさんは一切.tsxファイルに触れることなく作業が完結できるので、コンポーネントとスタイルの完全な分離、完璧な分業を実現することができます。
実際に className
を適用したり、composes
で親子関係を繋いでいる様子は React + TypeScript における ViewComponent の美しい合成技術 に書いてありますので、参考にしてみてください。
なお、複雑なアニメーションを行う場合は、当然コンポーネント側でも作りの調整が必要になります。
複雑なアニメーションに関しては例外的ですが、あくまで論理的に構築されたコンポーネントやアプリケーションが先にあって、そこにスタイルを当てているという依存関係がベースにあることは間違いありません。
複雑なアニメーションが要求される時にそこだけコンポーネント側に何かを追加するだけで対応できるような仕組み等、別の仕組みを用意する方向で実現できたらいいなと考えてはいます。
ペインを制御する場所について
今回設計していて気付いたのですが、設計中にも書いていたように、ペインの境界をドラッグで移動できたら便利そうというのが一つと、PaneSeparatorController は境界自体に組み込まれていても良いのではないかと感じました。
|
|
┌─┐
│ │
│<│
│ │
└─┘
┌─┐
│ │
│>│
│ │
└─┘
|
|
<TextMarkdownEditor>
<PaneGroup>
<Pane>
<TextEditor />
</Pane>
<PaneSeparator /> ← ここに領域境界コントローラが入っていれば良さそう
<Pane>
<TextViewer />
</Pane>
</PaneGroup>
</TextMarkdownEditor>
ドラッグで境界を移動することもできるし、クリックで境界を端に移動することもできる。
こうしたくなった理由はいくつかあり、設計中もここが悩みどころになったためです。
- ペイン側に操作が閉じていたほうが責務的にも収まりが良い
- エディタやビューアは本来ペインのことは知らないほうが良いが、領域境界コントローラを置かないといけない
- エディタやビューアをペインの無いところで使う時に領域境界コントローラが含まれることに説明がつかない
一応依存関係に関しては、エディタとビューアのヘッダーは領域境界コントローラを持たせず、text-markdown-editor
のスコープでそれらのヘッダーをラップして領域境界コントローラと組み合わせた拡張ヘッダーを注入するという方法を取ることにより、解決自体はできるのですが、ちょっと大げさな感じになってしまいます。
実際のところ、領域の境界を移動するボタンがヘッダーに置いてある理由が別に存在するかもしれないので、なんとも言えないのですが、こういった依存関係周りの検討は、コンポーネント開発においてかなり難しい部分だと感じています。
だいたいは依存関係がおかしいことに気付いてもらえないことが多いので、開発中にこのような場面に遭遇しても理解してもらうこと自体にパワーが必要だったりします。
まとめ
今回は前日の記事で書いたLayeredAtomicDesignを用いて、実際に Qiita さんの記事投稿ページを設計する例を紹介しました。
大きなアプリケーションを AtomicDesign を用いてどこからどのように設計していくのか、コンポーネントの名前や粒度はどうやって決めていくのか、依存関係に対する考え方などなど、かなり実践的な流れを感じていただけたのではないでしょうか。
実際、ニコニコ生放送の開発でも、一つ一つのコンポーネントに対して非常に深い考察を行っています。
ここで触れている内容以外にも、タブ操作を行った時の使い勝手や、タブキーを押した時のフォーカス順序、トグルボタンを押した時にフォーカスが維持されるか、といったアクセシビリティの観点からも、精度を高められるよう開発を進めています。
ニコニコ生放送で確立されたコンポーネント設計手法や実装方法などを実際に学んでみたい!一緒に作ってみたい!という仲間も大歓迎です。
これからもより良いプロダクトを作れるよう頑張ってまいります。
-
エディタとビューアで外側の世界を知らないようにするため、相互依存にしないため。 ↩