From 85d810af98c72454845d62eee7438e2afa19a4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Thu, 21 Sep 2023 12:02:06 +0100 Subject: [PATCH] feat(hooks): add use-form hook and changes to related components (#14) --- .changeset/violet-frogs-travel.md | 6 + .eslintignore | 1 - jest.config.js | 2 +- package.json | 7 +- packages/components/src/Input/BaseInput.tsx | 6 +- packages/components/src/Input/Input.style.tsx | 1 + packages/components/src/List/List.tsx | 7 +- .../src/NumberInput/NumberInput.tsx | 2 +- .../components/src/Overlay/OpenTransition.tsx | 6 +- .../components/src/Select/Select.stories.tsx | 3 +- packages/components/src/Select/Select.tsx | 4 +- packages/components/src/Table/Table.tsx | 4 +- .../components/src/TokenInput/TokenInput.tsx | 52 ++- .../src/TokenInput/TokenInputBalance.tsx | 4 +- .../src/TokenInput/TokenInputLabel.tsx | 3 + packages/hooks/package.json | 5 +- packages/hooks/src/index.ts | 1 + .../src/use-form/__tests__/use-form.test.tsx | 355 +++++++++++++++++ packages/hooks/src/use-form/index.tsx | 2 + packages/hooks/src/use-form/use-form.tsx | 161 ++++++++ pnpm-lock.yaml | 367 +++++++++++++----- scripts/setup-test.ts | 2 +- tsconfig.json | 5 +- 23 files changed, 883 insertions(+), 123 deletions(-) create mode 100644 .changeset/violet-frogs-travel.md create mode 100644 packages/hooks/src/use-form/__tests__/use-form.test.tsx create mode 100644 packages/hooks/src/use-form/index.tsx create mode 100644 packages/hooks/src/use-form/use-form.tsx diff --git a/.changeset/violet-frogs-travel.md b/.changeset/violet-frogs-travel.md new file mode 100644 index 000000000..fef9cbe45 --- /dev/null +++ b/.changeset/violet-frogs-travel.md @@ -0,0 +1,6 @@ +--- +"@interlay/ui": patch +"@interlay/hooks": patch +--- + +feat(hooks): add use-form hook and changes to related components diff --git a/.eslintignore b/.eslintignore index 9f2c1facb..199129c4b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,6 @@ *.css .changeset dist -scripts/* *.config.js .DS_Store node_modules diff --git a/jest.config.js b/jest.config.js index 4d704b7ed..4d8d94366 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,7 +18,7 @@ module.exports = { ] }, transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], - setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', './scripts/setup-test.ts'], + setupFilesAfterEnv: ['./scripts/setup-test.ts'], testTimeout: 10000, globals: { 'ts-jest': { diff --git a/package.json b/package.json index ef0be3493..fb1769f0b 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,10 @@ "@storybook/react-vite": "^7.4.0", "@swc/core": "^1.3.84", "@swc/jest": "^0.2.29", - "@testing-library/dom": "^8.1.0", - "@testing-library/jest-dom": "^5.16.4", + "@testing-library/dom": "^9.3.3", + "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", - "@testing-library/react-hooks": "^8.0.1", - "@testing-library/user-event": "^14.4.3", + "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.4", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", diff --git a/packages/components/src/Input/BaseInput.tsx b/packages/components/src/Input/BaseInput.tsx index efc41f3b6..dc674fd0b 100644 --- a/packages/components/src/Input/BaseInput.tsx +++ b/packages/components/src/Input/BaseInput.tsx @@ -1,5 +1,5 @@ import { ValidationState } from '@react-types/shared'; -import { forwardRef, InputHTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react'; +import { FocusEvent, forwardRef, InputHTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react'; import { Sizes, Spacing } from '@interlay/theme'; import { Field, FieldProps, useFieldProps } from '../Field'; @@ -22,6 +22,8 @@ type Props = { padding?: { top?: Spacing; bottom?: Spacing; left?: Spacing; right?: Spacing }; validationState?: ValidationState; onChange?: (e: React.ChangeEvent) => void; + onFocus?: (e: FocusEvent) => void; + onBlur?: (e: FocusEvent) => void; }; type NativeAttrs = Omit, keyof Props>; @@ -29,7 +31,7 @@ type NativeAttrs = Omit, keyof Props>; type InheritAttrs = Omit< HelperTextProps & Pick, - keyof Props & NativeAttrs + keyof (Props & NativeAttrs) >; type BaseInputProps = Props & NativeAttrs & InheritAttrs; diff --git a/packages/components/src/Input/Input.style.tsx b/packages/components/src/Input/Input.style.tsx index 63a618d28..2f9b558ac 100644 --- a/packages/components/src/Input/Input.style.tsx +++ b/packages/components/src/Input/Input.style.tsx @@ -32,6 +32,7 @@ const StyledBaseInput = styled.input` $adornments.bottom ? theme.input.overflow.large.text : theme.input[$size].text}; line-height: ${theme.lineHeight.base}; font-weight: ${({ $size }) => theme.input[$size].weight}; + text-overflow: ellipsis; background-color: ${({ $isDisabled }) => ($isDisabled ? theme.input.disabled.bg : theme.input.background)}; overflow: hidden; diff --git a/packages/components/src/List/List.tsx b/packages/components/src/List/List.tsx index 062f6dc45..e7882e9b8 100644 --- a/packages/components/src/List/List.tsx +++ b/packages/components/src/List/List.tsx @@ -25,8 +25,11 @@ type NativeAttrs = Omit; type ListProps = Props & NativeAttrs & InheritAttrs; const List = forwardRef( - ({ variant = 'primary', direction = 'column', onSelectionChange, ...props }, ref): JSX.Element => { - const ariaProps = { onSelectionChange, ...props }; + ( + { variant = 'primary', direction = 'column', onSelectionChange, selectionMode, selectedKeys, ...props }, + ref + ): JSX.Element => { + const ariaProps = { onSelectionChange, selectionMode, selectedKeys, ...props }; const state = useListState(ariaProps); const listRef = useDOMRef(ref); const { gridProps } = useGridList(ariaProps, state, listRef); diff --git a/packages/components/src/NumberInput/NumberInput.tsx b/packages/components/src/NumberInput/NumberInput.tsx index 9b1948519..d68149fa4 100644 --- a/packages/components/src/NumberInput/NumberInput.tsx +++ b/packages/components/src/NumberInput/NumberInput.tsx @@ -25,7 +25,7 @@ type InheritAttrs = Omit< keyof Props | 'errorMessageProps' | 'descriptionProps' | 'disabled' | 'required' | 'readOnly' >; -type AriaAttrs = Omit, (keyof Props & InheritAttrs) | 'onChange'>; +type AriaAttrs = Omit, keyof (Props & InheritAttrs)>; type NumberInputProps = Props & InheritAttrs & AriaAttrs; diff --git a/packages/components/src/Overlay/OpenTransition.tsx b/packages/components/src/Overlay/OpenTransition.tsx index f77564c1d..969e98df2 100644 --- a/packages/components/src/Overlay/OpenTransition.tsx +++ b/packages/components/src/Overlay/OpenTransition.tsx @@ -20,9 +20,9 @@ type OpenTransitionProps = Props & InheritAttrs; const OpenTransition = (props: OpenTransitionProps): any => { // Do not apply any transition if in chromatic (based on react-spectrum) - // if (process.env.CHROMATIC) { - // return Children.map(props.children, (child) => child && cloneElement(child as any, { isOpen: props.in })); - // } + if (process.env.NODE_ENV === 'test') { + return Children.map(props.children, (child) => child && cloneElement(child as any, { isOpen: props.in })); + } return ( diff --git a/packages/components/src/Select/Select.stories.tsx b/packages/components/src/Select/Select.stories.tsx index 5a8c2a9e6..e48754156 100644 --- a/packages/components/src/Select/Select.stories.tsx +++ b/packages/components/src/Select/Select.stories.tsx @@ -35,8 +35,7 @@ export default { layout: 'centered' }, args: { - label: 'Coin', - withModal: false + label: 'Coin' }, render: Render } as Meta; diff --git a/packages/components/src/Select/Select.tsx b/packages/components/src/Select/Select.tsx index 37ab84cb0..2123c1ce2 100644 --- a/packages/components/src/Select/Select.tsx +++ b/packages/components/src/Select/Select.tsx @@ -129,7 +129,7 @@ const Select = @@ -153,7 +153,7 @@ const Select = ( {/* TODO: rename `uid` to `id` */} {(column) => {column.name}} - {(item: any) => {(columnKey) => {item[columnKey]}}} + + {(item: any) => {(columnKey) => {item[columnKey.toString()]}}} + ) ); diff --git a/packages/components/src/TokenInput/TokenInput.tsx b/packages/components/src/TokenInput/TokenInput.tsx index d25722c41..3c3d3b5f7 100644 --- a/packages/components/src/TokenInput/TokenInput.tsx +++ b/packages/components/src/TokenInput/TokenInput.tsx @@ -1,13 +1,12 @@ import { useLabel } from '@react-aria/label'; import { chain, mergeProps, useId } from '@react-aria/utils'; -import { forwardRef, Key, ReactNode, useEffect, useState } from 'react'; +import { ChangeEvent, FocusEvent, forwardRef, Key, ReactNode, useEffect, useState } from 'react'; import { useDOMRef } from '@interlay/hooks'; import { Flex } from '../Flex'; import { HelperText } from '../HelperText'; import { NumberInput, NumberInputProps } from '../NumberInput'; import { formatUSD } from '../utils/format'; -import { triggerChangeEvent } from '../utils/input'; import { TokenAdornment, TokenTicker } from './TokenAdornment'; import { StyledUSDAdornment } from './TokenInput.style'; @@ -22,6 +21,7 @@ type Props = { ticker?: TokenTicker; onClickBalance?: (balance?: string | number) => void; onChangeTicker?: (ticker?: string) => void; + onValueChange?: (value?: string | number) => void; selectProps?: Omit; }; @@ -43,17 +43,22 @@ const TokenInput = forwardRef( hidden, className, onClickBalance, + onValueChange, onChangeTicker, selectProps, placeholder = '0', errorMessage, description, + value: valueProp, + defaultValue, + onBlur, ...props }, ref ): JSX.Element => { const inputRef = useDOMRef(ref); + const [value, setValue] = useState(defaultValue); const [ticker, setTicker] = useState( (selectProps?.defaultValue as string) || (typeof tickerProp === 'string' ? tickerProp : tickerProp?.text) || '' ); @@ -61,6 +66,7 @@ const TokenInput = forwardRef( const { labelProps, fieldProps } = useLabel({ label, ...props }); const selectHelperTextId = useId(); + const inputId = useId(); const itemsArr = Array.from(selectProps?.items || []); const isSelectAdornment = itemsArr.length > 1; @@ -72,11 +78,19 @@ const TokenInput = forwardRef( setTicker(selectProps.value as string); }, [selectProps?.value]); + useEffect(() => { + if (valueProp === undefined) return; + + setValue(valueProp); + }, [valueProp]); + const handleClickBalance = () => { if (!balance) return; - triggerChangeEvent(inputRef, balance); + inputRef.current?.focus(); onClickBalance?.(balance); + setValue(balance); + onValueChange?.(balance); }; const handleTokenChange = (ticker: Key) => { @@ -84,6 +98,25 @@ const TokenInput = forwardRef( setTicker(ticker as string); }; + const handleChange = (e: ChangeEvent) => { + const value = e.target.value; + + onValueChange?.(value); + setValue(value); + }; + + const handleBlur = (e: FocusEvent) => { + const relatedTargetEl = e.relatedTarget as HTMLButtonElement; + + if (!relatedTargetEl) return; + + const isTargetingMaxBtn = relatedTargetEl.getAttribute('aria-controls') === inputId; + + if (!isTargetingMaxBtn) { + onBlur?.(e); + } + }; + // Prioritise Number Input description and error message const hasSelectHelperText = !errorMessage && !description && (selectProps?.errorMessage || selectProps?.description); @@ -105,6 +138,15 @@ const TokenInput = forwardRef( ) : null; const hasLabel = !!label || balance !== undefined; + const hasLabelWithBalance = hasLabel && balance !== undefined; + + const numberInputProps: Partial = hasLabelWithBalance + ? { + id: inputId, + onBlur: handleBlur, + onChange: handleChange + } + : { onBlur, onChange: handleChange }; return (