From 5324571c778a95d58f38f0c2d9a7b7218436b7a6 Mon Sep 17 00:00:00 2001 From: Allan Oliveira Date: Wed, 24 Apr 2024 09:22:56 -0300 Subject: [PATCH] feat: up auto focus --- .vscode/settings.json | 2 +- .../Fields/AsyncSelect/AsyncSelect.tsx | 18 +++++ .../molecules/Fields/Field/Field.tsx | 17 +++++ .../molecules/Fields/Select/Select.tsx | 18 +++++ src/contexts/AutoFocusContext.tsx | 39 +++++++++++ src/contexts/FieldNextFocusManagerContext.tsx | 24 +++++++ src/hooks/index.ts | 1 + src/hooks/useRegisterFieldFocus.ts | 66 +++++++++++++++++++ src/main.ts | 1 + src/theme/providers.tsx | 23 ++++--- 10 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 src/contexts/AutoFocusContext.tsx create mode 100644 src/contexts/FieldNextFocusManagerContext.tsx create mode 100644 src/hooks/useRegisterFieldFocus.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 866c7fa..67e32c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.formatOnSave": true, "files.associations": { diff --git a/src/components/molecules/Fields/AsyncSelect/AsyncSelect.tsx b/src/components/molecules/Fields/AsyncSelect/AsyncSelect.tsx index 1176747..4e3174f 100644 --- a/src/components/molecules/Fields/AsyncSelect/AsyncSelect.tsx +++ b/src/components/molecules/Fields/AsyncSelect/AsyncSelect.tsx @@ -10,6 +10,7 @@ import ReactSelectAsync from 'react-select/async' import { StyleModeContext } from 'contexts/StyleModeContext' import debounce from 'lodash/debounce' +import { useRegisterFieldFocus } from 'hooks/useRegisterFieldFocus' import { StyleModes, System } from 'types' import { slugify } from 'utils' @@ -66,6 +67,7 @@ const AsyncSelect = (props: AsyncSelectProps(props: AsyncSelectProps(props: AsyncSelectProps noOptionsMessage} isLoading={isLoading} + onKeyDown={e => { + onKeyDownAuto(e) + + if (typeof onKeyDown === 'function') { + onKeyDown(e as any) + } + }} /> ) } @@ -279,6 +290,13 @@ const AsyncSelect = (props: AsyncSelectProps noOptionsMessage} isLoading={isLoading} + onKeyDown={e => { + onKeyDownAuto(e) + + if (typeof onKeyDown === 'function') { + onKeyDown(e as any) + } + }} /> ) }} diff --git a/src/components/molecules/Fields/Field/Field.tsx b/src/components/molecules/Fields/Field/Field.tsx index d769185..c46caee 100644 --- a/src/components/molecules/Fields/Field/Field.tsx +++ b/src/components/molecules/Fields/Field/Field.tsx @@ -5,6 +5,7 @@ import { StyleModeContext } from 'contexts/StyleModeContext' import { Input, InputProps, Label, Span } from 'components/atoms' +import { useRegisterFieldFocus } from 'hooks/useRegisterFieldFocus' import { StyleModes } from 'types' import { @@ -99,6 +100,8 @@ const Field = forwardRef((props, ref) => { setIsFocused(false) } + const onKeyDown = useRegisterFieldFocus(name) + return ( ((props, ref) => { target.blur() } }} + onKeyDown={e => { + onKeyDown(e) + + if (typeof props.onKeyDown === 'function') { + props.onKeyDown(e) + } + }} /> ) : ( @@ -232,6 +242,13 @@ const Field = forwardRef((props, ref) => { target.blur() } }} + onKeyDown={e => { + onKeyDown(e) + + if (typeof props.onKeyDown === 'function') { + props.onKeyDown(e) + } + }} /> )} {type !== 'file' && (sufixElement || null)} diff --git a/src/components/molecules/Fields/Select/Select.tsx b/src/components/molecules/Fields/Select/Select.tsx index 73b0a79..5f0322a 100644 --- a/src/components/molecules/Fields/Select/Select.tsx +++ b/src/components/molecules/Fields/Select/Select.tsx @@ -9,6 +9,7 @@ import ReactSelect, { MultiValue, SingleValue } from 'react-select' import { StyleModeContext } from 'contexts/StyleModeContext' +import { useRegisterFieldFocus } from 'hooks/useRegisterFieldFocus' import { StyleModes, System } from 'types' import { slugify } from 'utils' @@ -72,6 +73,7 @@ const Select = (props: SelectProps) => { onChange: customOnChange, reactSelectProps, reactSelectStyles, + onKeyDown, ...rest } = props @@ -127,6 +129,8 @@ const Select = (props: SelectProps) => { [isMulti, reactSelectStyles, selectedColor, styleMode] ) + const onKeyDownAuto = useRegisterFieldFocus(name) + return ( (props: SelectProps) => { data-cy={resolvedId} isSearchable={Boolean(enableSearch)} noOptionsMessage={() => enableSearch?.noOptionsMessage} + onKeyDown={e => { + onKeyDownAuto(e) + + if (typeof onKeyDown === 'function') { + onKeyDown(e as any) + } + }} /> ) } @@ -225,6 +236,13 @@ const Select = (props: SelectProps) => { data-cy={resolvedId} isSearchable={Boolean(enableSearch)} noOptionsMessage={() => enableSearch?.noOptionsMessage} + onKeyDown={e => { + onKeyDownAuto(e) + + if (typeof onKeyDown === 'function') { + onKeyDown(e as any) + } + }} /> ) }} diff --git a/src/contexts/AutoFocusContext.tsx b/src/contexts/AutoFocusContext.tsx new file mode 100644 index 0000000..81a0c06 --- /dev/null +++ b/src/contexts/AutoFocusContext.tsx @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react' +import { FieldValues, UseFormSetFocus, UseFormTrigger } from 'react-hook-form' + +import { createContext } from '@nexpy/react-easy-context-api' + +import { WithChildren } from 'types' + +type AutoFocusContextValue = { + trigger: UseFormTrigger | null + setFocus: UseFormSetFocus | null +} + +export const AutoFocusContext = createContext({ + trigger: null, + setFocus: null, +} as AutoFocusContextValue) + +type AutoFocusProviderProps = WithChildren<{ + trigger: UseFormTrigger + setFocus: UseFormSetFocus +}> + +export const AutoFocus = ({ + children, + trigger, + setFocus, +}: AutoFocusProviderProps) => { + return ( + + {children} + + ) +} diff --git a/src/contexts/FieldNextFocusManagerContext.tsx b/src/contexts/FieldNextFocusManagerContext.tsx new file mode 100644 index 0000000..8b4abc9 --- /dev/null +++ b/src/contexts/FieldNextFocusManagerContext.tsx @@ -0,0 +1,24 @@ +import React, { useRef, MutableRefObject } from 'react' + +import { createContext } from '@nexpy/react-easy-context-api' + +import { WithChildren } from 'types' + +type FieldNextFocusManagerContextValue = { + sequentialFieldNamesRef: MutableRefObject +} + +export const FieldNextFocusManagerContext = + createContext( + {} as FieldNextFocusManagerContextValue + ) + +export const FieldNextFocusManagerProvider = ({ children }: WithChildren) => { + const sequentialFieldNamesRef = useRef([]) + + return ( + + {children} + + ) +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a09fd91..9ba6057 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useOnClickOutside' export * from './useMask' +export * from './useRegisterFieldFocus' diff --git a/src/hooks/useRegisterFieldFocus.ts b/src/hooks/useRegisterFieldFocus.ts new file mode 100644 index 0000000..84b913e --- /dev/null +++ b/src/hooks/useRegisterFieldFocus.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useCallback, useEffect } from 'react' + +import { AutoFocusContext } from 'contexts/AutoFocusContext' +import { FieldNextFocusManagerContext } from 'contexts/FieldNextFocusManagerContext' + +export const useRegisterFieldFocus = (fieldName?: string | undefined) => { + const sequentialFieldNamesRef = FieldNextFocusManagerContext.useSelector( + state => state.sequentialFieldNamesRef + ) + + const autoFocusContextValue = AutoFocusContext.useContext() + + if (fieldName && autoFocusContextValue?.setFocus) { + sequentialFieldNamesRef.current.push(fieldName) + } + + const onKeyDown = useCallback( + (e: any) => { + if ( + !autoFocusContextValue?.setFocus || + !Array.isArray(sequentialFieldNamesRef.current) + ) { + return + } + + if (e.key === 'Enter') { + e.preventDefault() + + const fieldIndex = sequentialFieldNamesRef.current.findIndex( + val => val === fieldName + ) + + if (typeof fieldIndex === 'number' && fieldIndex !== -1) { + const nextFieldNameIndex = fieldIndex + 1 + const nextFieldName = sequentialFieldNamesRef.current[nextFieldNameIndex] + + if (nextFieldName) { + autoFocusContextValue.trigger?.(fieldName).then(passed => { + if (passed) { + autoFocusContextValue.setFocus?.(nextFieldName) + } + }) + } + } + } + }, + [autoFocusContextValue, fieldName, sequentialFieldNamesRef] + ) + + useEffect(() => { + return () => { + if (!autoFocusContextValue?.setFocus) { + return + } + + if (Array.isArray(sequentialFieldNamesRef.current)) { + sequentialFieldNamesRef.current = sequentialFieldNamesRef.current.filter( + val => val !== fieldName + ) + } + } + }, [autoFocusContextValue?.setFocus, fieldName, sequentialFieldNamesRef]) + + return onKeyDown +} diff --git a/src/main.ts b/src/main.ts index cca7669..9102902 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,3 +3,4 @@ export * from './hooks' export * from './theme' export * from './types' export * from './utils' +export { AutoFocus } from 'contexts/AutoFocusContext' diff --git a/src/theme/providers.tsx b/src/theme/providers.tsx index 60a9825..a330109 100644 --- a/src/theme/providers.tsx +++ b/src/theme/providers.tsx @@ -1,6 +1,7 @@ import React, { useMemo, memo, PropsWithChildren } from 'react' import { ThemeProvider as BaseThemeProvider } from '@xstyled/styled-components' +import { FieldNextFocusManagerProvider } from 'contexts/FieldNextFocusManagerContext' import { StyleModeProvider } from 'contexts/StyleModeContext' import merge from 'lodash/merge' @@ -27,16 +28,18 @@ const ThemeProviderBase = ({ return ( - {renderWithAdaptativeRootRemRefCSS ? ( - - ) : ( - - )} - {children} - - {disablePortalContainer !== true ? ( -
- ) : null} + + {renderWithAdaptativeRootRemRefCSS ? ( + + ) : ( + + )} + {children} + + {disablePortalContainer !== true ? ( +
+ ) : null} + )