From 592299eb5ca1dbbeec9d8db666de005320892e45 Mon Sep 17 00:00:00 2001 From: cheton Date: Thu, 31 Aug 2023 15:55:39 +0800 Subject: [PATCH 1/2] feat(react): add `useRunAfterUpdate` Hook to `react/src/utils` --- packages/react/src/utils/useRunAfterUpdate.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/react/src/utils/useRunAfterUpdate.js diff --git a/packages/react/src/utils/useRunAfterUpdate.js b/packages/react/src/utils/useRunAfterUpdate.js new file mode 100644 index 0000000000..32cddcb8e1 --- /dev/null +++ b/packages/react/src/utils/useRunAfterUpdate.js @@ -0,0 +1,18 @@ +import { useRef, useLayoutEffect } from 'react'; + +const useRunAfterUpdate = () => { + const afterPaintRef = useRef(null); + + useLayoutEffect(() => { + if (afterPaintRef.current) { + afterPaintRef.current?.(); + afterPaintRef.current = null; + } + }); + + return (callback) => { + afterPaintRef.current = callback; + }; +}; + +export default useRunAfterUpdate; From 977b96e0ce21f8c9b4619c2d86072694621bbf37 Mon Sep 17 00:00:00 2001 From: cheton Date: Thu, 31 Aug 2023 15:57:45 +0800 Subject: [PATCH 2/2] feat: utilize the `selectionStart` and `selectionEnd` properties to preserve and restore the cursor position of `SearchInput` --- packages/react/src/search-input/SearchInput.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/react/src/search-input/SearchInput.js b/packages/react/src/search-input/SearchInput.js index ac24ca742b..a61071e06f 100644 --- a/packages/react/src/search-input/SearchInput.js +++ b/packages/react/src/search-input/SearchInput.js @@ -2,6 +2,7 @@ import { useMergeRefs } from '@tonic-ui/react-hooks'; import { ensureString } from 'ensure-type'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { InputControl } from '../input'; +import useRunAfterUpdate from '../utils/useRunAfterUpdate'; import SearchInputAdornment from './SearchInputAdornment'; import SearchInputClearButton from './SearchInputClearButton'; import SearchInputLoadingIcon from './SearchInputLoadingIcon'; @@ -31,13 +32,22 @@ const SearchInput = React.forwardRef(( const combinedRef = useMergeRefs(nodeRef, ref); const [value, setValue] = useState(ensureString(valueProp ?? defaultValueProp)); const isClearable = !disabled && !readOnly && !!value; + const inputSelectionRef = useRef(); + const runAfterUpdate = useRunAfterUpdate(); useEffect(() => { const isControlled = (valueProp !== undefined); if (isControlled) { setValue(valueProp); } - }, [valueProp]); + + runAfterUpdate(() => { + if (inputSelectionRef.current && nodeRef.current) { + nodeRef.current.selectionStart = inputSelectionRef.current.start; + nodeRef.current.selectionEnd = inputSelectionRef.current.end; + } + }); + }, [valueProp, runAfterUpdate]); const iconState = (() => { if (isLoading) { @@ -73,10 +83,16 @@ const SearchInput = React.forwardRef(( const onChange = useCallback((e) => { const nextValue = ensureString(e.target.value ?? ''); const isControlled = (valueProp !== undefined); + if (!isControlled) { setValue(nextValue); } + inputSelectionRef.current = { + start: e.target.selectionStart, + end: e.target.selectionEnd, + }; + if (typeof onChangeProp === 'function') { onChangeProp(e); }