Skip to content

Commit

Permalink
refactor(components): token input (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao authored Sep 29, 2023
1 parent 85d810a commit 0609525
Show file tree
Hide file tree
Showing 22 changed files with 753 additions and 354 deletions.
6 changes: 6 additions & 0 deletions .changeset/spicy-jars-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@interlay/ui": patch
"@interlay/hooks": patch
---

refactor(components): token input
11 changes: 4 additions & 7 deletions packages/components/src/Input/BaseInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ValidationState } from '@react-types/shared';
import { FocusEvent, forwardRef, InputHTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react';
import { forwardRef, InputHTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react';
import { Sizes, Spacing } from '@interlay/theme';

import { Field, FieldProps, useFieldProps } from '../Field';
Expand All @@ -20,10 +19,8 @@ type Props = {
size?: Sizes;
// TODO: temporary
padding?: { top?: Spacing; bottom?: Spacing; left?: Spacing; right?: Spacing };
validationState?: ValidationState;
isInvalid?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFocus?: (e: FocusEvent<Element>) => void;
onBlur?: (e: FocusEvent<Element>) => void;
};

type NativeAttrs = Omit<InputHTMLAttributes<HTMLInputElement>, keyof Props>;
Expand All @@ -37,7 +34,7 @@ type BaseInputProps = Props & NativeAttrs & InheritAttrs;

const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
(
{ startAdornment, endAdornment, bottomAdornment, disabled, size = 'medium', validationState, padding, ...props },
{ startAdornment, endAdornment, bottomAdornment, disabled, size = 'medium', isInvalid, padding, ...props },
ref
): JSX.Element => {
const endAdornmentRef = useRef<HTMLDivElement>(null);
Expand All @@ -51,7 +48,7 @@ const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
setEndAdornmentWidth(endAdornmentRef.current.getBoundingClientRect().width);
}, [endAdornment]);

const error = hasError({ validationState, errorMessage: props.errorMessage });
const error = hasError({ isInvalid, errorMessage: props.errorMessage });

return (
<Field {...fieldProps}>
Expand Down
5 changes: 2 additions & 3 deletions packages/components/src/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ type AriaAttrs = Omit<AriaTextFieldOptions<'input'>, (keyof Props & InheritAttrs

type InputProps = Props & InheritAttrs & AriaAttrs;

const Input = forwardRef<HTMLInputElement, InputProps>(({ onChange, validationState, ...props }, ref): JSX.Element => {
const Input = forwardRef<HTMLInputElement, InputProps>(({ onChange, isInvalid, ...props }, ref): JSX.Element => {
const inputRef = useDOMRef(ref);
// We are specifing `validationState` so that when there are errors, `aria-invalid` is set to `true`
const { inputProps, descriptionProps, errorMessageProps, labelProps } = useTextField(
{ ...props, validationState: validationState || props.errorMessage ? 'invalid' : validationState },
{ ...props, isInvalid: isInvalid || !!props.errorMessage },
inputRef
);

Expand Down
14 changes: 10 additions & 4 deletions packages/components/src/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AriaTextFieldOptions, useTextField } from '@react-aria/textfield';
import { mergeProps } from '@react-aria/utils';
import { ChangeEventHandler, forwardRef, useEffect, useState } from 'react';
import { ChangeEventHandler, FocusEvent, forwardRef, useEffect, useState } from 'react';
import { useDOMRef } from '@interlay/hooks';

import { BaseInput, BaseInputProps } from '../Input';
Expand All @@ -18,6 +18,8 @@ const decimalRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`);
type Props = {
value?: string | number;
defaultValue?: string | number;
onFocus?: (e: FocusEvent<Element>) => void;
onBlur?: (e: FocusEvent<Element>) => void;
};

type InheritAttrs = Omit<
Expand All @@ -29,9 +31,10 @@ type AriaAttrs = Omit<AriaTextFieldOptions<'input'>, keyof (Props & InheritAttrs

type NumberInputProps = Props & InheritAttrs & AriaAttrs;

// FIXME: some event are running duplicate
const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
(
{ onChange, validationState, value: valueProp, defaultValue = '', inputMode = 'numeric', ...props },
{ onChange, value: valueProp, defaultValue = '', inputMode = 'numeric', isDisabled, onFocus, onBlur, ...props },
ref
): JSX.Element => {
const [value, setValue] = useState<string | undefined>(defaultValue?.toString());
Expand Down Expand Up @@ -63,10 +66,13 @@ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
const { inputProps, descriptionProps, errorMessageProps, labelProps } = useTextField(
{
...props,
isDisabled,
inputMode,
validationState: props.errorMessage ? 'invalid' : validationState,
isInvalid: !!props.errorMessage,
value: value,
autoComplete: 'off'
autoComplete: 'off',
onFocus,
onBlur
},
inputRef
);
Expand Down
8 changes: 4 additions & 4 deletions packages/components/src/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type SelectProps<F extends SelectType = 'listbox', T = SelectObject> = Props<F,
NativeAttrs<F, T> &
InheritAttrs<F, T>;

// TODO: when type is modal, we should use also types from our List
// FIXME: when type is modal, we should use also types from our List and improve this components types
const Select = <F extends SelectType = 'listbox', T extends SelectObject = SelectObject>(
{
value,
Expand All @@ -61,7 +61,7 @@ const Select = <F extends SelectType = 'listbox', T extends SelectObject = Selec
placeholder = 'Select an option',
asSelectTrigger,
modalTitle,
validationState,
isInvalid,
onChange,
renderValue = (item) => item.rendered,
items,
Expand All @@ -84,7 +84,7 @@ const Select = <F extends SelectType = 'listbox', T extends SelectObject = Selec
defaultSelectedKey: defaultValue as Key,
label,
errorMessage,
validationState,
isInvalid,
items,
children,
...props
Expand All @@ -109,7 +109,7 @@ const Select = <F extends SelectType = 'listbox', T extends SelectObject = Selec
})
);

const error = hasError({ errorMessage, validationState });
const error = hasError({ errorMessage, isInvalid });

const selectTriggerProps =
type === 'listbox'
Expand Down
51 changes: 30 additions & 21 deletions packages/components/src/Select/SelectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useId } from '@react-aria/utils';
import { SelectState } from '@react-stately/select';
import { forwardRef, ReactNode } from 'react';

import { Modal, ModalBody, ModalHeader, ModalProps } from '..';
import { Modal, ModalBody, ModalHeader, ModalProps, P } from '..';
import { ListItem, ListProps } from '../List';

import { StyledList } from './Select.style';
Expand Down Expand Up @@ -33,32 +33,41 @@ const SelectModal = forwardRef<HTMLDivElement, SelectModalProps>(
onClose();
};

const items = [...state.collection];
const hasItems = !!items.length;

return (
<SelectModalContext.Provider value={{ selectedItem: state.selectedItem }}>
<Modal ref={ref} hasMaxHeight onClose={onClose} {...props}>
<ModalHeader color='secondary' id={headerId} size='lg' weight='medium'>
{title}
</ModalHeader>
<ModalBody noPadding overflow='hidden'>
<StyledList
{...listProps}
aria-labelledby={headerId}
selectionMode='single'
variant='secondary'
onSelectionChange={handleSelectionChange}
>
{[...state.collection].map((item) => (
<ListItem
key={item.key}
alignItems='center'
gap='spacing2'
justifyContent='space-between'
textValue={item.textValue}
>
{item.rendered}
</ListItem>
))}
</StyledList>
<ModalBody noPadding={hasItems} overflow='hidden'>
{hasItems ? (
<StyledList
{...listProps}
aria-labelledby={headerId}
selectionMode='single'
variant='secondary'
onSelectionChange={handleSelectionChange}
>
{[...state.collection].map((item) => (
<ListItem
key={item.key}
alignItems='center'
gap='spacing2'
justifyContent='space-between'
textValue={item.textValue}
>
{item.rendered}
</ListItem>
))}
</StyledList>
) : (
<P align='center' color='tertiary'>
No options
</P>
)}
</ModalBody>
</Modal>
</SelectModalContext.Provider>
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/Select/SelectTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useDOMRef } from '@interlay/hooks';
import { Sizes } from '@interlay/theme';
import { useButton } from '@react-aria/button';
import { PressEvent } from '@react-types/shared';
import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
import { Sizes } from '@interlay/theme';
import { useDOMRef } from '@interlay/hooks';

import { TextProps } from '../Text';

Expand Down
77 changes: 77 additions & 0 deletions packages/components/src/TokenInput/BaseTokenInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useLabel } from '@react-aria/label';
import { mergeProps } from '@react-aria/utils';
import { forwardRef, ReactNode } from 'react';

import { Flex } from '../Flex';
import { NumberInput, NumberInputProps } from '../NumberInput';
import { formatUSD } from '../utils/format';

import { TokenInputLabel } from './TokenInputLabel';
import { StyledUSDAdornment } from './TokenInput.style';

type Props = {
valueUSD?: number;
balance?: ReactNode;
};

type InheritAttrs = Omit<NumberInputProps, keyof Props>;

type BaseTokenInputProps = Props & InheritAttrs;

const BaseTokenInput = forwardRef<HTMLInputElement, BaseTokenInputProps>(
(
{
label,
style,
hidden,
className,
placeholder = '0',
errorMessage,
description,
balance,
children,
valueUSD,
isDisabled,
...props
},
ref
): JSX.Element => {
const { labelProps, fieldProps } = useLabel({ label, ...props });

const hasLabel = !!label || !!balance;

const bottomAdornment = valueUSD !== undefined && (
<StyledUSDAdornment $isDisabled={isDisabled}>{formatUSD(valueUSD, { compact: true })}</StyledUSDAdornment>
);

return (
<Flex className={className} direction='column' gap='spacing0' hidden={hidden} style={style}>
{hasLabel && (
<TokenInputLabel {...labelProps} balance={balance}>
{label}
</TokenInputLabel>
)}
<NumberInput
ref={ref}
bottomAdornment={bottomAdornment}
description={description}
errorMessage={errorMessage}
inputMode='decimal'
isDisabled={isDisabled}
maxLength={79}
minLength={1}
pattern='^[0-9]*[.,]?[0-9]*$'
placeholder={placeholder}
size='large'
{...mergeProps(props, fieldProps)}
/>
{children}
</Flex>
);
}
);

BaseTokenInput.displayName = 'BaseTokenInput';

export { BaseTokenInput };
export type { BaseTokenInputProps };
51 changes: 51 additions & 0 deletions packages/components/src/TokenInput/FixedTokenInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ReactNode, forwardRef } from 'react';

import { TokenAdornment, TokenTicker } from './TokenAdornment';
import { BaseTokenInput, BaseTokenInputProps } from './BaseTokenInput';
import { TokenInputBalance } from './TokenInputBalance';

type Props = {
balance?: string | number;
humanBalance?: string | number;
balanceLabel?: ReactNode;
onClickBalance?: (balance: string | number) => void;
ticker: TokenTicker;
};

type InheritAttrs = Omit<BaseTokenInputProps, keyof Props>;

type FixedTokenInputProps = Props & InheritAttrs;

const FixedTokenInput = forwardRef<HTMLInputElement, FixedTokenInputProps>(
(
{ balance: balanceProp, humanBalance, balanceLabel, onClickBalance, ticker: tickerProp, isDisabled, id, ...props },
ref
): JSX.Element => {
const balance = balanceProp !== undefined && (
<TokenInputBalance
balance={balanceProp}
balanceHuman={humanBalance}
inputId={id}
isDisabled={isDisabled}
label={balanceLabel}
onClickBalance={onClickBalance}
/>
);

return (
<BaseTokenInput
ref={ref}
balance={balance}
endAdornment={<TokenAdornment ticker={tickerProp} />}
id={id}
isDisabled={isDisabled}
{...props}
/>
);
}
);

FixedTokenInput.displayName = 'FixedTokenInput';

export { FixedTokenInput };
export type { FixedTokenInputProps };
Loading

0 comments on commit 0609525

Please sign in to comment.