Criar um botão com o Ripple Effect para XMars UI


Olá pessoal, hoje vou lhe contar como desenvolvi o botão para o projeto de interface do usuário do XMars . Ah, sim, é um pouco, mas há algo a dizer. Omitirei os detalhes associados à adição de um novo componente ao projeto de código aberto. Mais detalhadamente, falarei sobre o projeto em outro artigo.


1. Introdução


A interface do usuário do XMars é um dos meus novos projetos de código aberto. Uma biblioteca simples de componentes de interface do usuário para HTML / CSS e React. No futuro, pretendo apoiar o Vue. Até agora, ele possui apenas um botão e ícones :)


O projeto nasceu como uma ideia no âmbito do concurso Telegram, cuja essência era desenvolver uma versão web do cliente. Juntamente com um colega, decidimos por que não participar disso. As funções foram compartilhadas para que eu tenha um layout e, quando um colega entender a autorização, conectarei para escrever componentes. Tudo ficaria bem, mas o login no Telegram não é tão simples. Como resultado, não enviamos nada, mas eu pego um monte de tudo e acontece - em vão. Mas, como diz Varlamov, seu projeto já vale alguma coisa, já que você gastou seu tempo nele. É difícil discordar disso, porque se você transferir para horas e dinheiro, apenas a configuração do Webpack no início do projeto não será mais gratuita. Olhando para toda essa desgraça, eu decidi que tinha que de alguma forma jogá-la em código aberto. Qual bootstrap único para usar? Quero minha própria estrutura de interface do usuário para meus outros projetos.


O botão


O botão na interface é talvez o elemento principal com o qual o usuário interage com o aplicativo. Portanto, este é um dos primeiros componentes de qualquer estrutura / biblioteca de interface do usuário.


No design do Telegram, não há muitas variações de botões:



Eu destaquei 3 principais (padrão, sotaque, primário), redondos com um ícone e verde. Ainda há semi-transparente, mas desaponte. Na maioria das vezes, ao desenvolver a interface do usuário do XMars , tento prosseguir com as necessidades, ainda não descobri onde o botão transparente seria necessário.


O usuário da biblioteca deve se sentir confortável usando classes CSS. Não sou fã de sistemas de nomes como o BEM. Eu gosto da maneira como o Bootstrap nomeia as classes. Mas eu simplificaria um pouco mais. Em vez de .btn .btn-primary , apenas .btn .primary . .btn .primary . E no caso do componente React, ele terá a seguinte aparência:


 <Button primary>Hey</Button> 

O mesmo botão, mas com efeito cascata:


 <Button primary ripple>Hey</Button> 

HTML / CSS


A biblioteca da interface do usuário não deve estar vinculada a nenhuma estrutura da interface do usuário. No futuro, pretendo estender o layout dos componentes do Vue . Vamos começar com HTML / CSS simples.


Sob o capô do projeto Tailwindcss , essa é uma estrutura CSS de primeiro utilitário, ou seja, uma estrutura que fornece utilitários, em vez de componentes completos.



Além do Tailwindcss, o PostCSS é usado para mixins , variáveis e estilos aninhados.


Em mais detalhes sobre o uso dessa estrutura e como o projeto está configurado, vou contar em outro artigo. Nesse estágio, basta que tenhamos um kit de ferramentas tão poderoso e que utilizemos os componentes que ele utiliza ao máximo.


A <button> possui vários estilos padrão que precisamos remover ou redefinir.



No caso de Tailwindcss, a tag button tem este estilo:



Tudo desnecessário por padrão removido. Você pode esculpir o que deseja, sem medo de que uma borda padrão caia em algum estado. Mas aqui está uma ressalva, o outline padrão ainda precisa ser pregado:



Um botão na interface do usuário do XMars possui uma classe .btn :


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

Adicione esta classe aos nossos 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; } } 

Além do fato de o Tailwindcss fornecer classes que você pode usar, ele fornece um tipo de mixins . @apply não é SCSS ou algum tipo de plugin para PostCSS. Essa é a sintaxe do próprio Tailwindcss. Os estilos aplicados geralmente são semanticamente claros no nome. Somente py-3 e px-4 podem causar perguntas. O primeiro é o preenchimento ao longo de y, ou seja, verticalmente, ou seja, o padding-top: 0.75rem; padding-bottom: 0.75rem; . Conseqüentemente, o px-4 horizontalmente é padding-right: 1rem; , padding-left: 1rem; .


O design que o Telegram forneceu para dizer o mínimo é pouco documentado e coisas como botões de border-radius devem ser tirados com uma régua diretamente da imagem. Já imaginou o que exatamente significam valores no border-radius ?



Este é literalmente o raio do círculo resultante no canto. Se a fazenda coletiva, você pode alterar a régua, como mostrado na figura acima. Então usei a seleção retangular no Gimp.



border-radius botões no design é 10px, infelizmente não existe essa classe da caixa no Tailwindcss, mas eu visualmente tinha rounded-lg que é 8px para o tamanho da fonte padrão (rem).


Aqui está o que aconteceu no momento: eu pintei o botão em cinza para que as bordas pudessem ser vistas:



Em seguida, precisamos fazer efeito sobre :hover . Então os designers do Telegram decidiram lançar alguma luz e indicaram a cor como 0,08% de #707579 . Vejo duas opções: basta pegar a cor com o conta-gotas ou fazê-lo como documentado. A primeira opção é mais simples, mas no futuro não é a melhor. O fato é que, se o fundo for diferente do branco, em seguida :hover , obteremos uma cor específica, perderemos a “leveza” e a transparência do botão. Para isso, é melhor seguir a documentação e colocar alpha macho o canal. Isso pode ser feito de inúmeras maneiras, por exemplo, usando as funções de cores SCSS. Mas não há SCSS no projeto e, por causa da mesma cor, não quero conectar nenhum plug-in ao PostCSS, tornaremos tudo muito simples. No Chrome, existe um colopaker que permite transformar cores em sistemas diferentes, levar as cores HEX #707579 , traduzir para rgba e definir o canal alfa - 0,08%.



Voila! Algo brilhou rapidamente na minha foto:



Nós temos - rgba(112, 117, 121, 0.08) .



(: passe o mouse)


Mais chato e sem muito esforço, adicionei o resto do estado:


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

Reagir componente


Inicialmente, o botão foi criado para o concurso Telegram e era impossível usar qualquer estrutura. Eu tive que implementar o efeito cascata em JS puro. Eu realmente gostaria que isso permanecesse, mas enquanto você está fazendo o projeto sozinho, você tem que sacrificar alguma coisa.


Os componentes que requerem algum tipo de lógica, como o efeito cascata, serão implementados e disponíveis apenas como componentes do React.


Enrolar o botão no componente React não é 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ão será exibido no estilo especificado, mas na verdade é pouco útil. Precisamos habilitar o usuário a personalizar o botão, adicionar estilos personalizados, travar manipuladores de eventos e assim por diante.


Para que o usuário possa transferir tudo o necessário, primeiro você precisa superar o Typescript, caso contrário, nem o onClick permitirá que você transfira normalmente. Editando levemente a interface ButtonProps , resolvemos o problema:


 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> 

após o qual podemos fazer com segurança a destruição dos props


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

O uso semelhante do botão se comportará conforme o esperado:


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


Em seguida, adicionamos os estilos de botão e a capacidade de registrar uma classe personalizada (de repente alguém precisará). O pacote npm classnames é perfeito para esses propósitos.


 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> 

A classe btn é sempre definida, mas primary e accent somente se true . Os nomes de classe adicionam uma classe se ela tiver um valor lógico true . Usando a abreviação ES6, obtemos uma entrada simples { primary } , em vez de { primary: true } .


additionalClass é uma string e, se estiver vazia ou indefinida, não desempenha um papel especial para nós, apenas nada será adicionado ao elemento.


No começo, designei props seguinte maneira:


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

Omitir tudo o que não se aplica aos props do elemento button, mas isso não é necessário, pois o React não renderizará muito.


Efeito cascata



Na verdade, é assim que parece, mas é desejável que a “onda” diverja do local do clique.
Existem inúmeras maneiras de fazer essa animação, é como uma piada sobre o quadrado azul .


Mas o google, olhando os exemplos no codepen, ficou claro que, na maioria dos casos, uma “onda” é realizada através de um elemento filho, que se expande e desaparece.


É posicionado dentro do botão de acordo com as coordenadas do clique. Na interface do XMars , no momento, decidi não implementar esse efeito no onPress como a interface do material, mas pretendo onPress lo no futuro. Até agora, apenas no onClick .



A imagem acima é toda mágica. Um clique cria um elemento de botão filho, é posicionado absolutamente, no centro do clique e se expande. A propriedade overflow: hidden onda ultrapasse o botão. O item deve ser excluído no final da animação.


Primeiro, definiremos os estilos onde pudermos, usando o Tailwindcss o máximo possível:


 .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); } } } 

O elemento responsável pelo efeito receberá a classe .ripple . border-radius: 50%; é igual a um círculo (filete de 50% no ângulo * 2), o botão tem posicionamento relativo, o botão .ripple tem posicionamento absoluto. A animação é muito simples, o aumento da onda fica transparente em 0,6 segundos. A cor de fundo é a mesma que :hover e dobra, duas cores e botões transparentes de "onda" nos dão o resultado desejado. No botão azul .primary , isso não é tão importante e é possível usar uma cor não transparente.


Ao clicar, você precisa criar o elemento "wave". Portanto, criamos um estado para esta empresa e adicionamos o manipulador de cliques apropriado ao botão, mas de forma que não interfira com o 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 - uma matriz de elementos JSX, a função de renderização aqui pode parecer redundante, mas isso é mais uma questão de estilo e incorporação para o 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> ); } 

Manipulador onRippleClick que cria "ondas". Ao clicar no botão, descobrimos o tamanho do botão usado para posicionar o círculo corretamente, após o qual tudo é passado para a função newRippleElement que por sua vez simplesmente cria um elemento div com a classe ripple , criando os estilos necessários para o posicionamento.


Das principais coisas que vale a pena destacar onAnimationEnd . Precisamos desse evento para limpar o DOM dos elementos já usados.


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

É muito importante não esquecer, passar os rippleElements atuais para argumentos, caso contrário, você pode obter uma matriz com valores antigos e tudo não funcionará como pretendido.


Código do botão 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; 

O resultado final pode ser reportado aqui.


Conclusão


Muito foi omitido, por exemplo, como o projeto foi configurado, como a documentação é escrita, testes para um novo componente no projeto. Vou tentar cobrir esses tópicos com publicações separadas.


Repositório do XMars UI Github

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


All Articles