Cree un botón con efecto dominó para la interfaz de usuario de XMars


Hola a todos, hoy les contaré cómo desarrollé el botón para el proyecto XMars UI . Oh sí, es un poco, pero hay algo que contar. Omitiré los detalles asociados con la adición de un nuevo componente al proyecto de código abierto. Con más detalle hablaré sobre el proyecto en un artículo separado.


Introduccion


XMars UI es uno de mis nuevos proyectos de código abierto. Una biblioteca simple de componentes de UI para HTML / CSS y React. En el futuro planeo apoyar a Vue. Hasta ahora, solo tiene un botón e íconos :)


El proyecto nació como una idea en el marco del Concurso Telegram, cuya esencia era desarrollar una versión web del cliente. Junto con un colega, decidimos por qué no participar en esto. Los roles se compartieron para que yo tenga un diseño, y cuando un colega entiende la autorización, me conectaré para escribir componentes. Todo estaría bien, pero iniciar sesión en Telegram no es tan simple. Como resultado, no enviamos nada, pero me encontré con un montón de todo y resultó, en vano. Pero como dice Varlamov, su proyecto ya vale algo, ya que pasó su tiempo en él. Es difícil estar en desacuerdo con esto, porque si se transfiere a horas y dinero, simplemente configurar Webpack al comienzo del proyecto ya no es gratuito. Mirando toda esta desgracia, decidí que tenía que arrojarla de alguna manera en código abierto. ¿Qué arranque único usar? Quiero mi propio marco de IU para mis otros proyectos.


El botón


El botón en la interfaz es quizás el elemento principal con el que el usuario interactúa con la aplicación. Por lo tanto, este es uno de los primeros componentes de cualquier marco / biblioteca de interfaz de usuario.


En el diseño de Telegram, no hay muchas variaciones de botones:



Destaqué 3 principales (predeterminado, acento, primario), redondo con un icono y verde. Todavía hay semitransparente, pero decepciona. En su mayor parte, al desarrollar la interfaz de usuario de XMars , trato de responder a las necesidades, no he descubierto dónde se necesitaría el botón transparente.


El usuario de la biblioteca debería sentirse cómodo usando clases de CSS. No soy fanático de sistemas de nombres como BEM. Me gusta la forma en que Bootstrap nombra las clases. Pero simplificaría un poco más. En lugar de .btn .btn-primary , solo .btn .primary . Y en el caso del componente React, se verá así:


 <Button primary>Hey</Button> 

El mismo botón pero efecto dominó:


 <Button primary ripple>Hey</Button> 

HTML / CSS


La biblioteca de IU no debe estar vinculada a ningún marco de IU. En el futuro planeo estirar el diseño en los componentes Vue . Comencemos con HTML / CSS simple.


Bajo el capó del proyecto Tailwindcss , este es un marco CSS de primera utilidad, es decir, un marco que le proporciona utilidades, en lugar de componentes completos.



Además de Tailwindcss, PostCSS se usa para mixins , variables y estilos anidados.


Con más detalle sobre el uso de dicho marco y cómo se configura el proyecto, lo contaré en un artículo separado. En esta etapa, es suficiente que tengamos un kit de herramientas tan poderoso y que los componentes se utilicen al máximo.


La <button> tiene una serie de estilos predeterminados que necesitamos eliminar o redefinir.



En el caso de Tailwindcss, la etiqueta del botón tiene este estilo:



Todo innecesario por defecto eliminado. Puede esculpir lo que quiera sin temor a que un borde predeterminado se caiga en algún estado. Pero aquí hay una advertencia, el outline predeterminado aún necesita ser clavado:



Un botón en XMars UI tiene una clase .btn :


 <button class="btn">Button</button> 

Agregue esta clase a nuestros estilos:


 .btn { @apply text-black text-base leading-snug; @apply py-3 px-4 border-none rounded-lg; @apply inline-block cursor-pointer no-underline; &:focus { @apply outline-none; } } 

Además del hecho de que Tailwindcss proporciona clases que puedes usar, proporciona una especie de mixins . @apply no es SCSS o algún tipo de complemento para PostCSS. Esta es la sintaxis de Tailwindcss en sí. Los estilos que se aplican generalmente se eliminan semánticamente del nombre. py-3 y px-4 solo pueden causar preguntas. El primero es el relleno a lo largo de y, es decir, verticalmente, a saber, padding-top: 0.75rem; padding-bottom: 0.75rem; . En consecuencia, el px-4 horizontalmente es padding-right: 1rem; , padding-left: 1rem; .


El diseño que proporcionó Telegram para decirlo suavemente no está bien documentado y cosas como border-radius botones de border-radius deben tomarse con una regla directamente desde la imagen. ¿Alguna vez se preguntó qué significan exactamente los valores en border-radius ?



Este es literalmente el radio del círculo resultante en la esquina. Si la granja colectiva, puede cambiar la regla como se muestra en la imagen de arriba. Entonces lo hice usando la selección rectangular en Gimp.



border-radius los botones en el diseño es de 10px, desafortunadamente no existe tal clase desde el cuadro en Tailwindcss, pero visualmente tenía rounded-lg que es 8px para el tamaño de fuente predeterminado (rem).


Esto es lo que sucedió en este momento: pinté el botón en gris para que se pudieran ver los bordes:



A continuación, necesitamos hacer un efecto en :hover . Luego, los diseñadores de Telegram decidieron arrojar algo de luz e indicaron el color como 0.08% de #707579 . Veo dos opciones, solo toma el color con el cuentagotas o hazlo como está documentado. La primera opción es más simple, pero en el futuro no es la mejor. El hecho es que si el fondo es diferente del blanco, entonces :hover , obtendremos un color específico, perderemos la "claridad" y la transparencia del botón. Para esto es mejor seguir la documentación y establecer alfa hombre El canal. Esto se puede hacer de innumerables maneras, por ejemplo, utilizando las funciones de color SCSS. Pero no hay SCSS en el proyecto y, debido al mismo color, no quiero conectar ningún complemento a PostCSS, haremos todo muy simple. En Chrome, hay un colopaker que le permite transformar colores en diferentes sistemas, conducir colores HEX #707579 , traducirlos a rgba y configurar el canal alfa - 0.08%.



Voila! Algo brilló bruscamente en mi foto:



Obtenemos - rgba(112, 117, 121, 0.08) .



(: desplazarse)


Más aburrido y sin mucho esfuerzo, agregué los estados restantes:


  &:hover { background-color: var(--grey04); } &.accent { color: var(--blue01); } &.primary { @apply text-white; background-color: var(--blue01); &:hover { background-color: var(--blue02); } } 

Componente de reacción


Inicialmente, el botón fue creado para el concurso de Telegram y era imposible usar ningún marco. Tuve que implementar el efecto dominó en JS puro. Realmente me gustaría que siga siendo así, pero mientras haces el proyecto solo, tienes que sacrificar algo.


Los componentes que requieren algún tipo de lógica, como el efecto dominó, se implementarán y estarán disponibles solo como componentes React.


Ajustar el botón en React componente no es difícil:


 import React, { FunctionComponent } from 'react'; export interface ButtonProps { } const Button: FunctionComponent<ButtonProps> = (props) => { return ( <button className="btn">props.children</button> ); } export default Button; 

Este botón se mostrará en el estilo especificado, pero de hecho es de poca utilidad. Necesitamos permitir que el usuario personalice el botón, agregue estilos personalizados, cuelgue controladores de eventos, etc.


Para que el usuario pueda transferir todo lo necesario, primero debe superar Typecript, de lo contrario, incluso onClick no le permitirá transferir normalmente. ButtonProps editar ligeramente la interfaz ButtonProps , resolvemos el problema:


 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> 

después de lo cual podemos hacer con seguridad la destrucción de los props :


 <button className="btn" {...props}>props.children</button> 

El uso similar del botón se comportará como se esperaba:


 <Button onClick={() => alert()}>Hey</Button> 


A continuación, agregamos los estilos de botón y la capacidad de registrar una clase personalizada (de repente alguien necesitará). El paquete npm classnames es perfecto para estos fines.


 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, additionalClass?: string, } ... const classNames = classnames( 'btn', { primary }, { accent }, additionalClass ); ... <button className={classNames} {...props}>props.children</button> 

La clase btn siempre se establece, pero primary y accent solo si es true . Classnames agrega una clase si tiene un valor true lógico, usando la abreviatura ES6 obtenemos una entrada simple { primary } lugar de { primary: true } .


additionalClass es una cadena, y si está vacía o indefinida, no juega un papel especial para nosotros, simplemente no se agregará nada al elemento.


Al principio, asigné props siguiente manera:


 {...omit(props, ['additionalClass', 'primary'])} 

Omitir todo lo que no se aplica a los props del elemento del botón, pero esto no es necesario ya que React no generará demasiado.


Efecto dominó



En realidad, así es como se ve, pero es deseable que la "ola" diverja del lugar del clic.
Hay innumerables formas de hacer tal animación, es como una broma sobre el cuadrado azul .


Pero google, al observar los ejemplos en codepen, quedó claro que en la mayoría de los casos, una "ola" se realiza a través de un elemento hijo, que se expande y desaparece.


Se coloca dentro del botón de acuerdo con las coordenadas del clic. En XMars UI , en este momento decidí no implementar este efecto en onPress como lo hace la UI de material, pero planeo onPress en el futuro. Hasta ahora, solo en onClick .



La imagen de arriba es mágica. Un clic crea un elemento de botón secundario, se posiciona absolutamente, en el centro del clic y se expande. El overflow: hidden propiedad overflow: hidden ola vaya más allá del botón. El elemento debe eliminarse al final de la animación.


Primero, definiremos los estilos donde podamos, usando Tailwindcss tanto como sea posible:


 .with-ripple { @apply relative overflow-hidden; @keyframes ripple { to { @apply opacity-0; transform: scale(2.5); } } .ripple { @apply absolute; z-index: 1; border-radius: 50%; background-color: var(--grey04); transform: scale(0); animation: ripple 0.6s linear; } &.primary { .ripple { background-color: var(--black02); } } } 

Al elemento responsable del efecto se le asignará la clase .ripple . border-radius: 50%; es igual a un círculo (50% de filete en ángulo * 2), el botón tiene una posición relativa, el botón .ripple tiene una posición absoluta. La animación es muy simple, el aumento de la onda se vuelve transparente en 0.6 segundos. El color de fondo es el mismo que :hover y plegarse, dos colores y botones transparentes de "onda" nos dan el resultado deseado. En el botón azul .primary , esto no es tan importante y allí puede usar un color no transparente.


Al hacer clic, debe crear el elemento "ola". Por lo tanto, creamos un estado para este negocio y agregamos el controlador de clic apropiado al botón, pero de tal manera que no interfiera con el onClick personalizado.


 ... const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); ... function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> ); 

rippleElements : una matriz de elementos JSX, la función de renderizado aquí puede parecer redundante, pero esto es más una cuestión de estilo e incorporación para el futuro.


  function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); } 

controlador onRippleClick que crea "ondas". Al hacer clic en el botón, descubrimos el tamaño del botón que se utiliza para colocar el círculo correctamente, después de lo cual todo lo necesario se pasa a la newRippleElement función newRippleElement que a su vez simplemente crea un elemento div con la clase de ripple , creando los estilos necesarios para el posicionamiento.


De las cosas principales que vale la pena destacar en onAnimationEnd . Necesitamos este evento para borrar el DOM de elementos ya utilizados.


  function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); } 

Es muy importante no olvidar, pasar los rippleElements actuales a argumentos, de lo contrario, puede obtener una matriz con valores antiguos, y todo no funcionará según lo previsto.


Código de botón completo:


 import React, { FunctionComponent, ButtonHTMLAttributes, useState } from 'react'; import uuid from 'uuid/v4'; import classnames from 'classnames'; export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, circle?: boolean, ripple?: boolean, additionalClass?: string, } const Button: FunctionComponent<ButtonProps> = (props) => { const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); const {primary, accent, circle, ripple, additionalClass, children} = props; const classNames = classnames( 'btn', { primary }, { 'with-ripple': ripple }, { circle }, { accent }, additionalClass ); function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); } function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); } function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> ); } export default Button; 

El resultado final se puede informar aquí.


Conclusión


Se omitió mucho, por ejemplo, cómo se configuró el proyecto, cómo se escribe la documentación, las pruebas de un nuevo componente en el proyecto. Trataré de cubrir estos temas con publicaciones separadas.


Repositorio de XMars UI Github

Source: https://habr.com/ru/post/484554/


All Articles