Creación de componentes de interfaz de usuario en SVG | Programar Plus

Estoy completamente convencido de que SVG desbloquea todo un mundo de interfaces de construcción en la web. Puede parecer abrumador aprender SVG al principio, pero tiene una especificación que fue diseñada para crear formas y, sin embargo, todavía tiene elementos, como texto, enlaces y etiquetas aria disponibles para usted. Puede lograr algunos de los mismos efectos en CSS, pero es un poco más particular obtener el posicionamiento correcto, especialmente en las ventanas gráficas y para el desarrollo receptivo.

Lo especial de SVG es que todo el posicionamiento se basa en un sistema de coordenadas, un poco como el juego. Acorazado. Eso significa que decidir dónde va todo y cómo se dibuja, así como también cómo se relaciona entre sí, puede ser muy sencillo de razonar. El posicionamiento CSS es para el diseño, lo cual es excelente porque tiene cosas que se corresponden entre sí en términos del flujo del documento. Es más difícil trabajar con este rasgo positivo si está creando un componente que es muy particular, con elementos superpuestos y colocados con precisión.

En verdad, una vez que aprenda SVG, puede dibujar cualquier cosa y hacer que se escale en cualquier dispositivo. Incluso este mismo sitio usa SVG para elementos de interfaz de usuario personalizados, como mi avatar, arriba (¡meta!).

Ese pequeño semicírculo debajo de la imagen del autor es solo una marca SVG.

No cubriremos todo sobre los SVG en esta publicación (puede aprender algunos de esos fundamentos aquí, aquí, aquí y aquí), pero para ilustrar las posibilidades que SVG abre para el desarrollo de componentes de interfaz de usuario, hablemos de un caso de uso particular y analizar cómo pensaríamos en construir algo personalizado.

El componente de lista de tareas de línea de tiempo

Recientemente, estaba trabajando en un proyecto con mi equipo en Netlify. Queríamos mostrar al espectador qué vídeo de una serie de vídeos de un curso estaba viendo en ese momento. En otras palabras, queríamos crear algo parecido a una lista de tareas pendientes, pero que mostrara el progreso general a medida que se completan los elementos. (Hicimos una plataforma de aprendizaje gratuita con temática espacial y es genial. Sí, dije hella).

Así es como se ve:

Entonces, ¿cómo haríamos esto? Mostraré un ejemplo tanto en Vue como en React para que pueda ver cómo podría funcionar en ambos marcos.

El versión vue

Decidimos hacer la plataforma en Next.js con fines de prueba interna (es decir, probar nuestro propio complemento de compilación Next en Netlify), pero soy más fluido en Vue, así que escribí el prototipo inicial en Vue y lo transfiero a React.

Aquí está la demostración completa de CodePen:

Veamos un poco este código. En primer lugar, este es un componente de archivo único (SFC), por lo que la plantilla HTML, el script reactivo y los estilos de alcance están todos encapsulados en este único archivo.

Guardaremos algunas tareas ficticias en data, incluido si cada tarea se completó o no. También crearemos un método al que podamos llamar en una directiva de clic para que podamos cambiar si el estado está listo o no.

<script>
export default {
  data() {
    return {
      tasks: [
        {
          name: 'thing',
          done: false
        },
        // ...
      ]
    };
  },
  methods: {
    selectThis(index) {
      this.tasks[index].done = !this.tasks[index].done
    }
  }
};
</script>

Ahora, lo que queremos hacer es crear un SVG que tenga un formato flexible viewBox dependiendo de la cantidad de elementos. También queremos decirles a los lectores de pantalla que este es un elemento de presentación y que proporcionaremos un título con una identificación única de timeline. (Obtenga más información sobre cómo crear archivos SVG accesibles).

<template>
  <div id="app">
    <div>
      <svg :viewBox="`0 0 30 ${tasks.length * 50}`"
           xmlns="http://www.w3.org/2000/svg" 
           width="30" 
           stroke="currentColor" 
           fill="white"
           aria-labelledby="timeline"
           role="presentation">
           <title id="timeline">timeline element</title>
        <!-- ... -->
      </svg>
    </div>
  </div>
</template>

El stroke se establece en currentColor para permitir cierta flexibilidad: si queremos reutilizar el componente en varios lugares, heredará lo que sea color se utiliza en el div de encapsulación.

A continuación, dentro del SVG, queremos crear una línea vertical que tenga la longitud de la lista de tareas. Las líneas son bastante sencillas. Tenemos x1 y x2 valores (donde la línea se traza en el eje x), y de manera similar, y1 y y2.

<line x1="10" x2="10" :y1="num2" :y2="tasks.length * num1 - num2" />

El eje x permanece consistentemente en 10 porque estamos dibujando una línea hacia abajo en lugar de de izquierda a derecha. Guardaremos dos números en datos: la cantidad que queremos que sea nuestro espaciado, que será num1, y la cantidad que queremos que sea nuestro margen, que será num2.

data() {
  return {
    num1: 32,
    num2: 15,
    // ...
  }
}

El eje y comienza con num2, que se resta del final, así como del margen. El tasks.length se multiplica por el espaciado, que es num1.

Ahora, necesitaremos los círculos que se encuentran en la línea. Cada círculo es un indicador de si una tarea se ha completado o no. Necesitaremos un círculo para cada tarea, así que usaremos v-for con un único key, que es el índice (y es seguro usarlo aquí ya que nunca se reordenarán). Conectaremos el click directiva con nuestro método y pasar el índice como un parámetro también.

Los círculos en SVG se componen de tres atributos. El centro del círculo se traza en cx y cy, y luego dibujamos un radio con r. como la línea, cx comienza en 10. El radio es 4 porque eso es lo que se puede leer en esta escala. cy estará espaciado como la línea: índice multiplicado por el espaciado (num1), más el margen (num2).

Finalmente, usaremos un ternario para configurar el fill. Si la tarea está hecha, se llenará con currentColor. Si no, se llenará con white (o cualquiera que sea el fondo). Esto podría llenarse con un accesorio que pasa en el fondo, por ejemplo, donde tiene círculos claros y oscuros.

<circle 
  @click="selectThis(i)" 
  v-for="(task, i) in tasks"
  :key="task.name"
  cx="10"
  r="4"
  :cy="i * num1 + num2"
  :fill="task.done ? 'currentColor' : 'white'"
  class="select"/>

Finalmente, estamos usando la cuadrícula CSS para alinear un div con los nombres de las tareas. Esto se presenta de la misma manera, donde estamos recorriendo las tareas y también estamos vinculados al mismo evento de clic para alternar el estado terminado.

<template>
  <div>
    <div 
      @click="selectThis(i)"
      v-for="(task, i) in tasks"
      :key="task.name"
      class="select">
      {{ task.name }}
    </div>
  </div>
</template>

El Reaccionar versión

Aquí es donde terminamos con la versión React. Estamos trabajando para abrir esto para que pueda ver el código completo y su historial. Aquí hay algunas modificaciones:

  • Estamos usando módulos CSS en lugar de SCF en Vue
  • Estamos importando el enlace de Next.js, de modo que, en lugar de alternar el estado “hecho”, estamos llevando al usuario a una página dinámica en Next.js.
  • Las tareas que estamos usando son en realidad etapas del curso, o “Misión”, como las llamamos, que se pasan aquí en lugar de que las retenga el componente.

La mayor parte de la otra funcionalidad es la misma 🙂

import styles from './MissionTracker.module.css';
import React, { useState } from 'react';
import Link from 'next/link';

function MissionTracker({ currentMission, currentStage, stages }) {
 const [tasks, setTasks] = useState([...stages]);
 const num1 = 32;
 const num2 = 15;

 const updateDoneTasks = (index) => () => {
   let tasksCopy = [...tasks];
   tasksCopy[index].done = !tasksCopy[index].done;
   setTasks(tasksCopy);
 };

 const taskTextStyles = (task) => {
   const baseStyles = `${styles['tracker-select']} ${styles['task-label']}`;

   if (currentStage === task.slug.current) {
     return baseStyles + ` ${styles['is-current-task']}`;
   } else {
     return baseStyles;
   }
 };

 return (
   <div className={styles.container}>
     <section>
       {tasks.map((task, index) => (
         <div
           key={`mt-${task.slug}-${index}`}
           className={taskTextStyles(task)}
         >
           <Link href={`/learn/${currentMission}/${task.slug.current}`}>
             {task.title}
           </Link>
         </div>
       ))}
     </section>

     <section>
       <svg
         viewBox={`0 0 30 ${tasks.length * 50}`}
         className={styles['tracker-svg']}
         xmlns="http://www.w3.org/2000/svg"
         width="30"
         stroke="currentColor"
         fill="white"
         aria-labelledby="timeline"
         role="presentation"
       >
         <title id="timeline">timeline element</title>

         <line x1="10" x2="10" y1={num2} y2={tasks.length * num1 - num2} />
         {tasks.map((task, index) => (
           <circle
             key={`mt-circle-${task.name}-${index}`}
             onClick={updateDoneTasks(index)}
             cx="10"
             r="4"
             cy={index * +num1 + +num2}
             fill={
               task.slug.current === currentStage ? 'currentColor' : 'black'
             }
             className={styles['tracker-select']}
           />
         ))}
       </svg>
     </section>
   </div>
 );
}

export default MissionTracker;

Versión final

Puede ver la versión final de trabajo aquí:

Ver sitio

Este componente es lo suficientemente flexible para acomodar listas pequeñas y grandes, múltiples navegadores y tamaños receptivos. También permite al usuario tener una mejor comprensión de dónde se encuentra en su progreso en el curso.

Pero esto es solo un componente. Puede crear cualquier cantidad de elementos de la interfaz de usuario: perillas, controles, indicadores de progreso, cargadores… el cielo es el límite. Puede diseñarlos con CSS o estilos en línea, puede hacer que se actualicen según los accesorios, el contexto, los datos reactivos, ¡el cielo es el límite! Espero que esto abra algunas puertas sobre cómo usted mismo puede desarrollar elementos de interfaz de usuario más atractivos para la web.

(Visited 7 times, 1 visits today)