NymConnect - Add button animations (#2950)

* add button animations

* pulse and disable button on connecting/disconnecting status

* update button component story

* disabled hover on connecting/disconnecting

* add transition delay

fix up overflow
This commit is contained in:
Fouad
2023-02-02 16:42:11 +00:00
committed by Jon Häggblad
parent 3ec0fcb0a4
commit d7deb9819b
6 changed files with 204 additions and 143 deletions
-132
View File
@@ -1,132 +0,0 @@
import React from 'react';
import { ConnectionStatusKind } from 'src/types';
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
if (isError && hover) {
return '#21D072';
}
if (isError) {
return '#40475C';
}
switch (status) {
case 'disconnected':
if (hover) {
return '#FFF';
}
return '#BBB';
case 'connecting':
return '#FFF';
case 'disconnecting':
return '#FFF';
default:
// connected
if (hover) {
return '#E43E3E';
}
return '#21D072';
}
};
export const PowerButton: FCWithChildren<{
onClick?: (status: ConnectionStatusKind) => void;
isError?: boolean;
disabled?: boolean;
status: ConnectionStatusKind;
busy?: boolean;
}> = ({ onClick, disabled, status, isError }) => {
const [hover, setHover] = React.useState<boolean>(false);
const handleClick = () => {
if (disabled === true) {
return;
}
if (onClick) {
onClick(status);
}
};
const statusFillColor = getStatusFillColor(status, hover, Boolean(isError));
return (
<svg
width="190"
height="190"
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={handleClick}
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
onMouseEnter={() => !disabled && setHover(true)}
onMouseLeave={() => !disabled && setHover(false)}
>
<g transform="translate(-30, -25) ">
<circle cx={131} cy={131} r={70} strokeWidth={2} stroke={statusFillColor} filter="url(#blur)" opacity="0.6" />
<circle cx={131} cy={131} r={22} strokeWidth={1} stroke={statusFillColor} filter="url(#blur)" opacity="0.3" />
<circle opacity={0.6} cx={131} cy={131} r={68.5} stroke={statusFillColor} />
<g filter="url(#filter1_d_944_9033)">
<circle cx={131} cy={131} r={64} fill="url(#paint1_radial_944_9033)" />
<circle cx={131} cy={131} r={63} stroke={statusFillColor} strokeWidth={2} />
</g>
<g opacity={0.5} filter="url(#filter2_f_944_9033)">
<g clipPath="url(#clip0_944_9033)">
<path
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
stroke={statusFillColor}
/>
</g>
</g>
<g clipPath="url(#clip1_944_9033)">
<path
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
fill={statusFillColor}
/>
</g>
<defs>
<filter
id="filter0_f_944_9033"
x={0}
y={0}
width={240}
height={240}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation={40} result="effect1_foregroundBlur_944_9033" />
</filter>
<filter
id="filter1_d_944_9033"
x={52}
y={58}
width={158}
height={158}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_944_9033" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_944_9033" result="shape" />
</filter>
<filter
id="filter2_f_944_9033"
x={97}
y={97}
width={68}
height={68}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation={5} result="effect1_foregroundBlur_944_9033" />
</filter>
<filter id="blur">
<feGaussianBlur stdDeviation="5" />
</filter>
</defs>
</g>
</svg>
);
};
@@ -0,0 +1,123 @@
import React, { useCallback } from 'react';
import { ConnectionStatusKind } from 'src/types';
import './power-button.css';
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
if (isError && hover) {
return '#21D072';
}
if (isError) {
return '#40475C';
}
switch (status) {
case 'disconnected':
if (hover) {
return '#FFF';
}
return '#BBB';
case 'connecting':
return '#FFF';
case 'disconnecting':
return '#FFF';
default:
// connected
if (hover) {
return '#E43E3E';
}
return '#21D072';
}
};
export const PowerButton: FCWithChildren<{
onClick?: (status: ConnectionStatusKind) => void;
isError?: boolean;
disabled?: boolean;
status: ConnectionStatusKind;
busy?: boolean;
}> = ({ onClick, disabled, status, isError }) => {
const [hover, setHover] = React.useState<boolean>(false);
const handleClick = () => {
if (disabled === true) {
return;
}
if (onClick) {
onClick(status);
}
};
const statusFillColor = getStatusFillColor(status, hover, Boolean(isError));
const getClassName = useCallback(() => {
if (hover) {
switch (status) {
case 'disconnected':
return 'expand';
default:
return 'contract';
}
}
if (!hover) {
switch (status) {
case 'connected':
return 'expand';
default:
return 'contract';
}
}
}, [status, hover]);
const buttonPulse = () => {
if (status === 'connecting' || status === 'disconnecting') return 'pulse';
return undefined;
};
return (
<svg
width="190"
height="190"
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={handleClick}
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
onMouseEnter={() => !disabled && setHover(true)}
onMouseLeave={() => !disabled && setHover(false)}
>
<g transform="translate(-30, -25) ">
<circle cx={131} cy={131} r={75} strokeWidth={4} stroke={statusFillColor} filter="url(#blur)" opacity="0.6" />
<circle cx={131} cy={131} r={25} strokeWidth={2} stroke={statusFillColor} filter="url(#blur)" opacity="0.5" />
<g id="Button power">
<circle cx="131" cy="131" r="68.5" stroke={statusFillColor} stroke-width="0.5" />
<circle id="ring-one" className={getClassName()} cx="131" cy="131" r="73" stroke={statusFillColor} />
<circle id="ring-two" className={getClassName()} cx="131" cy="131" r="77" stroke={statusFillColor} />
<circle id="ring-three" className={getClassName()} cx="131" cy="131" r="81" stroke={statusFillColor} />
<circle id="ring-four" className={getClassName()} cx="131" cy="131" r="85" stroke={statusFillColor} />
<g id="button bg">
<circle cx="131" cy="131" r="63" stroke={statusFillColor} stroke-width="3" className={buttonPulse()} />
</g>
<g id="Power icon">
<g id="Icon">
<g id="Group 672_2">
<g id="power_settings_new_black_24dp (1) 1_2" clip-path="url(#clip1_944_8739)">
<path
id="Vector_2"
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
fill={statusFillColor}
className={buttonPulse()}
/>
</g>
</g>
</g>
</g>
</g>
<defs>
<filter id="blur" width="200%" height="200%" x="-50%" y="-50%">
<feGaussianBlur stdDeviation="12.5" />
</filter>
</defs>
</g>
</svg>
);
};
@@ -0,0 +1,72 @@
#ring-expand {
animation-name: rings-expand;
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
animation-play-state: paused;
}
#ring-one.expand {
opacity: 0.3;
transition: opacity 0.1s ease-in-out;
}
#ring-two.expand {
opacity: 0.2;
transition: opacity 0.2s ease-in-out;
}
#ring-three.expand {
opacity: 0.1;
transition: opacity 0.3s ease-in-out;
}
#ring-four.expand {
opacity: 0.05;
transition: opacity 0.4s ease-in-out;
}
#ring-one.contract {
opacity: 0;
transition: opacity 0.4s;
transition-delay: 0.1s;
}
#ring-two.contract {
opacity: 0;
transition: opacity 0.3s;
transition-delay: 0.1s;
}
#ring-three.contract {
opacity: 0;
transition: opacity 0.1s;
transition-delay: 0.1s;
}
#ring-four.contract {
opacity: 0;
transition: opacity 0.1s;
transition-delay: 0.1s;
}
circle,
path {
transition: stroke 0.5s, fill 0.5s;
}
.pulse {
animation-name: pulse;
animation-duration: 0.5s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes pulse {
from {
opacity: 1;
}
to {
opacity: 0.3;
}
}
@@ -10,7 +10,7 @@ import { IpAddressAndPort } from 'src/components/IpAddressAndPort';
import { ServiceProvider } from 'src/types/directory';
import { ExperimentalWarning } from 'src/components/ExperimentalWarning';
import { ConnectionLayout } from 'src/layouts/ConnectionLayout';
import { PowerButton } from 'src/components/PowerButton';
import { PowerButton } from 'src/components/PowerButton/PowerButton';
export const Connected: FCWithChildren<{
status: ConnectionStatusKind;
@@ -58,7 +58,7 @@ export const Connected: FCWithChildren<{
busy={busy}
onClick={onConnectClick}
isError={isError}
disabled={status === 'connecting' || status === 'disconnecting'}
disabled={status === 'disconnecting'}
/>
}
BottomContent={
@@ -2,14 +2,12 @@ import React from 'react';
import { Stack, Typography } from '@mui/material';
import { ConnectionStatus } from 'src/components/ConnectionStatus';
import { ConnectionTimer } from 'src/components/ConntectionTimer';
import { useClientContext } from 'src/context/main';
import { InfoModal } from 'src/components/InfoModal';
import { Error } from 'src/types/error';
import { ExperimentalWarning } from 'src/components/ExperimentalWarning';
import { ServiceProvider, Services } from 'src/types/directory';
import { ConnectionStatusKind } from 'src/types';
import { ConnectionButton } from 'src/components/ConnectionButton';
import { PowerButton } from 'src/components/PowerButton';
import { PowerButton } from 'src/components/PowerButton/PowerButton';
import { Box } from '@mui/system';
import { ConnectionLayout } from 'src/layouts/ConnectionLayout';
@@ -22,7 +20,7 @@ export const Disconnected: FCWithChildren<{
serviceProvider?: ServiceProvider;
clearError: () => void;
onConnectClick: (status: ConnectionStatusKind) => void;
}> = ({ status, error, onConnectClick, clearError, serviceProvider }) => {
}> = ({ status, error, onConnectClick, clearError }) => {
return (
<>
{error && <InfoModal show title={error.title} description={error.message} onClose={clearError} />}
@@ -33,7 +31,7 @@ export const Disconnected: FCWithChildren<{
<ConnectionTimer />
</Box>
}
ConnectButton={<PowerButton onClick={onConnectClick} status={status} disabled={false} />}
ConnectButton={<PowerButton onClick={onConnectClick} status={status} disabled={status === 'connecting'} />}
BottomContent={
<Stack justifyContent="space-between" pt={1}>
<Typography
@@ -1,6 +1,6 @@
import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { PowerButton } from 'src/components/PowerButton';
import { PowerButton } from 'src/components/PowerButton/PowerButton';
import { ConnectionStatusKind } from 'src/types';
export default {
@@ -13,7 +13,7 @@ export const Disconnected: ComponentStory<typeof PowerButton> = () => (
);
export const Connecting: ComponentStory<typeof PowerButton> = () => (
<PowerButton status={ConnectionStatusKind.connecting} />
<PowerButton status={ConnectionStatusKind.connecting} disabled />
);
export const Connected: ComponentStory<typeof PowerButton> = () => (
@@ -21,9 +21,9 @@ export const Connected: ComponentStory<typeof PowerButton> = () => (
);
export const Disconnecting: ComponentStory<typeof PowerButton> = () => (
<PowerButton status={ConnectionStatusKind.disconnecting} />
<PowerButton status={ConnectionStatusKind.disconnecting} disabled />
);
export const Disabled: ComponentStory<typeof PowerButton> = () => (
<PowerButton status={ConnectionStatusKind.connecting} disabled />
<PowerButton status={ConnectionStatusKind.disconnected} disabled />
);