diff --git a/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx b/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx index 05395f36d..06a0f700d 100644 --- a/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx +++ b/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx @@ -27,6 +27,10 @@ export const PlaygroundTestConnectButton = () => {
+

Connected (loading)

+
+ +

Interactive (active: {active ? 'true' : 'false'})

setActive((v) => !v)} /> diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx index 9d5dfbf65..34e0a3edb 100644 --- a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx +++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx @@ -1,8 +1,12 @@ +import { motion } from 'motion/react'; +import { useEffect, useRef, useState } from 'react'; +import { motionTransitionStandard } from '../../../../consts'; import { TooltipContent } from '../../../../providers/tooltip/TooltipContent'; import { TooltipProvider } from '../../../../providers/tooltip/TooltipContext'; import { TooltipTrigger } from '../../../../providers/tooltip/TooltipTrigger'; import { isPresent } from '../../../../utils/isPresent'; import { Icon, type IconKindValue } from '../../../Icon'; +import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; import './style.scss'; import clsx from 'clsx'; @@ -10,6 +14,7 @@ interface Props { active: boolean; onClick: () => void; disabled?: boolean; + loading?: boolean; icon?: IconKindValue | null; text?: string | null; tooltip?: string | null; @@ -22,8 +27,35 @@ export const ConnectButton = ({ text, tooltip, disabled = false, + loading = false, }: Props) => { const label = isPresent(text) ? text : active ? 'Disconnect' : 'Connect VPN'; + const isLoading = loading && !disabled; + const [swapDirection, setSwapDirection] = useState<'to-loading' | 'to-content' | null>( + null, + ); + const previousLoadingRef = useRef(isLoading); + + useEffect(() => { + if (previousLoadingRef.current !== isLoading) { + setSwapDirection(isLoading ? 'to-loading' : 'to-content'); + previousLoadingRef.current = isLoading; + } + }, [isLoading]); + + const contentTransition = { + ...motionTransitionStandard, + delay: + !isLoading && swapDirection === 'to-content' + ? motionTransitionStandard.duration + : 0, + }; + + const loaderTransition = { + ...motionTransitionStandard, + delay: + isLoading && swapDirection === 'to-loading' ? motionTransitionStandard.duration : 0, + }; const button = ( // aria-disabled instead of disabled, a disabled button gets no hover to open the tooltip @@ -34,14 +66,32 @@ export const ConnectButton = ({ disconnected: !active, icon: isPresent(icon), disabled, + loading: isLoading, })} - aria-disabled={disabled} + aria-disabled={disabled || loading} onClick={() => { - if (!disabled) onClick(); + if (!disabled && !loading) onClick(); }} > - {isPresent(icon) && } -

{label}

+ + {isPresent(icon) && } +

{label}

+
+ + + ); diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss b/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss index 915045edc..a78b1955e 100644 --- a/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss +++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss @@ -3,6 +3,7 @@ --shadow: 0 4px 5px 0 rgb(53 84 179 / 7%); --color: var(--fg-action); --border: transparent; + --loader-bg: transparent; background: var(--bg); border: 1px solid var(--border); @@ -13,6 +14,7 @@ transition-timing-function: ease-in-out; cursor: pointer; display: inline-flex; + position: relative; width: 100%; align-items: center; justify-content: center; @@ -21,19 +23,31 @@ box-sizing: border-box; padding: 0 var(--spacing-lg); - &:not(.connected, .disabled):hover { + &:not(.connected, .disabled, .loading):hover { --bg: var(--bg-white-80); } + &:not(.disabled).loading { + --bg: var(--c-white-10); + --loader-bg: var(--bg-white-10); + + cursor: default; + } + &.connected { --bg: transparent; --color: var(--fg-white-100); --border: var(--border-default); - &:not(.disabled):hover { + &:not(.disabled, .loading):hover { --border: var(--border-default); --bg: var(--bg-white-10); } + + &:not(.disabled).loading { + --bg: transparent; + --border: var(--border-disabled); + } } &.disabled { @@ -44,14 +58,35 @@ cursor: not-allowed; } - .icon { - --icon-color: var(--color); + & > .content { + display: inline-flex; + align-items: center; + justify-content: center; + color: inherit; - margin-right: var(--spacing-sm); + & > .icon { + --icon-color: var(--color); + + margin-right: var(--spacing-sm); + } + + & > p { + font: var(--t-body-sm-600); + color: inherit; + } } - p { - font: var(--t-body-sm-600); - color: inherit; + & > .loader-overlay { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border-radius: inherit; + background-color: var(--loader-bg); + pointer-events: none; } } diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx index 05fd85e10..df71004f7 100644 --- a/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx +++ b/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx @@ -56,7 +56,7 @@ export const LocationCardConnectButton = () => { }, }); - const isBusy = isConnecting || isDisconnecting || isOpeningConfiguration; + const isBusy = isConnecting || isOpeningConfiguration; const handleClick = () => { if (location.active) { @@ -89,6 +89,7 @@ export const LocationCardConnectButton = () => { active={location.active} onClick={handleClick} disabled={isBusy || isBlocked} + loading={isDisconnecting} tooltip={isBlocked ? connectBlockedTooltip : null} /> ); diff --git a/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx b/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx index 65e7ff496..4561959ef 100644 --- a/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx +++ b/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx @@ -71,7 +71,7 @@ export const OverviewLocationCard = ({ location, instance }: Props) => { const { mutate: configureMfa, isPending: isOpeningConfiguration } = useConfigureFactorsScreen(); - const isBusy = isConnecting || isDisconnecting || isOpeningConfiguration; + const isBusy = isConnecting || isOpeningConfiguration; const connectionAbility = useConnectionAbility(location, instance); @@ -143,6 +143,7 @@ export const OverviewLocationCard = ({ location, instance }: Props) => { active={location.active} onClick={handleConnectClick} disabled={isBusy || isBlocked} + loading={isDisconnecting} tooltip={isBlocked ? connectBlockedTooltip : null} />