Créer un bouton avec Ripple Effect pour XMars UI


Bonjour à tous, aujourd'hui, je vais vous dire comment j'ai développé le bouton pour le projet XMars UI . Oh oui, c'est une bagatelle, mais il y a quelque chose à dire. Je vais omettre les détails associés à l'ajout d'un nouveau composant au projet open source. Plus en détail, je parlerai du projet dans un article séparé.


Présentation


XMars UI est l'un de mes nouveaux projets open source. Une bibliothèque simple de composants d'interface utilisateur pour HTML / CSS et React. À l'avenir, je prévois de soutenir Vue. Jusqu'à présent, il n'a qu'un bouton et des icônes :)


Le projet est né comme une idée dans le cadre du Telegram Contest, dont l'essence était de développer une version web du client. Avec un collègue, nous avons décidé, pourquoi ne pas y participer. Les rôles ont été partagés afin d'avoir une mise en page, et lorsqu'un collègue comprend l'autorisation, je me connecte pour écrire des composants. Tout irait bien, mais se connecter au Telegram n'est pas si simple. En conséquence, nous n'avons rien envoyé, mais j'ai rattrapé un tas de tout et cela s'est avéré - en vain. Mais comme le dit Varlamov, votre projet vaut déjà quelque chose, puisque vous y avez consacré votre temps. Il est difficile d'être en désaccord avec cela, car si vous transférez en heures et en argent, la simple configuration de Webpack au tout début du projet n'est plus gratuite. En regardant toute cette honte, j'ai décidé que je devais en quelque sorte la jeter sur l'open source. Quel bootstrap unique utiliser? Je veux mon propre cadre d'interface utilisateur pour mes autres projets.


Le bouton


Le bouton de l'interface est peut-être l'élément principal avec lequel l'utilisateur interagit avec l'application. Par conséquent, c'est l'un des premiers composants de tout framework / bibliothèque d'interface utilisateur.


Dans la conception de Telegram, il n'y a pas beaucoup de variations de boutons:



J'ai mis en évidence 3 principaux (par défaut, accent, primaire), rond avec une icône et vert. Il est encore semi transparent, mais laisse tomber. Pour l'essentiel, en développant l' interface utilisateur XMars, j'essaie de partir des besoins, je n'ai pas compris où le bouton transparent serait nécessaire.


L'utilisateur de la bibliothèque doit être à l'aise avec les classes CSS. Je ne suis pas un fan de systèmes de nommage tels que BEM. J'aime la façon dont Bootstrap nomme les classes. Mais je simplifierais un peu plus. Au lieu de .btn .btn-primary , juste .btn .primary . Et dans le cas du composant React, il ressemblera à ceci:


 <Button primary>Hey</Button> 

Le même bouton mais effet d'entraînement:


 <Button primary ripple>Hey</Button> 

HTML / CSS


La bibliothèque d'interface utilisateur ne doit être liée à aucun cadre d'interface utilisateur. À l'avenir, je prévois d'étendre la disposition sur les composants Vue . Commençons par le HTML / CSS simple.


Sous le capot du projet Tailwindcss , il s'agit d'un framework CSS d'abord utilitaire, c'est-à-dire un framework qui vous fournit des utilitaires, au lieu de composants à part entière.



En plus de Tailwindcss, PostCSS est utilisé pour les mixins , les variables et les styles imbriqués.


Plus en détail sur l'utilisation d'un tel framework et comment le projet est configuré, je le dirai dans un article séparé. À ce stade, il suffit d'avoir une boîte à outils aussi puissante et d'utiliser pleinement les composants.


La <button> a un certain nombre de styles par défaut que nous devons supprimer ou redéfinir.



Dans le cas de Tailwindcss, la balise button a ce style:



Tous inutiles par défaut supprimés. Vous pouvez sculpter ce que vous voulez sans craindre qu'une bordure par défaut ne tombe sur un état. Mais voici une mise en garde, le outline par défaut doit encore être cloué:



Un bouton dans l' interface utilisateur de XMars a une classe .btn :


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

Ajoutez cette classe à nos styles:


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

Outre le fait que Tailwindcss fournit des classes que vous pouvez utiliser, il fournit une sorte de mixins . @apply n'est pas SCSS ou une sorte de plugin pour PostCSS. C'est la syntaxe de Tailwindcss elle-même. Les styles appliqués sont généralement sémantiquement clairs du nom. py-3 et px-4 seuls peuvent poser des questions. Le premier est le remplissage le long de y, c'est-à-dire verticalement, à savoir le padding-top: 0.75rem; padding-bottom: 0.75rem; . Par conséquent, le px-4 horizontalement est padding-right: 1rem; , padding-left: 1rem; .


Le design que Telegram a fourni pour le dire légèrement est mal documenté et des choses comme border-radius boutons de border-radius doivent être prises avec une règle directement à partir de l'image. Vous êtes-vous déjà demandé ce que signifient exactement les valeurs dans border-radius ?



C'est littéralement le rayon du cercle résultant dans le coin. Si la ferme collective, vous pouvez changer la règle comme indiqué dans l'image ci-dessus. J'ai donc utilisé la sélection rectangulaire dans Gimp.



border-radius boutons dans la conception est de 10 pixels, malheureusement il n'y a pas une telle classe dans la boîte de Tailwindcss, mais j'avais visuellement rounded-lg qui est de 8 8px pour la taille de police par défaut (rem).


Voici ce qui s'est passé en ce moment, j'ai peint le bouton en gris pour que les bords soient visibles:



Ensuite, nous devons faire un effet sur :hover . Ensuite, les concepteurs de Telegram ont décidé de faire la lumière et ont indiqué la couleur à 0,08% par rapport à #707579 . Je vois deux options, prenez simplement la couleur avec la pipette ou faites-le comme indiqué. La première option est plus simple, mais à l'avenir ce n'est pas la meilleure. Le fait est que si l'arrière-plan est différent du blanc, alors :hover nous obtiendrons une couleur spécifique, perdrons la «légèreté» et la transparence du bouton. Pour cela il vaut mieux suivre la documentation et poser alpha mâle la chaîne. Cela peut être fait de nombreuses manières, par exemple en utilisant les fonctions de couleur SCSS. Mais il n'y a pas de SCSS dans le projet, et à cause de la même couleur, je ne veux pas connecter de plug-in à PostCSS, nous rendrons tout très simple. Dans Chrome, il existe un colopaker qui vous permet de transformer les couleurs en différents systèmes, de piloter les couleurs HEX #707579 , de les traduire en rgba et de définir le canal alpha - 0,08%.



Voila! Quelque chose a brusquement flashé ma photo:



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



(: survoler)


Plus ennuyeux et sans trop d'effort, j'ai ajouté le reste de l'état:


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

Composant React


Initialement, le bouton était composé pour le concours Telegram et il était impossible d'utiliser un cadre. J'ai dû implémenter un effet d'entraînement sur du JS pur. J'aimerais vraiment que cela reste ainsi, mais pendant que vous faites le projet seul, vous devez sacrifier quelque chose.


Les composants qui nécessitent une sorte de logique, comme l'effet d'entraînement, seront implémentés et disponibles uniquement en tant que composants React.


Envelopper le bouton dans le composant React n'est pas difficile:


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

Ce bouton sera affiché dans le style spécifié, mais en fait il est de peu d'utilité. Nous devons permettre à l'utilisateur de personnaliser le bouton, d'ajouter des styles personnalisés, de bloquer les gestionnaires d'événements, etc.


Pour que l'utilisateur puisse transférer tout ce qui est nécessaire, vous devez d'abord surmonter Typescript, sinon même onClick ne vous permettra pas de transférer normalement. ButtonProps modifiant légèrement l'interface ButtonProps , nous résolvons le problème:


 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> 

après quoi nous pouvons faire la destruction des props toute sécurité:


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

Une utilisation similaire du bouton se comportera comme prévu:


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


Ensuite, nous ajoutons les styles de bouton et la possibilité d'enregistrer une classe personnalisée (tout à coup, quelqu'un aura besoin). Le package npm classnames est parfait à ces fins.


 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 classe btn est toujours définie, mais primary et accent uniquement si true . Les noms de classe ajoutent une classe si elle a une true valeur logique, en utilisant l'abréviation ES6, nous obtenons une simple entrée { primary } , au lieu de { primary: true } .


additionalClass est une chaîne, et si elle est vide ou non définie, elle ne joue pas un rôle spécial pour nous, rien ne sera ajouté à l'élément.


Au début, j'ai attribué les props comme suit:


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

Omettre tout ce qui ne s'applique pas aux props de l'élément bouton, mais ce n'est pas nécessaire car React ne rendra pas trop.


Effet d'entraînement



En fait, c'est à cela qu'il ressemble, mais il est souhaitable que la «vague» diverge de l'endroit du clic.
Il existe d'innombrables façons de faire une telle animation, c'est comme une blague sur le carré bleu .


Mais Google, en regardant les exemples sur codepen, il est devenu clair que dans la plupart des cas, une "vague" est réalisée à travers un élément enfant, qui se dilate et disparaît.


Il est positionné à l'intérieur du bouton en fonction des coordonnées du clic. Dans XMars UI , pour le moment, j'ai décidé de ne pas implémenter cet effet sur onPress comme le fait l'interface utilisateur Material, mais je prévois de l' onPress à l'avenir. Jusqu'à présent, uniquement onClick .



L'image ci-dessus est toute magique. Un clic crée un élément de bouton enfant, il est positionné absolument, au centre du clic et se développe. La propriété overflow: hidden onde de dépasser le bouton. L'élément doit être supprimé à la fin de l'animation.


Tout d'abord, nous allons définir les styles où nous pouvons, en utilisant autant que possible Tailwindcss:


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

L'élément responsable de l'effet se verra attribuer la classe .ripple . border-radius: 50%; est égal à un cercle (50% de congé à un angle * 2), le bouton a un positionnement relatif, le bouton .ripple a un positionnement absolu. L'animation est très simple, l'augmentation de l'onde devient transparente en 0,6 seconde. La couleur d'arrière-plan est la même que :hover et pliage, deux couleurs et boutons transparents «vague» nous donnent le résultat souhaité. Sur le bouton bleu .primary , ce n'est pas si important et là vous pouvez utiliser une couleur non transparente.


Au clic, vous devez créer l'élément "vague". Par conséquent, nous créons un état pour cette entreprise et ajoutons le gestionnaire de clics approprié au bouton, mais de telle manière qu'il n'interfère pas avec le onClick personnalisé.


 ... 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 - un tableau d'éléments JSX, la fonction de rendu ici peut sembler redondante, mais c'est plus une question de style et d'incorporation pour l'avenir.


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

onRippleClick qui crée des "vagues". En cliquant sur le bouton, nous découvrons la taille du bouton qui est utilisé pour positionner correctement le cercle, après quoi tout le nécessaire est passé à la fonction newRippleElement qui à son tour crée simplement un élément div avec la classe d' ripple , créant les styles nécessaires pour le positionnement.


Parmi les principales choses qui méritent d'être soulignées onAnimationEnd . Nous avons besoin de cet événement pour effacer le DOM des éléments déjà utilisés.


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

Il est très important de ne pas oublier, de passer les rippleElements actuels en arguments, sinon vous pouvez obtenir un tableau avec des anciennes valeurs, et tout ne fonctionnera pas comme prévu.


Code de bouton complet:


 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; 

Le résultat final peut être rapporté ici.


Conclusion


Beaucoup a été omis, par exemple, comment le projet a été mis en place, comment la documentation est écrite, les tests pour un nouveau composant dans le projet. J'essaierai de couvrir ces sujets avec des publications séparées.


Référentiel Github XMars UI

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


All Articles