diff --git a/content/intro-to-storybook/react/ru/composite-component.md b/content/intro-to-storybook/react/ru/composite-component.md new file mode 100644 index 000000000..ee0878155 --- /dev/null +++ b/content/intro-to-storybook/react/ru/composite-component.md @@ -0,0 +1,227 @@ +--- +title: 'Создание составного компонента' +tocTitle: 'Составной компонент' +description: 'Создание составного компонента из простых компонентов' +commit: '73d7821' +--- + +В прошлой главе мы создали наш первый компонент; в этой главе мы расширим полученные знания, чтобы создать компонент `TaskList` - список задач. Давайте объединим компоненты вместе и посмотрим, что произойдет, когда мы добавим больше сложности. + +## Компонент TaskList + +`Taskbox` выделяет закреплённые задачи, размещая их над обычными задачами. Это даёт два варианта отображения компонента `TaskList`, для которых нужно создавать истории: обычный и с закреплёнными элементами. + +![обычные and закреплённые задачи](/intro-to-storybook/tasklist-states-1.png) + +Поскольку данные для `Task` могут быть отправлены асинхронно, нам **также** необходимо состояние загрузки для рендеринга в отсутствие соединения. Кроме того, нам нужно пустое состояние, когда нет задач. + +![состояния когда нет задач и когда задачи загружаются](/intro-to-storybook/tasklist-states-2.png) + +## Начинаем подготовку + +Составной компонент мало чем отличается от простых компонентов, которые он содержит. Создайте компонент `TaskList` и сопроводительный файл историй: `src/components/TaskList.js` и `src/components/TaskList.stories.js`. + +Начните с грубой реализации `TaskList`. Вам нужно будет импортировать компонент `Task` и передать атрибуты и действия в качестве входных данных. + +```js:title=src/components/TaskList.js +import React from 'react'; + +import Task from './Task'; + +export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) { + const events = { + onPinTask, + onArchiveTask, + }; + + if (loading) { + return
loading
; + } + + if (tasks.length === 0) { + return
empty
; + } + + return ( +
+ {tasks.map(task => ( + + ))} +
+ ); +} +``` + +Затем создайте тестовые состояния `TaskList` в файле истории. + +```js:title=src/components/TaskList.stories.js +import React from 'react'; + +import TaskList from './TaskList'; +import * as TaskStories from './Task.stories'; + +export default { + component: TaskList, + title: 'TaskList', + decorators: [story =>
{story()}
], +}; + +const Template = args => ; + +export const Default = Template.bind({}); +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' }, + ], +}; + +export const WithPinnedTasks = Template.bind({}); +WithPinnedTasks.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 = Template.bind({}); +Loading.args = { + tasks: [], + loading: true, +}; + +export const Empty = Template.bind({}); +Empty.args = { + // Shaping the stories through args composition. + // Inherited data coming from the Loading story. + ...Loading.args, + loading: false, +}; +``` + +
+💡 Декораторы это способ создания произвольных обёрток для историй. В данном случае мы используем декоратор для того, чтобы добавить padding вокруг отрисованного компонента. Их также можно использовать для обёртывания историй в "провайдеры" - т.е. библиотечные компоненты, которые устанавливают контекст React. +
+ +Импортировав `TaskStories`, мы смогли [составить](https://storybook.js.org/docs/react/writing-stories/args#args-composition) аргументы (сокращенно args) в наших историях с минимальными усилиями. Таким образом, данные и действия (замоканные коллбеки), ожидаемые обоими компонентами, сохраняются. + +Теперь давайте проверим Storybook на наличие новых историй для `TaskList`. + + + +## Реализуем состояния + +Наш компонент все ещё сырой, но теперь у нас есть представление о том, над чем нужно работать. Вы можете подумать, что обёртка `.list-items` слишком упрощена. Вы правы - в большинстве случаев мы не стали бы создавать новый компонент только для того, чтобы добавить обёртку. Но **реальная сложность** компонента `TaskList` проявляется в граничных случаях `withPinnedTasks`, `load` и `empty`. + +```js:title=src/components/TaskList.js +import React from 'react'; + +import Task from './Task'; + +export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) { + const events = { + onPinTask, + onArchiveTask, + }; + const LoadingRow = ( +
+ + + Loading cool state + +
+ ); + if (loading) { + return ( +
+ {LoadingRow} + {LoadingRow} + {LoadingRow} + {LoadingRow} + {LoadingRow} + {LoadingRow} +
+ ); + } + if (tasks.length === 0) { + return ( +
+
+ +

You have no tasks

+

Sit back and relax

+
+
+ ); + } + + const tasksInOrder = [ + ...tasks.filter((t) => t.state === "TASK_PINNED"), + ...tasks.filter((t) => t.state !== "TASK_PINNED"), + ]; + return ( +
+ {tasksInOrder.map((task) => ( + + ))} +
+ ); +} +``` + +Результат добавленной разметки следующий: + + + +Обратите внимание на положение закреплённого элемента в списке. Мы хотим, чтобы закреплённый элемент отображался в верхней части списка, чтобы сделать его приоритетным для наших пользователей. + +## Требования к данным и свойствам + +По мере роста компонента растут и входные требования. Определите требования к свойствам `TaskList`. Поскольку `Task` является дочерним компонентом, убедитесь, что данные имеют соответствующую структуру для его рендеринга. Чтобы сэкономить время и головную боль, повторно используйте `propTypes`, которые вы определили в `Task` ранее. + +```diff:title=src/components/TaskList.js +import React from 'react'; ++ import PropTypes from 'prop-types'; + +import Task from './Task'; + +export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) { + ... +} + ++ TaskList.propTypes = { ++ /** Checks if it's in loading state */ ++ loading: PropTypes.bool, ++ /** The list of tasks */ ++ tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired, ++ /** Event to change the task to pinned */ ++ onPinTask: PropTypes.func, ++ /** Event to change the task to archived */ ++ onArchiveTask: PropTypes.func, ++ }; ++ TaskList.defaultProps = { ++ loading: false, ++ }; +``` + +
+💡 Не забудьте зафиксировать свои изменения с помощью git! +
diff --git a/content/intro-to-storybook/react/ru/data.md b/content/intro-to-storybook/react/ru/data.md new file mode 100644 index 000000000..a826bce9c --- /dev/null +++ b/content/intro-to-storybook/react/ru/data.md @@ -0,0 +1,304 @@ +--- +title: 'Передача данных' +tocTitle: 'Данные' +description: 'Узнайте как передать данные UI-компоненту' +commit: '94b134e' +--- + +До сих пор мы создавали изолированные компоненты без статического состояния - это хорошо для Storybook, но в конечном счете бесполезно, пока мы не предоставим им какие-либо данные в нашем приложении. + +В этом руководстве не рассматриваются особенности создания приложения, поэтому мы не будем углубляться в эти детали. Но мы уделим немного времени рассмотрению общего шаблона для подключения данных к подключаемым компонентам. + +## Присоединённые компоненты + +На данный момент, наш компонент `TaskList`, является "презентационным", т.е. он не взаимодействует ни с чем внешним, кроме своей собственной реализации. Нам нужно подключить его к поставщику данных, чтобы получить данные. + +Чтобы построить простую модель данных для нашего приложения мы будем использовать [Redux Toolkit](https://redux-toolkit.js.org/). Это эффективный набор инструментов для разработки приложений которые хранят данные с помощью [Redux](https://redux.js.org/). Однако используемый здесь паттерн также применим и к другим библиотекам управления данными, таким как [Apollo](https://www.apollographql.com/client/) и [MobX](https://mobx.js.org/). + +Добавьте необходимые зависимости в ваш проект с помощью команды: + +```bash +yarn add @reduxjs/toolkit react-redux +``` + +Сначала мы создадим простое хранилище Redux, которое реагирует на действия, изменяющие состояние задачи, в файле `store.js` в каталоге `src/lib` (намеренно оставленном простым): + +```js:title=src/lib/store.js +/* Простая имплементация redux store/actions/reducer. + * В настоящем приложении хранилище будет сложнее и разбито на отдельные файлы. + */ +import { configureStore, createSlice } from '@reduxjs/toolkit'; + +/* + * Начальное состояние нашего приложения, когда оно будет загружено. + * Обычно, вы будете получать данные от сервера. Давайте не беспокоиться об этом сейчас. + */ +const defaultTasks = [ + { id: '1', title: 'Something', state: 'TASK_INBOX' }, + { id: '2', title: 'Something more', state: 'TASK_INBOX' }, + { id: '3', title: 'Something else', state: 'TASK_INBOX' }, + { id: '4', title: 'Something again', state: 'TASK_INBOX' }, +]; +const TaskBoxData = { + tasks: defaultTasks, + status: 'idle', + error: null, +}; + +/* + * Хранилище создано тут. + * Вы можете узнать больше о Redux Toolkit слайсах в документации: + * https://redux-toolkit.js.org/api/createSlice + */ +const TasksSlice = createSlice({ + name: 'taskbox', + initialState: TaskBoxData, + reducers: { + updateTaskState: (state, action) => { + const { id, newTaskState } = action.payload; + const task = state.tasks.findIndex((task) => task.id === id); + if (task >= 0) { + state.tasks[task].state = newTaskState; + } + }, + }, +}); + +// Экшены, содержащиеся в слайсах экспортированы для использования в наших компонентах. +export const { updateTaskState } = TasksSlice.actions; + +/* + * Конфигурация хранилища нашего приложения начинается отсюда. + * Узнать больше о configureStore можно в документации Redux: + * https://redux-toolkit.js.org/api/configureStore + */ +const store = configureStore({ + reducer: { + taskbox: TasksSlice.reducer, + }, +}); + +export default store; +``` + +Затем мы изменим наш компонент `TaskList`, подключив его к хранилищу Redux и отрендерим задачи, в которых мы заинтересованы: + +```js:title=src/components/TaskList.js +import React from 'react'; +import Task from './Task'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateTaskState } from '../lib/store'; + +export default function TaskList() { + // Получаем наше состояние из хранилища + const tasks = useSelector((state) => { + const tasksInOrder = [ + ...state.taskbox.tasks.filter((t) => t.state === 'TASK_PINNED'), + ...state.taskbox.tasks.filter((t) => t.state !== 'TASK_PINNED'), + ]; + const filteredTasks = tasksInOrder.filter( + (t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED' + ); + return filteredTasks; + }); + + const { status } = useSelector((state) => state.taskbox); + + const dispatch = useDispatch(); + + const pinTask = (value) => { + // Отправляем событие Pinned обратно в наше хранилище + dispatch(updateTaskState({ id: value, newTaskState: 'TASK_PINNED' })); + }; + const archiveTask = (value) => { + // Отправляем событие Archive обратно в наше хранилище + dispatch(updateTaskState({ id: value, newTaskState: 'TASK_ARCHIVED' })); + }; + const LoadingRow = ( +
+ + + Loading cool state + +
+ ); + if (status === 'loading') { + return ( +
+ {LoadingRow} + {LoadingRow} + {LoadingRow} + {LoadingRow} + {LoadingRow} + {LoadingRow} +
+ ); + } + if (tasks.length === 0) { + return ( +
+
+ +

You have no tasks

+

Sit back and relax

+
+
+ ); + } + + return ( +
+ {tasks.map((task) => ( + pinTask(task)} + onArchiveTask={(task) => archiveTask(task)} + /> + ))} +
+ ); +} +``` + +Теперь, когда у нас есть некоторые фактические данные, заполняющие наш компонент, полученные из хранилища Redux, мы могли бы подключить их к `src/App.js` и отрендерить компонент там. Но пока давайте воздержимся от этого и продолжим наш путь, основанный на компонентах. + +Не беспокойтесь об этом. Мы позаботимся об этом в следующей главе. + +## Supplying context with decorators + +С этим изменением, наши истории Storybook перестали работать, потому что наш `Tasklist` теперь подключен, и полагается на хранилище Redux для получения и обновления наших задач. + +![Сломанный список задач](/intro-to-storybook/broken-tasklist-optimized.png) + +Мы можем использовать различные подходы для решения этой проблемы. Поскольку наше приложение довольно простое, мы можем использовать декоратор, подобный тому, что мы делали в [предыдущей главе](/intro-to-storybook/react/en/composite-component), и предоставить замоканное хранилище в наших историях Storybook: + +```js:title=src/components/TaskList.stories.js +import React from 'react'; + +import TaskList from './TaskList'; +import * as TaskStories from './Task.stories'; + +import { Provider } from 'react-redux'; + +import { configureStore, createSlice } from '@reduxjs/toolkit'; + +// Суперпростой мок состояния нашего хранилища. +export const MockedState = { + 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' }, + ], + status: 'idle', + error: null, +}; + +// Суперпростой мок redux-хранилища. +const Mockstore = ({ taskboxState, children }) => ( + { + const { id, newTaskState } = action.payload; + const task = state.tasks.findIndex((task) => task.id === id); + if (task >= 0) { + state.tasks[task].state = newTaskState; + } + }, + }, + }).reducer, + }, + })} + > + {children} + +); + +export default { + component: TaskList, + title: 'TaskList', + decorators: [(story) =>
{story()}
], + excludeStories: /.*MockedState$/, +}; + +const Template = () => ; + +export const Default = Template.bind({}); +Default.decorators = [ + (story) => {story()}, +]; + +export const WithPinnedTasks = Template.bind({}); +WithPinnedTasks.decorators = [ + (story) => { + const pinnedtasks = [ + ...MockedState.tasks.slice(0, 5), + { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, + ]; + + return ( + + {story()} + + ); + }, +]; + +export const Loading = Template.bind({}); +Loading.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export const Empty = Template.bind({}); +Empty.decorators = [ + (story) => ( + + {story()} + + ), +]; +``` + +
+💡 excludeStories это поле конфигурации Storybook, которое запрещает рассматривать наше замоканное состояние как историю. Подробнее об этом поле вы можете прочитать в документации Storybook. +
+ + + +
+💡 Не забудьте зафиксировать свои изменения с помощью git! +
+ +Успех! Мы вернулись к тому, с чего начали, наш Storybook теперь работает, и мы можем видеть, как мы можем поставлять данные в подключенный компонент. В следующей главе мы возьмем то, чему научились здесь, и применим это к экрану. diff --git a/content/intro-to-storybook/react/ru/get-started.md b/content/intro-to-storybook/react/ru/get-started.md new file mode 100644 index 000000000..71122b6a7 --- /dev/null +++ b/content/intro-to-storybook/react/ru/get-started.md @@ -0,0 +1,81 @@ +--- +title: 'Руководство по Storybook для React' +tocTitle: 'Введение' +description: 'Настройка Storybook в вашей среде разработки' +commit: '9245261' +--- + +Storybook работает параллельно с вашим приложением в режиме разработки. Он помогает создавать UI-компоненты, изолированные от бизнес-логики и контекста вашего приложения. Данное руководство предназначено для React; существуют и другие руководства для [React Native](/intro-to-storybook/react-native/en/get-started), [Vue](/intro-to-storybook/vue/en/get-started), [Angular](/intro-to-storybook/angular/en/get-started), [Svelte](/intro-to-storybook/svelte/en/get-started) и [Ember](/intro-to-storybook/ember/en/get-started). + +![Storybook и ваше приложение](/intro-to-storybook/storybook-relationship.jpg) + +## Настройка React Storybook + +Нам нужно выполнить несколько шагов, чтобы настроить процесс сборки. Для начала, мы используем [degit](https://github.com/Rich-Harris/degit) для настройки нашей системы сборки.Используя этот пакет, вы можете загрузить «шаблоны» (частично созданные приложения с некоторой конфигурацией по умолчанию), которые помогут вам ускорить процесс разработки. + +Давайте выполним следующие команды: + +```bash +# Клонируем шаблон +npx degit chromaui/intro-storybook-react-template taskbox + +cd taskbox + +# Устанавливаем зависимости +yarn +``` + +
+💡 Этот шаблон содержит необходимые стили, ассеты и базовые конфигурации для этой версии руководства. +
+ +Теперь мы можем проверить, правильно ли работают различные среды нашего приложения: + +```bash +# Запускаем Jest: +yarn test --watchAll + +# Запускаем обозреватель компонентов на порту 6006: +yarn storybook + +# Запускаем приложение на порту 3000: +yarn start +``` + +
+💡 Обратите внимание на флаг --watchAll, включение этого флага обеспечивает выполнение всех тестов. По мере прохождения этого руководства вы познакомитесь с различными тестовыми сценариями. Возможно, вы захотите соответствующим образом скорректировать скрипты вашего package.json. +
+ +Три модальности нашего фронтенд-приложения: автоматизированное тестирование (Jest), разработка компонентов (Storybook) и само приложение. + +![3 модальности](/intro-to-storybook/app-three-modalities.png) + +В зависимости от того, над какой частью приложения вы работаете, вы можете запустить одну или несколько из них одновременно. Поскольку сейчас мы сосредоточены на создании одного UI-компонента, мы остановимся на запуске Storybook. + +## Сохраним изменения + +На данном этапе нам лучше добавлять наши файлы в локальный репозиторий. Выполните следующие команды, чтобы создать локальный репозиторий, добавить и зафиксировать изменения, которые мы уже сделали. + +```shell +$ git init +``` + +Добавим файлы: + +```shell +$ git add . +``` + +Зафиксируем изменения: + +```shell +$ git commit -m "first commit" +``` + +И наконец: + +```shell +$ git branch -M main +``` + +А теперь, давайте создадим наш первый компонент! diff --git a/content/intro-to-storybook/react/ru/simple-component.md b/content/intro-to-storybook/react/ru/simple-component.md new file mode 100644 index 000000000..f0d9200b9 --- /dev/null +++ b/content/intro-to-storybook/react/ru/simple-component.md @@ -0,0 +1,328 @@ +--- +title: 'Создание простого компонента' +tocTitle: 'Простой компонент' +description: 'Создание простого компонента в изоляции' +commit: 'efa06ff' +--- + +Мы создадим наш пользовательский интерфейс, следуя методологии [Component-Driven Development](https://www.componentdriven.org/) (CDD). Это процесс, при котором пользовательский интерфейс создается «снизу вверх», начиная с компонентов и заканчивая экранами. CDD помогает вам масштабировать уровень сложности, с которым вы сталкиваетесь при создании пользовательского интерфейса. + +## Task + +![Компонент Task в трех состояниях](/intro-to-storybook/task-states-learnstorybook.png) + +`Task` это основной компонент нашего приложения. Каждая задача отображается по-разному в зависимости от того, в каком именно состоянии она находится. Мы отображаем отмеченным (или не отмеченным) чекбокс, некоторую информацию о задаче и кнопку "прикрепить", позволяющую перемещать задачи вверх и вниз по списку. Чтобы собрать всё это вместе, нам понадобятся следующие параметры: + +- `title` – строка, описывающая задачу +- `state` – в каком списке находится задание в данный момент, и отмечено ли оно? + +Начиная создавать компонент `Task`, мы сначала пишем наши тестовые состояния, которые соответствуют различным типам задач, описанным выше. Затем мы используем Storybook для создания компонента в изоляции с использованием замоканных данных. Мы будем вручную тестировать внешний вид компонента в каждом состоянии по ходу работы. + +## Начинаем подготовку + +Сначала создадим компонент `Task` и сопровождающий его файл истории: `src/components/Task.js` and `src/components/Task.stories.js`. + +Мы начнём с базовой реализации `Task`, просто взяв атрибуты, которые нам понадобятся, и два действия, которые можно выполнить над задачей (чтобы переместить её между списками): + +```js:title=src/components/Task.js +import React from 'react'; + +export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) { + return ( +
+ +
+ ); +} +``` + +Выше мы сделали разметку для `Task` на основе существующей HTML-структуры приложения Todos. + +Ниже мы создадим три тестовых состояния `Task` в файле истории: + +```js:title=src/components/Task.stories.js +import React from 'react'; + +import Task from './Task'; + +export default { + component: Task, + title: 'Task', +}; + +const Template = args => ; + +export const Default = Template.bind({}); +Default.args = { + task: { + id: '1', + title: 'Test Task', + state: 'TASK_INBOX', + }, +}; + +export const Pinned = Template.bind({}); +Pinned.args = { + task: { + ...Default.args.task, + state: 'TASK_PINNED', + }, +}; + +export const Archived = Template.bind({}); +Archived.args = { + task: { + ...Default.args.task, + state: 'TASK_ARCHIVED', + }, +}; +``` + +В Storybook есть два основных уровня организации: компонент и его дочерние истории. Считайте, что каждая история – это один из вариантов компонента. Вы можете иметь столько историй на компонент, сколько вам нужно. + +- **Component** + - Story + - Story + - Story + +Чтобы рассказать Storybook о компоненте, который мы документируем, мы создаем экспорт по умолчанию (`export default`), который содержит: + +- `component` – сам компонент +- `title` – как ссылаться на компонент в боковой панели Storybook + +Чтобы определить наши истории, мы экспортируем функцию для каждого из наших тестовых состояний для создания истории. История – это функция, которая возвращает отрендеренный элемент (т.е. компонент с набором параметров) в данном состоянии – точно так же, как [Functional Component](https://ru.reactjs.org/docs/components-and-props.html#function-and-class-components). + +Поскольку у нас есть несколько вариантов нашего компонента, удобно присвоить его переменной Template. Внедрение этого паттерна в ваши истории сократит количество кода, который вам нужно писать и поддерживать. + +
+💡 Template.bind({}) это стандартный способ JavaScript для создания копии функции. Мы используем этот способ, чтобы позволить каждой экспортируемой истории устанавливать свои собственные свойства, но использовать одну и ту же реализацию. +
+ +Аргументы или кратко [`args`](https://storybook.js.org/docs/react/writing-stories/args), позволят нам редактировать наши компоненты в реальном времени с помощью аддона `controls` без перезапуска Storybook. Как только значение [`args`](https://storybook.js.org/docs/react/writing-stories/args) изменяется, изменяется и компонент. + +При создании истории мы используем базовый аргумент `task` для построения структуры задачи, которую ожидает компонент. Обычно она моделируется на основе того, как выглядят фактические данные. Опять же, экпортирование этой структуры позволит нам повторно использовать её в последующих историях, как мы увидим. + +
+💡 Actions помогут вам проверить взаимодействие при изолированном создании компонентов пользовательского интерфейса. Зачастую у вас не будет доступа к функциям и состояниям, которые есть в контексте приложения. Используйте action(), чтобы сымитировать их. +
+ +## Конфигурация + +Нам потребуется внести пару изменений в конфигурационные файлы Storybook, чтобы он обнаруживал не только наши недавно созданные истории, но и позволил нам использовать CSS-файл приложения (расположенный в `src/index.css`). + +Начните с изменения конфигурационного файла Storybook (`.storybook/main.js`) на следующий: + +```diff:title=.storybook/main.js +module.exports = { +- stories: [ +- '../src/**/*.stories.mdx', +- '../src/**/*.stories.@(js|jsx|ts|tsx)' +- ], ++ stories: ['../src/components/**/*.stories.js'], + staticDirs: ['../public'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/preset-create-react-app', + '@storybook/addon-interactions', + ], + framework: '@storybook/react', + core: { + builder: '@storybook/builder-webpack5', + }, + features: { + interactionsDebugger: true, + }, +}; +``` + +После выполнения вышеуказанных изменений, внутри папки `.storybook` внесите следующие изменения в файл `preview.js`: + +```diff:title=.storybook/preview.js ++ import '../src/index.css'; + +//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI. +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; +``` + +[`parameters`](https://storybook.js.org/docs/react/writing-stories/parameters) обычно используются для управления поведением функций и аддонов Storybook. В нашем случае мы будем использовать их для настройки того, как будут обрабатываться `actions` (замоканные функции). + +`actions` позволяет нам создавать коллбэки, которые появляются на панели **actions** пользовательского интерфейса Storybook при нажатии на кнопку. Таким образом, когда мы создадим кнопку `pin`, мы сможем определить, было ли нажатие кнопки успешным в пользовательском интерфейсе. + +После этого перезапуск Storybook должен привести к появлению тестовых примеров для трех состояний задачи: + + + +## Реализуем состояния + +Теперь, когда Storybook настроен, стили импортированы, а тестовые примеры созданы, мы можем быстро приступить к реализации HTML компонента в соответствии с дизайном. + +На данный момент этот компонент всё ещё находится в зачаточном состоянии. Сначала напишите код, который соответствует дизайну, не вдаваясь в излишние подробности: + +```js:title=src/components/Task.js +import React from 'react'; + +export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) { + return ( +
+ + + + + {state !== "TASK_ARCHIVED" && ( + + )} +
+ ); +} +``` + +Дополнительная разметка сверху в сочетании с CSS, который мы импортировали ранее, даёт следующий пользовательский интерфейс: + + + +## Определяем требования к данным + +Лучшей практикой является использование `propTypes` в React для указания структуры данных, которые ожидает компонент. Это не только самодокументирование, но и помогает выявить проблемы на ранней стадии. + +```diff:title=src/components/Task.js +import React from 'react'; ++ import PropTypes from 'prop-types'; + +export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) { + // ... +} + ++ Task.propTypes = { ++ /** Composition of the task */ ++ task: PropTypes.shape({ ++ /** Id of the task */ ++ id: PropTypes.string.isRequired, ++ /** Title of the task */ ++ title: PropTypes.string.isRequired, ++ /** Current state of the task */ ++ state: PropTypes.string.isRequired, ++ }), ++ /** Event to change the task to archived */ ++ onArchiveTask: PropTypes.func, ++ /** Event to change the task to pinned */ ++ onPinTask: PropTypes.func, ++ }; +``` + +Теперь при неправильном использовании компонента Task в режиме разработки будет появляться предупреждение. + +
+💡 Альтернативным способом достижения той же цели является использование системы типов JavaScript, например TypeScript, для создания типа для свойств компонента. +
+ +## Компонент готов! + +Мы успешно создали компонент без необходимости использования сервера или запуска всего фронтенд-приложения. Следующим шагом будет создание остальных компонентов Taskbox аналогичным образом. + +Как видите, начать создавать изолированные компоненты легко и быстро. Мы можем рассчитывать на создание более качественного пользовательского интерфейса с меньшим количеством ошибок и большей доработкой, потому что можно покопаться и протестировать все возможные состояния. + +## Устраняем проблемы с доступностью + +Тесты доступности относятся к практике аудита визуализации DOM с помощью автоматизированных инструментов на основе набора эвристик, основанных на правилах [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/) и других принятых в отрасли лучших практик. Они выступают в качестве первой линии QA для выявления вопиющих нарушений доступности, гарантируя, что приложение будет пригодно для использования как можно большим количеством людей, включая людей с ограниченными возможностями, такими как нарушения зрения, проблемы со слухом и когнитивные заболевания. + +Storybook имеет официальный [аддон доступности](https://storybook.js.org/addons/@storybook/addon-a11y). Он работает на основе [axe-core](https://github.com/dequelabs/axe-core) от Deque и позволяет решить [57% проблем WCAG](https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues/). + +Давайте посмотрим, как это работает! Выполните следующую команду для установки аддона: + +```bash +yarn add --dev @storybook/addon-a11y +``` + +Затем обновите файл конфигурации Storybook (`.storybook/main.js`), чтобы включить его: + +```diff:title=.storybook/main.js +module.exports = { + stories: ['../src/components/**/*.stories.js'], + staticDirs: ['../public'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/preset-create-react-app', + '@storybook/addon-interactions', ++ '@storybook/addon-a11y', + ], + framework: '@storybook/react', + core: { + builder: '@storybook/builder-webpack5', + }, + features: { + interactionsDebugger: true, + }, +}; +``` + +![Проблема доступности задачи в Storybook](/intro-to-storybook/finished-task-states-accessibility-issue.png) + +Просматривая наши истории, мы видим, что аддон обнаружил проблему доступности в одном из наших тестовых состояний. Сообщение [**"Элементы должны иметь достаточный цветовой контраст "**](https://dequeuniversity.com/rules/axe/4.4/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; +} +``` + +Вот и всё! Мы сделали первый шаг к тому, чтобы сделать пользовательский интерфейс доступным. По мере дальнейшего усложнения нашего приложения мы сможем повторить этот процесс для всех остальных компонентов без необходимости в дополнительных инструментах или средах тестирования. + +
+💡 Не забудьте зафиксировать свои изменения с помощью git! +