Skip to content

Commit

Permalink
Fix cursor stealing & display loading for AddressInput (#738)
Browse files Browse the repository at this point in the history
  • Loading branch information
technophile-04 committed Feb 25, 2024
1 parent 16e6508 commit a482feb
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 8 deletions.
41 changes: 34 additions & 7 deletions packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { blo } from "blo";
import { useDebounce } from "usehooks-ts";
import { useDebounceValue } from "usehooks-ts";
import { Address, isAddress } from "viem";
import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi";
import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth";
Expand All @@ -11,29 +11,39 @@ import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth";
export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps<Address | string>) => {
// Debounce the input to keep clean RPC calls when resolving ENS names
// If the input is an address, we don't need to debounce it
const _debouncedValue = useDebounce(value, 500);
const [_debouncedValue] = useDebounceValue(value, 500);
const debouncedValue = isAddress(value) ? value : _debouncedValue;
const isDebouncedValueLive = debouncedValue === value;

// If the user changes the input after an ENS name is already resolved, we want to remove the stale result
const settledValue = isDebouncedValueLive ? debouncedValue : undefined;

const { data: ensAddress, isLoading: isEnsAddressLoading } = useEnsAddress({
const {
data: ensAddress,
isLoading: isEnsAddressLoading,
isError: isEnsAddressError,
isSuccess: isEnsAddressSuccess,
} = useEnsAddress({
name: settledValue,
enabled: isENS(debouncedValue),
enabled: isDebouncedValueLive && isENS(debouncedValue),
chainId: 1,
cacheTime: 30_000,
});

const [enteredEnsName, setEnteredEnsName] = useState<string>();
const { data: ensName, isLoading: isEnsNameLoading } = useEnsName({
const {
data: ensName,
isLoading: isEnsNameLoading,
isError: isEnsNameError,
isSuccess: isEnsNameSuccess,
} = useEnsName({
address: settledValue as Address,
enabled: isAddress(debouncedValue),
chainId: 1,
cacheTime: 30_000,
});

const { data: ensAvatar } = useEnsAvatar({
const { data: ensAvatar, isLoading: isEnsAvtarLoading } = useEnsAvatar({
name: ensName,
enabled: Boolean(ensName),
chainId: 1,
Expand All @@ -57,6 +67,14 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C
[onChange],
);

const reFocus =
isEnsAddressError ||
isEnsNameError ||
isEnsNameSuccess ||
isEnsAddressSuccess ||
ensName === null ||
ensAddress === null;

return (
<InputBase<Address>
name={name}
Expand All @@ -65,9 +83,11 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C
value={value as Address}
onChange={handleChange}
disabled={isEnsAddressLoading || isEnsNameLoading || disabled}
reFocus={reFocus}
prefix={
ensName && (
ensName ? (
<div className="flex bg-base-300 rounded-l-full items-center">
{isEnsAvtarLoading && <div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>}
{ensAvatar ? (
<span className="w-[35px]">
{
Expand All @@ -78,6 +98,13 @@ export const AddressInput = ({ value, name, placeholder, onChange, disabled }: C
) : null}
<span className="text-accent px-2">{enteredEnsName ?? ensName}</span>
</div>
) : (
(isEnsNameLoading || isEnsAddressLoading) && (
<div className="flex bg-base-300 rounded-l-full items-center gap-2 pr-2">
<div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>
<div className="skeleton bg-base-200 h-3 w-20"></div>
</div>
)
)
}
suffix={
Expand Down
19 changes: 18 additions & 1 deletion packages/nextjs/components/scaffold-eth/Input/InputBase.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ChangeEvent, ReactNode, useCallback } from "react";
import { ChangeEvent, FocusEvent, ReactNode, useCallback, useEffect, useRef } from "react";
import { CommonInputProps } from "~~/components/scaffold-eth";

type InputBaseProps<T> = CommonInputProps<T> & {
error?: boolean;
prefix?: ReactNode;
suffix?: ReactNode;
reFocus?: boolean;
};

export const InputBase = <T extends { toString: () => string } | undefined = string>({
Expand All @@ -16,7 +17,10 @@ export const InputBase = <T extends { toString: () => string } | undefined = str
disabled,
prefix,
suffix,
reFocus,
}: InputBaseProps<T>) => {
const inputReft = useRef<HTMLInputElement>(null);

let modifier = "";
if (error) {
modifier = "border-error";
Expand All @@ -31,6 +35,17 @@ export const InputBase = <T extends { toString: () => string } | undefined = str
[onChange],
);

// Runs only when reFocus prop is passed, usefull for setting the cursor
// at the end of the input. Example AddressInput
const onFocus = (e: FocusEvent<HTMLInputElement, Element>) => {
if (reFocus !== undefined) {
e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length);
}
};
useEffect(() => {
if (reFocus !== undefined && reFocus === true) inputReft.current?.focus();
}, [reFocus]);

return (
<div className={`flex border-2 border-base-300 bg-base-200 rounded-full text-accent ${modifier}`}>
{prefix}
Expand All @@ -42,6 +57,8 @@ export const InputBase = <T extends { toString: () => string } | undefined = str
onChange={handleChange}
disabled={disabled}
autoComplete="off"
ref={inputReft}
onFocus={onFocus}
/>
{suffix}
</div>
Expand Down

0 comments on commit a482feb

Please sign in to comment.