Skip to content

Commit

Permalink
Improved network dropdown functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
quietbits committed Feb 22, 2024
1 parent e900f87 commit 931f3c9
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 45 deletions.
173 changes: 134 additions & 39 deletions src/components/NetworkSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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[] = [
Expand Down Expand Up @@ -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<HTMLButtonElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(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) {
Expand All @@ -70,7 +110,58 @@ export const NetworkSelector = () => {
setNetwork();
}, [setNetwork]);

const handleSelectNetwork = (event: React.FormEvent<HTMLFormElement>) => {
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<HTMLFormElement> | KeyboardEvent,
) => {
event.preventDefault();

const networkData = getNetworkById(activeNetworkId);
Expand All @@ -82,8 +173,11 @@ export const NetworkSelector = () => {
: networkData;

selectNetwork(data);
setCustomNetwork(initialCustomState);
setCustomNetwork(
networkData.id === "custom" ? customNetwork : initialCustomState,
);
localStorageSavedNetwork.set(data);
toggleDropdown(false);
}
};

Expand All @@ -96,44 +190,49 @@ 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);
}
};

return (
<div className="NetworkSelector">
<button
className="NetworkSelector__button"
onClick={() => setIsVisible(!isVisible)}
ref={buttonRef}
onClick={() => toggleDropdown(!isDropdownVisible)}
tabIndex={0}
>
<NetworkIndicator networkId={network.id} networkLabel={network.label} />
<Icon.ChevronDown />
</button>
<div
className="NetworkSelector__floater Floater__content Floater__content--light"
data-is-visible={isVisible}
data-is-active={isDropdownActive}
data-is-visible={isDropdownVisible}
ref={dropdownRef}
tabIndex={0}
>
<div className="NetworkSelector__body">
<div className="NetworkSelector__body__links">
Expand All @@ -144,17 +243,15 @@ export const NetworkSelector = () => {
data-is-active={op.id === activeNetworkId}
role="button"
onClick={() => handleSelectActive(op.id)}
tabIndex={0}
>
<NetworkIndicator networkId={op.id} networkLabel={op.label} />
<div className="NetworkSelector__body__link__url">{op.url}</div>
</div>
))}
</div>

<div
className="NetworkSelector__body__inputs"
data-is-active={activeNetworkId === "custom"}
>
<div className="NetworkSelector__body__inputs">
<form onSubmit={handleSelectNetwork}>
<Input
id="network-url"
Expand All @@ -170,6 +267,7 @@ export const NetworkSelector = () => {
setCustomNetwork({ ...customNetwork, url: e.target.value })
}
error={isNetworkUrlInvalid()}
tabIndex={0}
/>
<Input
id="network-passphrase"
Expand All @@ -187,19 +285,16 @@ export const NetworkSelector = () => {
passphrase: e.target.value,
})
}
tabIndex={0}
/>
<Button
size="sm"
variant="secondary"
type="submit"
disabled={
isSameNetwork() ||
(activeNetworkId === "custom" &&
!(customNetwork.url && customNetwork.passphrase)) ||
Boolean(customNetwork.url && isNetworkUrlInvalid())
}
disabled={isSubmitDisabled}
tabIndex={0}
>
{`Use ${getNetworkById(activeNetworkId)?.label} Network`}
{getButtonLabel()}
</Button>
</form>
</div>
Expand Down
12 changes: 6 additions & 6 deletions src/components/NetworkSelector/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 931f3c9

Please sign in to comment.