From 8952d45c2ec43244a3c73b3475fb3d14f1f1e6a6 Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Thu, 26 May 2022 14:35:10 -0700 Subject: [PATCH] Have `InputControl` favor entered value for a render cycle --- .../components/src/input-control/index.tsx | 10 +++- .../components/src/input-control/utils.ts | 56 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/components/src/input-control/index.tsx b/packages/components/src/input-control/index.tsx index 6ff965f9ab131..c3f4de5396a02 100644 --- a/packages/components/src/input-control/index.tsx +++ b/packages/components/src/input-control/index.tsx @@ -17,6 +17,7 @@ import { useState, forwardRef } from '@wordpress/element'; import InputBase from './input-base'; import InputField from './input-field'; import type { InputControlProps } from './types'; +import { useDraft } from './utils'; function useUniqueId( idProp?: string ) { const instanceId = useInstanceId( InputControl ); @@ -52,6 +53,12 @@ export function UnforwardedInputControl( const id = useUniqueId( idProp ); const classes = classNames( 'components-input-control', className ); + const draftHookProps = useDraft( { + value, + onBlur: props.onBlur, + onChange, + } ); + return ( ); diff --git a/packages/components/src/input-control/utils.ts b/packages/components/src/input-control/utils.ts index 3c755de9b7c17..b051cb8b4dd5e 100644 --- a/packages/components/src/input-control/utils.ts +++ b/packages/components/src/input-control/utils.ts @@ -1,7 +1,22 @@ +/** + * External dependencies + */ +import type { FocusEventHandler } from 'react'; + /** * WordPress dependencies */ -import { useEffect } from '@wordpress/element'; +import { + useEffect, + useLayoutEffect, + useRef, + useState, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { InputChangeCallback } from './types'; /** * Gets a CSS cursor value based on a drag direction. @@ -52,3 +67,42 @@ export function useDragCursor( return dragCursor; } + +export function useDraft( props: { + value: string | undefined; + onBlur?: FocusEventHandler; + onChange: InputChangeCallback; +} ) { + const refPreviousValue = useRef( props.value ); + const [ draft, setDraft ] = useState< { + value?: string; + isStale?: boolean; + } >( {} ); + const value = draft.value !== undefined ? draft.value : props.value; + + // Determines when to discard the draft value to restore controlled status. + // To do so, it tracks the previous value and marks the draft value as stale + // after each render. + useLayoutEffect( () => { + const { current: previousValue } = refPreviousValue; + refPreviousValue.current = props.value; + if ( draft.value !== undefined && ! draft.isStale ) + setDraft( { ...draft, isStale: true } ); + else if ( draft.isStale && props.value !== previousValue ) + setDraft( {} ); + }, [ props.value, draft ] ); + + const onChange: InputChangeCallback = ( nextValue, extra ) => { + // Mutates the draft value to avoid an extra effect run. + setDraft( ( current ) => + Object.assign( current, { value: nextValue, isStale: false } ) + ); + props.onChange( nextValue, extra ); + }; + const onBlur: FocusEventHandler = ( event ) => { + setDraft( {} ); + props.onBlur?.( event ); + }; + + return { value, onBlur, onChange }; +}