Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ Currently, the [Intro to Storybook tutorial](https://storybook.js.org/tutorials/
| | Spanish | ❌ |
| | Portuguese | ❌ |
| | Japanese | ❌ |
| Svelte | English | |
| Svelte | English | |
| | Spanish | ❌ |

---
Expand Down
1 change: 1 addition & 0 deletions content/intro-to-storybook/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ toc:
'screen',
'deploy',
'test',
'accessibility-testing',
'using-addons',
'conclusion',
'contribute',
Expand Down
75 changes: 75 additions & 0 deletions content/intro-to-storybook/svelte/en/accessibility-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: 'Accessibility tests'
tocTitle: 'Accessibility Testing'
description: 'Learn how to integrate accessibility tests into your workflow'
---

So far, we've been focused on building our UI components with a strong emphasis on functionality and visual testing, adding complexity as we go. But we've not yet addressed an important aspect of UI development: accessibility.

## Why Accessibility (A11y)?

Accessibility ensures that all users can interact effectively with our components regardless of their abilities. This includes users with visual, auditory, motor, or cognitive impairments. Accessibility is not only the right thing to do, but it's increasingly mandated based on legal requirements and industry standards. Given these requirements, we must test our components for accessibility issues early and often.

## Catch accessibility issues with Storybook

Storybook provides an [Accessibility addon](https://storybook.js.org/addons/@storybook/addon-a11y) (A11y) that helps you test the accessibility of your components. Built on top of [axe-core](https://github.com/dequelabs/axe-core), it can catch up to [57% of WCAG issues](https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues/).

Let's see how it works! Run the following command to install the addon:

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

<div class="aside">

💡 Storybook's `add` command automates the addon's installation and configuration. See the [official documentation](https://storybook.js.org/docs/api/cli-options) to learn more about the other available commands.

</div>

Restart your Storybook to see the new addon enabled in the UI.

![Task accessibility issue in Storybook](/intro-to-storybook/accessibility-issue-task-non-react-9-0.png)

Cycling through our stories, we can see that the addon found an accessibility issue with one of our test states. The [**Color contrast**](https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=axeAPI) violation essentially means there isn't enough contrast between the task title and the background. We can quickly fix it by changing the text color to a darker gray in our application's CSS (located in `src/index.css`).

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

That's it. We've taken the first step to ensure our UI remains accessible. However, our job isn't finished yet. Maintaining an accessible UI is an ongoing process, and we should monitor our UI for any new accessibility issues to prevent regressions from being introduced as our app evolves and our UIs gain complexity.

## Accessibility tests with Chromatic

With Storybook's accessibility addon, we can test and get instant feedback on accessibility issues during development. However, keeping track of accessibility issues can be challenging, and prioritizing which issues to address first may require a dedicated effort. This is where Chromatic can help us. As we've already seen, it helped us [visually test](/intro-to-storybook/svelte/en/test/) our components to prevent regressions. We'll use its [accessibility testing feature](https://www.chromatic.com/docs/accessibility) to ensure our UI remains accessible and we don't accidentally introduce new violations.

### Enable accessibility tests

Go to your Chromatic project and navigate to the **Manage** page. Click the **Enable** button to activate accessibility tests for your project.

![Chromatic accessibility tests enabled](/intro-to-storybook/chromatic-a11y-tests-enabled.png)

### Run accessibility tests

Now that we've enabled accessibility testing and fixed the color contrast issue in our CSS, let's push our changes to trigger a new Chromatic build.

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

When Chromatic runs, it establishes the [accessibility baselines](https://www.chromatic.com/docs/accessibility/#what-is-an-accessibility-baseline) as the starting point against which future tests will compare their results. This allows us to prioritize, address, and fix accessibility issues more effectively without introducing new regressions.

<!--

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

![Chromatic build with accessibility tests](/intro-to-storybook/chromatic-build-a11y-tests.png)

We've now successfully built a workflow that ensures our UI remains accessible at each stage of development. Storybook will help us catch accessibility issues during development, while Chromatic keeps track of accessibility regressions, making it easier to fix them incrementally over time.
203 changes: 116 additions & 87 deletions content/intro-to-storybook/svelte/en/composite-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,59 @@ Since `Task` data can be sent asynchronously, we **also** need a loading state t

## Get set up

A composite component isn’t much different from the basic components it contains. Create a `TaskList` component, an auxiliary component to help us display the correct markup, and an accompanying story file: `src/components/TaskList.svelte`, `src/components/MarginDecorator.svelte`, and `src/components/TaskList.stories.js`.
A composite component isn’t much different from the basic components it contains. Create a `TaskList` component, an auxiliary component to help us display the correct markup, and an accompanying story file: `src/lib/components/TaskList.svelte`, `src/lib/components/MarginDecorator.svelte`, and `src/lib/components/TaskList.stories.svelte`.

Start with a rough implementation of the `TaskList`. You’ll need to import the `Task` component from earlier and pass in the attributes and actions as inputs.

```html:title=src/components/TaskList.svelte
<script>
```html:title=src/lib/components/TaskList.svelte
<script lang="ts">
import type { TaskData } from '../../types';

import Task from './Task.svelte';

/* Sets the loading state */
export let loading = false;
interface Props {
/** Checks if it's in loading state */
loading?: boolean;
/** The list of tasks */
tasks: TaskData[];
/** Event to change the task to pinned */
onPinTask: (id: string) => void;
/** Event to change the task to archived */
onArchiveTask: (id: string) => void;
}

/* Defines a list of tasks */
export let tasks = [];
const {
loading = false,
tasks = [],
onPinTask,
onArchiveTask,
}: Props = $props();

/* Reactive declaration (computed prop in other frameworks) */
$: noTasks = tasks.length === 0;
$: emptyTasks = noTasks && !loading;
const noTasks = $derived(tasks.length === 0);
</script>

{#if loading}
<div class="list-items">loading</div>
{/if}
{#if emptyTasks}

{#if !loading && noTasks}
<div class="list-items">empty</div>
{/if}

{#each tasks as task}
<Task {task} on:onPinTask on:onArchiveTask />
<Task {task} {onPinTask} {onArchiveTask} />
{/each}

```

Next, create `MarginDecorator` with the following inside:

```html:title=src/components/MarginDecorator.svelte
```html:title=src/lib/components/MarginDecorator.svelte
<script>
let { children } = $props();
</script>

<div>
<slot />
{@render children()}
</div>

<style>
Expand All @@ -65,74 +82,73 @@ Next, create `MarginDecorator` with the following inside:

Finally, create `Tasklist`’s test states in the story file.

```js:title=src/components/TaskList.stories.js
import TaskList from './TaskList.svelte';
```html:title=src/lib/components/TaskList.stories.svelte
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';

import MarginDecorator from './MarginDecorator.svelte';
import TaskList from './TaskList.svelte';
import MarginDecorator from './MarginDecorator.svelte';

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

export default {
component: TaskList,
title: 'TaskList',
tags: ['autodocs'],
//👇 The auxiliary component will be added as a decorator to help show the UI correctly
decorators: [() => MarginDecorator],
render: (args) => ({
Component: TaskList,
props: args,
on: {
...TaskStories.actionsData,
},
}),
};
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' },
];

export const Default = {
args: {
// Shaping the stories through args composition.
// The data was inherited from the Default story in task.stories.js.
tasks: [
{ ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
{ ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
{ ...TaskStories.Default.args.task, id: '3', title: 'Task 3' },
{ ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
{ ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
{ ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
],
},
};
const { Story } = defineMeta({
component: TaskList,
title: 'TaskList',
tags: ['autodocs'],
excludeStories: /.*Data$/,
decorators: [() => MarginDecorator],
args: {
...TaskStories.TaskData.events,
},
});
</script>

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

export const Loading = {
args: {
<Story
name="Loading"
args={{
tasks: [],
loading: true,
},
};

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

<Story
name="Empty"
args={{
tasks: TaskListData.slice(0, 0),
loading: false,
},
};
}}
/>
```

<div class="aside">

[**Decorators**](https://storybook.js.org/docs/writing-stories/decorators) are a way to provide arbitrary wrappers to stories. In this case were using a decorator `key` on the default export to add styling around the rendered component. They can also be used to add other context to components.
[**Decorators**](https://storybook.js.org/docs/writing-stories/decorators) are a way to provide arbitrary wrappers to stories. In this case, we're using Svelte's CSF's `decorators` property to add styling around the rendered component. They can also be used to add other context to components.

</div>

Expand All @@ -142,7 +158,7 @@ Now check Storybook for the new `TaskList` stories.

<video autoPlay muted playsInline loop>
<source
src="/intro-to-storybook/inprogress-tasklist-states-7-0.mp4"
src="/intro-to-storybook/inprogress-tasklist-states-9-0.mp4"
type="video/mp4"
/>
</video>
Expand All @@ -155,9 +171,9 @@ For the loading edge case, we will create a new component that will display the

Create a new file called `LoadingRow.svelte` and inside add the following markup:

```html:title=src/components/LoadingRow.svelte
```html:title=src/lib/components/LoadingRow.svelte
<div class="loading-item">
<span class="glow-checkbox" />
<span class="glow-checkbox"></span>
<span class="glow-text">
<span>Loading</span>
<span>cool</span>
Expand All @@ -168,54 +184,67 @@ Create a new file called `LoadingRow.svelte` and inside add the following markup

And update `TaskList.svelte` to the following:

```html:title=src/components/TaskList.svelte
<script>
```html:title=src/lib/components/TaskList.svelte
<script lang="ts">
import type { TaskData } from '../../types';

import Task from './Task.svelte';
import LoadingRow from './LoadingRow.svelte';

/* Sets the loading state */
export let loading = false;
interface Props {
/** Checks if it's in loading state */
loading?: boolean;
/** The list of tasks */
tasks: TaskData[];
/** Event to change the task to pinned */
onPinTask: (id: string) => void;
/** Event to change the task to archived */
onArchiveTask: (id: string) => void;
}

/* Defines a list of tasks */
export let tasks = [];
const {
loading = false,
tasks = [],
onPinTask,
onArchiveTask,
}: Props = $props();

/* Reactive declaration (computed prop in other frameworks) */
$: noTasks = tasks.length === 0;
$: emptyTasks = noTasks && !loading;
$: tasksInOrder = [
const noTasks = $derived(tasks.length === 0);
const tasksInOrder = $derived([
...tasks.filter((t) => t.state === 'TASK_PINNED'),
...tasks.filter((t) => t.state !== 'TASK_PINNED')
];
...tasks.filter((t) => t.state !== 'TASK_PINNED'),
]);
</script>

{#if loading}
<div class="list-items">
<div class="list-items" data-testid="loading" id="loading">
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
</div>
{/if}
{#if emptyTasks}
{#if !loading && noTasks}
<div class="list-items">
<div class="wrapper-message">
<span class="icon-check" />
<span class="icon-check"></span>
<p class="title-message">You have no tasks</p>
<p class="subtitle-message">Sit back and relax</p>
</div>
</div>
{/if}

{#each tasksInOrder as task}
<Task {task} on:onPinTask on:onArchiveTask />
<Task {task} {onPinTask} {onArchiveTask} />
{/each}
```

The added markup results in the following UI:

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