Construye un componente simple
Construiremos nuestra UI siguiendo la metodología (CDD) Component-Driven Development. Es un proceso que construye UIs de “abajo hacia arriba”, empezando con los componentes y terminando con las pantallas. CDD te ayuda a escalar la cantidad de complejidad con la que te enfrentas a medida que construyes la UI.
Task - Tarea
Task
(o Tarea) es el componente principal de nuestra aplicación. Cada tarea se muestra de forma ligeramente diferente según el estado en el que se encuentre. Mostramos un checkbox marcado (o no marcado), información sobre la tarea y un botón “pin” que nos permite mover la tarea hacia arriba o abajo en la lista de tareas. Para lograr esto, necesitaremos estas propiedades (props) :
title
– un string que describe la tareastate
- ¿en qué lista se encuentra la tarea actualmente y está marcado el checkbox?
A medida que comencemos a construir Task
, primero escribiremos nuestros tests para los estados que corresponden a los distintos tipos de tareas descritas anteriormente. Luego, utilizamos Storybook para construir el componente de forma aislada usando datos de prueba. Vamos a “testear visualmente” la apariencia del componente a medida que cambiemos cada estado.
Ajustes iniciales
Primero, vamos a crear el componente Task y el archivo de historias de Storybook que lo acompaña: src/components/Task.js
y src/components/Task.stories.jsx
.
Comenzaremos con una implementación básica de Task
, simplemente teniendo en cuenta los atributos que sabemos que necesitaremos y las dos acciones que puedes realizar con una tarea (para moverla entre las listas):
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className="list-item">
<label htmlFor={`title-${id}`} aria-label={title}>
<input type="text" value={title} readOnly={true} name="title" id={`title-${id}`} />
</label>
</div>
);
}
Arriba, renderizamos directamente Task
basándonos en la estructura HTML existente de la app Todos.
A continuación creamos los tres estados de prueba de Task
dentro del archivo de historia:
import { fn } from "@storybook/test";
import Task from './Task';
export const ActionsData = {
onArchiveTask: fn(),
onPinTask: fn(),
};
export default {
component: Task,
title: 'Task',
tags: ['autodocs'],
//👇 Our exports that end in "Data" are not stories.
excludeStories: /.*Data$/,
args: {
...ActionsData,
},
};
export const Default = {
args: {
task: {
id: '1',
title: 'Test Task',
state: 'TASK_INBOX',
},
},
};
export const Pinned = {
args: {
task: {
...Default.args.task,
state: 'TASK_PINNED',
},
},
};
export const Archived = {
args: {
task: {
...Default.args.task,
state: 'TASK_ARCHIVED',
},
},
};
💡 Las Acciones ayudan a verificar las interacciones cuando creamos componentes UI en aislamiento. A menudo no tendrás acceso a las funciones y el estado que tienes en el contexto de la aplicación. Utiliza fn()
para agregarlas.
Existen dos niveles básicos de organización en Storybook: el componente y sus historias hijas. Piensa en cada historia como una permutación posible del componente. Puedes tener tantas historias por componente como se necesite.
- Componente
- Historia
- Historia
- Historia
Para contarle a Storybook sobre el componente que estamos documentando, creamos un export default
que contiene:
component
-- el propio componentetitle
-- como hacer referencia al componente en el sidebar de la aplicación Storybooktags
-- para generar automáticamente documentación para nuestros componentesexcludeStories
-- información adicional requerida por la historia pero que no debe mostrarse en Storybookargs
- define la acción args que el component espera para simular los eventos personalizados
Para definir nuestras historias, utilizaremos el formato Component Story Format 3 (también conocido como CSF3 ) para crear cada uno de nuestros casos de prueba. Este formato está diseñado para crear cada uno de nuestros casos de prueba de manera concisa. Al exportar un objeto que contiene cada estado del componente, podemos definir nuestras pruebas de manera más intuitiva y crear y reutilizar historias de manera más eficiente.
Argumentos o args
, nos permiten editar en vivo nuestros componentes con el complemento "Controls" sin reiniciar Storybook. Una vez que cambia el valor de un arg
, el componente también cambia.
fn()
nos permite crear llamadas o "callbacks" que aparecen en el panel de actions de la UI de Storybook cuando se hace clic. Así que cuando construyamos un botón de pin, podremos determinar si un clic en el botón es exitoso en la interfaz de usuario.
Dado que necesitamos pasar el mismo conjunto de acciones a todas las permutaciones de nuestro componente, es conveniente agruparlas en una única variable ActionsData
(acciones de data) y pasarlas en nuestra definición de historia cada vez. Otra ventaja de agrupar los ActionsData
que un componente necesita es que puedes exportarlas y usarlas en historias para componentes que reutilizan este componente, como veremos más adelante.
Al crear una historia utilizamos un argumento base de task
para construir la forma de la task que el componente espera. Esto generalmente se modela a partir del aspecto de los datos verdaderos. Nuevamente, export
-ando esta función nos permitirá reutilizarla en historias posteriores, como veremos.
Configuración
Necesitamos hacer un par de cambios en los archivos de configuración de Storybook para que reconozca nuestras historias recién creadas y nos permita usar el archivo CSS de la aplicación (ubicado en src/index.css
).
Comenzaremos cambiando tu archivo de configuración de Storybook (.storybook/main.js
) a lo siguiente:
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+ stories: ['../src/components/**/*.stories.@(js|jsx)'],
staticDirs: ['../public'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;
Una vez que hayamos hecho esto, dentro de la carpeta .storybook
, cambia tu preview.js
a lo siguiente:
+ import '../src/index.css';
//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
Los parametros
se utilizan normalmente para controlar el comportamiento de las funciones y complementos (addons) de Storybook. En nuestro caso, no los usaremos para ese propósito. En su lugar, importaremos el archivo CSS de nuestra aplicación.
Una vez que hayamos hecho esto, reiniciar el servidor de Storybook debería producir casos de prueba para los tres estados de Task:
Construyendo los estados
Ahora que tenemos configurado Storybook, los estilos importados y los casos de prueba creados, podemos comenzar rápidamente a implementar el HTML del componente para que coincida con el diseño.
El componente todavía es básico en este momento. Primero, escribiremos el código que logre el diseño sin entrar en demasiados detalles:
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className={`list-item ${state}`}>
<label
htmlFor={`archiveTask-${id}`}
aria-label={`archiveTask-${id}`}
className="checkbox"
>
<input
type="checkbox"
disabled={true}
name="checked"
id={`archiveTask-${id}`}
checked={state === "TASK_ARCHIVED"}
/>
<span
className="checkbox-custom"
onClick={() => onArchiveTask(id)}
/>
</label>
<label htmlFor={`title-${id}`} aria-label={title} className="title">
<input
type="text"
value={title}
readOnly={true}
name="title"
id={`title-${id}`}
placeholder="Input title"
/>
</label>
{state !== "TASK_ARCHIVED" && (
<button
className="pin-button"
onClick={() => onPinTask(id)}
id={`pinTask-${id}`}
aria-label={`pinTask-${id}`}
key={`pinTask-${id}`}
>
<span className={`icon-star`} />
</button>
)}
</div>
);
}
El maquetado adicional de arriba, combinado con el CSS que hemos importado antes, produce la siguiente UI:
Especificar los requerimientos de datos
Es una buena práctica utilizar propTypes
en React para especificar la forma de los datos que un componente espera. No sólo se auto documenta, sino que también ayuda a detectar problemas de manera temprana.
+ import PropTypes from 'prop-types';
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className={`list-item ${state}`}>
<label
htmlFor={`archiveTask-${id}`}
aria-label={`archiveTask-${id}`}
className="checkbox"
>
<input
type="checkbox"
disabled={true}
name="checked"
id={`archiveTask-${id}`}
checked={state === "TASK_ARCHIVED"}
/>
<span
className="checkbox-custom"
onClick={() => onArchiveTask(id)}
/>
</label>
<label htmlFor={`title-${id}`} aria-label={title} className="title">
<input
type="text"
value={title}
readOnly={true}
name="title"
id={`title-${id}`}
placeholder="Input title"
/>
</label>
{state !== "TASK_ARCHIVED" && (
<button
className="pin-button"
onClick={() => onPinTask(id)}
id={`pinTask-${id}`}
aria-label={`pinTask-${id}`}
key={`pinTask-${id}`}
>
<span className={`icon-star`} />
</button>
)}
</div>
);
}
+ 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,
+ };
Ahora aparecerá una advertencia en modo desarrollo si se usa incorrectamente el componente Task.
¡Componente construido!
Ahora hemos construido con éxito un componente sin necesidad de un servidor o sin ejecutar toda la aplicación frontend. El siguiente paso es construir los componentes restantes de la Taskbox, uno por uno de manera similar.
Como puedes ver, comenzar a construir componentes de forma aislada es fácil y rápido. Podemos esperar producir una UI de mayor calidad con menos errores y más elegancia porque es posible profundizar y probar todos los estados posibles.
Detectar problemas de accesibilidad
Las pruebas de accesibilidad se refieren a la práctica de auditar el DOM renderizado con herramientas automatizadas contra un conjunto de heurísticas basadas en las reglas WCAG y otras mejores prácticas aceptadas por la industria. Actuán como la primera línea de control de calidad (QA) para detectar infracciones obvias de accesibilidad y aseguran que una aplicación sea utilizable por la mayor cantidad de personas posible, incluidas personas con discapacidades como problemas de visión, problemas auditivos y condiciones cognitivas.
Storybook también tiene un complemento oficial de accesibilidad. Desarrollado con el axe-core de Deque, puede detectar hasta 57% de los problemas de WCAG.
¡Vamos a ver como funciona! Ejecuta el comando siguiente para instalar el complemento:
yarn add --dev @storybook/addon-a11y
Luego, actualiza tu archivo de configuración de Storybook (.storybook/main.js
) para activarlo:
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../src/components/**/*.stories.@(js|jsx)'],
staticDirs: ['../public'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
+ '@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;
Para terminar, reinicia tu Storybook para ver el nuevo complemento habilitado en la UI.
Mirando nuestras historias, podemos ver que el complemento encontró un problema de accesibilidad con uno de nuestros estados de prueba. El mensaje "Los elementos deben tener suficiente contraste de color", significa que no hay suficiente contraste entre el título de la tarea y el fondo. Podemos solucionarlo rápidamente cambiando el color de texto a un gris más oscuro en el CSS de nuestra aplicación (ubicado en src/index.css
).
.list-item.TASK_ARCHIVED input[type="text"] {
- color: #a0aec0;
+ color: #4a5568;
text-decoration: line-through;
}
¡Terminamos! Hemos dado el primer paso para garantizar que la UI sea accesible. Mientras continuamos agregando complejidad a nuestra aplicación, podemos repetir este proceso para todos los demás componentes sin necesidad de usar herramientas adicionales o entornos de prueba.