From 931f3c98ec1b6a7cf04249598ea3f7555c6ffb01 Mon Sep 17 00:00:00 2001 From: Iveta Date: Thu, 22 Feb 2024 15:53:08 -0500 Subject: [PATCH] Improved network dropdown functionality --- src/components/NetworkSelector/index.tsx | 173 ++++++++++++++++----- src/components/NetworkSelector/styles.scss | 12 +- 2 files changed, 140 insertions(+), 45 deletions(-) diff --git a/src/components/NetworkSelector/index.tsx b/src/components/NetworkSelector/index.tsx index 4253fed2..b9415086 100644 --- a/src/components/NetworkSelector/index.tsx +++ b/src/components/NetworkSelector/index.tsx @@ -1,4 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { Button, Icon, Input } from "@stellar/design-system"; import { NetworkIndicator } from "@/components/NetworkIndicator"; @@ -8,7 +14,6 @@ import { Network, NetworkType } from "@/types/types"; import "./styles.scss"; -// TODO: update dropdown open and close actions // TODO: update input const NetworkOptions: Network[] = [ @@ -42,16 +47,51 @@ export const NetworkSelector = () => { const { network, selectNetwork } = useStore(); const [activeNetworkId, setActiveNetworkId] = useState(network.id); - const [isVisible, setIsVisible] = useState(false); - - const isCustomNetwork = activeNetworkId === "custom"; + const [isDropdownActive, setIsDropdownActive] = useState(false); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); const initialCustomState = { - url: isCustomNetwork ? network.url : "", - passphrase: isCustomNetwork ? network.passphrase : "", + url: network.id === "custom" ? network.url : "", + passphrase: network.id === "custom" ? network.passphrase : "", }; const [customNetwork, setCustomNetwork] = useState(initialCustomState); + const buttonRef = useRef(null); + const dropdownRef = useRef(null); + + const isSameNetwork = () => { + if (activeNetworkId === "custom") { + return ( + network.url && + network.passphrase && + customNetwork.url === network.url && + customNetwork.passphrase === network.passphrase + ); + } + + return activeNetworkId === network.id; + }; + + const isNetworkUrlInvalid = () => { + if (activeNetworkId !== "custom" || !customNetwork.url) { + return ""; + } + + try { + new URL(customNetwork.url); + return ""; + } catch (e) { + return "Value is not a valid URL"; + } + }; + + const isSubmitDisabled = + isSameNetwork() || + (activeNetworkId === "custom" && + !(customNetwork.url && customNetwork.passphrase)) || + Boolean(customNetwork.url && isNetworkUrlInvalid()); + + const isCustomNetwork = activeNetworkId === "custom"; const setNetwork = useCallback(() => { if (!network?.id) { @@ -70,7 +110,58 @@ export const NetworkSelector = () => { setNetwork(); }, [setNetwork]); - const handleSelectNetwork = (event: React.FormEvent) => { + const handleKeyPress = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Escape") { + toggleDropdown(false); + } + + if (event.key === "Enter" && !isSubmitDisabled) { + handleSelectNetwork(event); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [isSubmitDisabled], + ); + + const handleClickOutside = useCallback( + (event: MouseEvent) => { + if ( + dropdownRef?.current?.contains(event.target as Node) || + buttonRef?.current?.contains(event.target as Node) + ) { + return; + } + + toggleDropdown(false); + setActiveNetworkId(network.id); + setCustomNetwork({ + url: network.url ?? "", + passphrase: network.passphrase ?? "", + }); + }, + [network.id, network.passphrase, network.url], + ); + + // Close dropdown when clicked outside + useLayoutEffect(() => { + if (isDropdownVisible) { + document.addEventListener("pointerup", handleClickOutside); + document.addEventListener("keyup", handleKeyPress); + } else { + document.removeEventListener("pointerup", handleClickOutside); + document.removeEventListener("keyup", handleKeyPress); + } + + return () => { + document.removeEventListener("pointerup", handleClickOutside); + document.removeEventListener("keyup", handleKeyPress); + }; + }, [isDropdownVisible, handleClickOutside, handleKeyPress]); + + const handleSelectNetwork = ( + event: React.FormEvent | KeyboardEvent, + ) => { event.preventDefault(); const networkData = getNetworkById(activeNetworkId); @@ -82,8 +173,11 @@ export const NetworkSelector = () => { : networkData; selectNetwork(data); - setCustomNetwork(initialCustomState); + setCustomNetwork( + networkData.id === "custom" ? customNetwork : initialCustomState, + ); localStorageSavedNetwork.set(data); + toggleDropdown(false); } }; @@ -96,29 +190,29 @@ export const NetworkSelector = () => { return NetworkOptions.find((op) => op.id === networkId); }; - const isSameNetwork = () => { + const getButtonLabel = () => { if (activeNetworkId === "custom") { - return ( - network.url && - network.passphrase && - customNetwork.url === network.url && - customNetwork.passphrase === network.passphrase - ); + return "Switch to Custom Network"; } - return activeNetworkId === network.id; + return `Switch to ${getNetworkById(activeNetworkId)?.label}`; }; - const isNetworkUrlInvalid = () => { - if (activeNetworkId !== "custom" || !customNetwork.url) { - return ""; - } + const toggleDropdown = (show: boolean) => { + const delay = 100; - try { - new URL(customNetwork.url); - return ""; - } catch (e) { - return "Value is not a valid URL"; + if (show) { + setIsDropdownActive(true); + const t = setTimeout(() => { + setIsDropdownVisible(true); + clearTimeout(t); + }, delay); + } else { + setIsDropdownVisible(false); + const t = setTimeout(() => { + setIsDropdownActive(false); + clearTimeout(t); + }, delay); } }; @@ -126,14 +220,19 @@ export const NetworkSelector = () => {
@@ -144,6 +243,7 @@ export const NetworkSelector = () => { data-is-active={op.id === activeNetworkId} role="button" onClick={() => handleSelectActive(op.id)} + tabIndex={0} >
{op.url}
@@ -151,10 +251,7 @@ export const NetworkSelector = () => { ))}
-
+
{ setCustomNetwork({ ...customNetwork, url: e.target.value }) } error={isNetworkUrlInvalid()} + tabIndex={0} /> { passphrase: e.target.value, }) } + tabIndex={0} />
diff --git a/src/components/NetworkSelector/styles.scss b/src/components/NetworkSelector/styles.scss index 67b11775..5e7f07e7 100644 --- a/src/components/NetworkSelector/styles.scss +++ b/src/components/NetworkSelector/styles.scss @@ -54,9 +54,14 @@ left: auto; bottom: auto; transform: none; + display: none; + opacity: 0; - &[data-is-visible="true"] { + &[data-is-active="true"] { display: block; + } + + &[data-is-visible="true"] { opacity: 1; } } @@ -108,11 +113,6 @@ background-color: transparent; border-radius: pxToRem(4px); transition: background-color var(--sds-anim-transition-default); - margin-top: pxToRem(-4px); - - &[data-is-active="true"] { - background-color: var(--sds-clr-gray-03); - } form { display: flex;