Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions content/intro-to-storybook/vue/ja/accessibility-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: 'アクセシビリティテスト'
tocTitle: 'アクセシビリティテスト'
description: 'アクセシビリティテストをワークフローに組み込む方法を学びましょう'
commit: 'c73e174'
---

ここまで、機能やビジュアルテストに重点を置きながら、少しずつ複雑さを増しつつ UI コンポーネントを構築してきました。しかし、UI 開発において重要な側面であるアクセシビリティについてはまだ対応していませんでした。

## なぜアクセシビリティ (A11y) が重要なのか?

アクセシビリティとは、視覚、聴覚、運動、認知などの障害を持つユーザーを含め、すべてのユーザーがコンポーネントを効果的に操作できるようにすることです。アクセシビリティは正しいことであるだけでなく、法的要件や業界標準に基づいてますます義務化されています。これらの要件を踏まえ、アクセシビリティの問題を早い段階で頻繁にテストする必要があります。

## Storybook でアクセシビリティの問題を検知する

Storybook には[アクセシビリティアドオン](https://storybook.js.org/addons/@storybook/addon-a11y) (A11y) が用意されており、コンポーネントのアクセシビリティをテストするのに役立ちます。[axe-core](https://github.com/dequelabs/axe-core) をベースに構築されており、[WCAG の問題の最大 57%](https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues/) を検知できます。

どのように動作するか見てみましょう!以下のコマンドでアドオンをインストールします:

```shell
yarn exec storybook add @storybook/addon-a11y
```

<div class="aside">

💡 Storybook の `add` コマンドは、アドオンのインストールと設定を自動化します。利用可能な他のコマンドについては、[公式ドキュメント](https://storybook.js.org/docs/api/cli-options)を参照してください。

</div>

Storybook を再起動すると、UI に新しいアドオンが有効になっているのが確認できます。

![Storybook で Task のアクセシビリティの問題を表示](/intro-to-storybook/accessibility-issue-task-non-react-9-0.png)

ストーリーをひととおり確認すると、アドオンがテスト状態の 1 つでアクセシビリティの問題を検知したことがわかります。[**コントラスト**](https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=axeAPI)の違反は、タスクのタイトルと背景の間に十分なコントラストがないことを意味しています。アプリケーションの CSS(`src/index.css`)でテキストの色をより暗いグレーに変更することで、すぐに修正できます。

```diff:title=src/index.css
.list-item.TASK_ARCHIVED input[type="text"] {
- color: #a0aec0;
+ color: #4a5568;
text-decoration: line-through;
}
```

以上です。UI のアクセシビリティを確保するための最初のステップが完了しました。しかし、まだ作業は終わりではありません。アクセシブルな UI を維持することは継続的なプロセスであり、アプリが進化して UI が複雑になっても、新たなアクセシビリティの問題が導入されてリグレッションが発生しないように監視し続ける必要があります。

## Chromatic によるアクセシビリティテスト

Storybook のアクセシビリティアドオンを使えば、開発中にアクセシビリティの問題をテストして即座にフィードバックを得ることができます。しかし、アクセシビリティの問題を追跡し続けることは難しく、どの問題を優先的に対処すべきかを判断するには専門的な取り組みが必要になることもあります。ここで Chromatic が役立ちます。すでに見てきたように、Chromatic はコンポーネントの[ビジュアルテスト](/intro-to-storybook/vue/ja/test/)を行い、リグレッションを防ぐのに役立ちました。Chromatic の[アクセシビリティテスト機能](https://www.chromatic.com/docs/accessibility)を使って、UI のアクセシビリティを維持し、新しい違反を誤って導入しないようにしましょう。

### アクセシビリティテストを有効にする

Chromatic のプロジェクトに移動し、**Manage** ページを開きます。**Enable** ボタンをクリックして、プロジェクトのアクセシビリティテストを有効にします。

![Chromatic のアクセシビリティテストを有効化](/intro-to-storybook/chromatic-a11y-tests-enabled.png)

### アクセシビリティテストを実行する

アクセシビリティテストを有効にし、CSS のコントラストの問題を修正したので、変更をプッシュして新しい Chromatic ビルドをトリガーしましょう。

```shell:clipboard=false
git add .
git commit -m "Fix color contrast accessibility violation"
git push
```

Chromatic が実行されると、[アクセシビリティのベースライン](https://www.chromatic.com/docs/accessibility/#what-is-an-accessibility-baseline)が確立されます。これは、今後のテストが結果を比較するための基準となるものです。これにより、新たなリグレッションを導入することなく、アクセシビリティの問題をより効果的に優先順位付けし、対処し、修正できるようになります。

<!--

TODO: Follow up with Design for an updated asset
- Needs a React and non-React version to ensure parity with the tutorial
-->

![Chromatic のアクセシビリティテスト付きビルド](/intro-to-storybook/chromatic-build-a11y-tests-non-react.png)

これで、開発の各段階で UI のアクセシビリティを確保するワークフローを構築できました。Storybook は開発中のアクセシビリティの問題を検知し、Chromatic はアクセシビリティのリグレッションを追跡して、時間をかけて段階的に修正しやすくしてくれます。
253 changes: 253 additions & 0 deletions content/intro-to-storybook/vue/ja/composite-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
---
title: '複合的なコンポーネントを組み立てる'
tocTitle: '複合的なコンポーネント'
description: '単純なコンポーネントから複合的なコンポーネントを組み立てましょう'
commit: '554783d'
---

前の章では、最初のコンポーネントを作成しました。この章では、学習した内容を基にタスクのリストである `TaskList` を作成します。それではコンポーネントを組み合わせて、複雑になった場合にどうすればよいか見てみましょう。

## TaskList (タスクリスト)

Taskbox はピン留めされたタスクを通常のタスクより上部に表示することで強調します。これにより `TaskList` に、タスクのリストが通常のタスクのみである場合と、ピン留めされたタスクとの組み合わせである場合というストーリーを追加するべき 2 つのバリエーションができます。

![デフォルトのタスクとピン留めされたタスク](/intro-to-storybook/tasklist-states-1.png)

`Task` のデータは非同期に送信されるので、接続がないことを示すため、読み込み中の状態**も併せて**必要となります。さらにタスクがない場合に備え、空の状態も必要です。

![空の状態と読み込み中のタスク](/intro-to-storybook/tasklist-states-2.png)

## セットアップする

複合的なコンポーネントも基本的なコンポーネントと大きな違いはありません。`TaskList` のコンポーネントとそのストーリーファイル、`src/components/TaskList.vue` と `src/components/TaskList.stories.ts` を作成しましょう。

まずは `TaskList` の大まかな実装から始めます。前の章で作成した `Task` コンポーネントをインポートし、属性とアクションを入力として渡します。

```html:title=src/components/TaskList.vue
<template>
<div class="list-items">
<template v-if="loading"> loading </template>
<template v-else-if="isEmpty"> empty </template>
<template v-else>
<Task
v-for="task in tasks"
:key="task.id"
:task="task"
@archive-task="onArchiveTask"
@pin-task="onPinTask"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { TaskData } from '../types'

import { computed } from 'vue'

import Task from './Task.vue'

type TaskListProps = {
tasks: TaskData[]
loading?: boolean
}

const props = defineProps<TaskListProps>()

const isEmpty = computed(() => props.tasks.length === 0)

const emit = defineEmits<{
(e: 'archive-task', id: string): void
(e: 'pin-task', id: string): void
}>()

/**
* Event handler for archiving tasks
*/
function onArchiveTask(taskId: string): void {
emit('archive-task', taskId)
}

/**
* Event handler for pinning tasks
*/
function onPinTask(taskId: string): void {
emit('pin-task', taskId)
}
</script>
```

次に `Tasklist` のテスト状態をストーリーファイルに記述します。

```ts:title=src/components/TaskList.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3-vite'

import TaskList from './TaskList.vue'

import * as TaskStories from './Task.stories'

export const TaskListData = [
{ ...TaskStories.TaskData, id: '1', title: 'Task 1' },
{ ...TaskStories.TaskData, id: '2', title: 'Task 2' },
{ ...TaskStories.TaskData, id: '3', title: 'Task 3' },
{ ...TaskStories.TaskData, id: '4', title: 'Task 4' },
{ ...TaskStories.TaskData, id: '5', title: 'Task 5' },
{ ...TaskStories.TaskData, id: '6', title: 'Task 6' },
]

const meta = {
component: TaskList,
title: 'TaskList',
tags: ['autodocs'],
excludeStories: /.*Data$/,
decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
args: {
...TaskStories.TaskData.events,
},
} satisfies Meta<typeof TaskList>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Default story.
tasks: TaskListData,
},
}

export const WithPinnedTasks: Story = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Default story.
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
},
}

export const Loading: Story = {
args: {
tasks: [],
loading: true,
},
}

export const Empty: Story = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
},
}
```

<div class="aside">

💡[**デコレーター**](https://storybook.js.org/docs/writing-stories/decorators)は、ストーリーに任意のラッパーを提供する方法です。今回は、デフォルトエクスポートの decorator キーを使って、レンダリングされるコンポーネントの周囲にスタイルを追加しています。後で説明するように、コンポーネントに別のコンテキストを追加することもできます。

</div>

`TaskStories` をインポートすることで、ストーリーに必要な引数 (args) を最小限の労力で[組み合わせる](https://storybook.js.org/docs/writing-stories/args#args-composition)ことができます。そうすることで、2 つのコンポーネントが想定するデータとアクション (呼び出しのモック) の一貫性が保たれます。

それでは `TaskList` の新しいストーリーを Storybook で確認してみましょう。

<video autoPlay muted playsInline loop>
<source
src="/intro-to-storybook/inprogress-tasklist-states-9-0.mp4"
type="video/mp4"
/>
</video>

## 状態を作りこむ

今のコンポーネントはまだ粗削りですが、ストーリーは見えています。単に `.list-items` だけのためにラッパーを作るのは単純すぎると思うかもしれません。実際、その通りです。ほとんどの場合、単なるラッパーのためだけに新しいコンポーネントは作りません。`TaskList` の**本当の複雑さ**は `withPinnedTasks`、`loading`、`empty` といったエッジケースに現れているのです。

```html:title=src/components/TaskList.vue
<template>
<div class="list-items">
<template v-if="loading">
<div v-for="n in 6" :key="n" class="loading-item" data-testid="loading" id="loading">
<span class="glow-checkbox" />
<span class="glow-text"> <span>Loading</span> <span>cool</span> <span>state</span> </span>
</div>
</template>

<div v-else-if="isEmpty" class="list-items" data-testid="empty" id="empty">
<div class="wrapper-message">
<span class="icon-check" />
<p class="title-message">You have no tasks</p>
<p class="subtitle-message">Sit back and relax</p>
</div>
</div>

<template v-else>
<Task
v-for="task in tasksInOrder"
:key="task.id"
:task="task"
@archive-task="onArchiveTask"
@pin-task="onPinTask"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { TaskData } from '../types'

import { computed } from 'vue'

import Task from './Task.vue'

type TaskListProps = {
tasks: TaskData[]
loading?: boolean
}

const props = defineProps<TaskListProps>()

const isEmpty = computed(() => props.tasks.length === 0)
const tasksInOrder = computed(() => {
return [
...props.tasks.filter((t) => t.state === 'TASK_PINNED'),
...props.tasks.filter((t) => t.state !== 'TASK_PINNED'),
]
})

const emit = defineEmits<{
(e: 'archive-task', id: string): void
(e: 'pin-task', id: string): void
}>()

/**
* Event handler for archiving tasks
*/
function onArchiveTask(taskId: string): void {
emit('archive-task', taskId)
}

/**
* Event handler for pinning tasks
*/
function onPinTask(taskId: string): void {
emit('pin-task', taskId)
}
</script>
```

追加したマークアップで UI は以下のようになります。

<video autoPlay muted playsInline loop>
<source
src="/intro-to-storybook/finished-tasklist-states-9-0.mp4"
type="video/mp4"
/>
</video>

リスト内のピン留めされたタスクの位置に注目してください。ピン留めされたタスクはユーザーにとって優先度を高くするため、リストの先頭に描画されます。

<div class="aside">
💡 Git へのコミットを忘れずに行ってください!
</div>
35 changes: 35 additions & 0 deletions content/intro-to-storybook/vue/ja/conclusion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: 'まとめ'
description: '今までの知識をまとめて、Storybook のテクニックをもっと学びましょう'
---

Storybook で最初の UI を作成しましたね。お疲れ様でした!ここまでの章で UI コンポーネントを作成し、複合させ、テストし、デプロイする方法を学びました。同じように進めていれば、リポジトリーとデプロイされた Storybook は以下のリンクと同じようになっていることでしょう。

[📕 **GitHub のリポジトリ: chromaui/learnstorybook-code**](https://github.com/chromaui/learnstorybook-code/tree/vue)
<br/>

[🌎 **デプロイされた Storybook**](https://vue--5ccbe484c994280020b6d128.chromatic.com/)

Storybook は React、React Native、Vue、Angular、Svelte、その他のフレームワークにとって強力なツールです。開発者コミュニティーも活発でアドオンも充実しています。このチュートリアルで紹介した内容は、Storybook でできることのほんの一部にすぎません。一度 Storybook を導入すれば、強固な UI を効率的に作れることにきっと驚くことでしょう。

## さらに学ぶには

もっと深く掘り下げたい方のために役に立つリソースを紹介します。

- [**Storybook の公式ドキュメント**](https://storybook.js.org/docs/get-started/install)には API ドキュメント、コミュニティのリンク、アドオンのギャラリーがあります。

- [**UI Testing Playbook**](https://storybook.js.org/blog/ui-testing-playbook/) では Twilio、Adobe、Peloton、Shopify といった効率の良いチームにおけるワークフローのベストプラクティスを紹介しています。

- [**視覚的なテストのハンドブック (Visual Testing Handbook)**](https://storybook.js.org/tutorials/visual-testing-handbook/) では、コンポーネントを Storybook で視覚的にテストする方法を掘り下げています。無料の 31 ページある eBook です。

- [**Storybook Discord**](https://discord.gg/UUt2PJb) では Storybook のコミュニティに参加できます。他の Storybook ユーザーと協力しましょう。

- [**Storybook ブログ**](https://storybook.js.org/blog/)ではリリース情報や、UI 開発のワークフローを合理的にするための機能を紹介します。

## 誰が Intro to Storybook チュートリアルを作成しているのでしょうか?

文書や、コード、製作は [Chromatic](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook) の貢献によるものです。このチュートリアルは Chromatic の [GraphQL + React チュートリアルシリーズ](https://www.chromatic.com/blog/graphql-react-tutorial-part-1-6)を参考にしています。

このようなチュートリアルや記事をさらに読みたい場合は、Storybook のメーリングリストに登録することをオススメします。

<iframe style="height:400px;width:100%;max-width:800px;margin:0px auto;" src="https://upscri.be/d42fc0?as_embed"></iframe>
12 changes: 12 additions & 0 deletions content/intro-to-storybook/vue/ja/contribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: '貢献する'
description: 'Storybook を世界に広めましょう'
---

このチュートリアルの作成にご協力ください!文法や句読点などの些細な問題であれば、プルリクエストを送ってください。もし大規模な問題であれば、 [GitHub の issue を追加](https://github.com/chromaui/learnstorybook.com/issues)して議論しましょう。

Storybook のチュートリアルはコミュニティによって作成・運営されていますので、最新の状態を保ち、荒い部分を削るには皆さんのご協力が不可欠です。どのような協力でも歓迎します。

## 翻訳

Storybook をすべての人が使用できるように、このチュートリアルの他言語への翻訳にお力を貸してください。中国語やスペイン語はとくに歓迎します。ご興味があれば[この issue](https://github.com/chromaui/learnstorybook.com/issues/3) にコメントしてください。
Loading