Skip to content

Commit da25f73

Browse files
pdclarksc0ttkclark
authored andcommitted
Repeatable Fields: Drag and Drop + Input Focus
useEffect value-sync approach in repeatable DateTime, Currency, Text, and Number fields have been replaced with useRef. A previous fix for drag-and-drop reorder (including value in the React key) caused repeatable text fields to lose focus on every keystroke. Removing the value from the key fixed focus but regressed drag-and-drop for date/currency/number fields that maintain internal state. This replaces both approaches with useRef for persistent field IDs which associate with items during reorder. React tracks component instances without remounting, so text fields keep focus and fields keep their order.
1 parent c4e2113 commit da25f73

File tree

9 files changed

+9691
-35
lines changed

9 files changed

+9691
-35
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"dependencies":["lodash","react","react-dom","wp-api-fetch","wp-autop","wp-block-editor","wp-blocks","wp-components","wp-compose","wp-date","wp-element","wp-i18n","wp-keycodes","wp-server-side-render","wp-url"],"version":"4748509d03f7df68d202"}
1+
{"dependencies":["lodash","react","react-dom","wp-api-fetch","wp-autop","wp-block-editor","wp-blocks","wp-components","wp-compose","wp-date","wp-element","wp-i18n","wp-keycodes","wp-server-side-render","wp-url"],"version":"d22b52bd9ea7ada14900"}

ui/js/blocks/pods-blocks-api.min.js

Lines changed: 2188 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/js/dfv/pods-dfv.min.asset.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"dependencies":["lodash","moment","react","react-dom","react-jsx-runtime","regenerator-runtime","wp-api-fetch","wp-autop","wp-components","wp-compose","wp-data","wp-element","wp-hooks","wp-i18n","wp-keycodes","wp-plugins","wp-primitives","wp-url"],"version":"5ddb11ac8c48641bcb2d"}
1+
{"dependencies":["lodash","moment","react","react-dom","react-jsx-runtime","regenerator-runtime","wp-api-fetch","wp-autop","wp-components","wp-compose","wp-data","wp-element","wp-hooks","wp-i18n","wp-keycodes","wp-plugins","wp-primitives","wp-url"],"version":"8ed9c3584aee2b81285b"}

ui/js/dfv/pods-dfv.min.js

Lines changed: 7467 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/js/dfv/src/components/field-wrapper/repeatable-field-list.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* External dependencies
33
*/
4-
import React from 'react';
4+
import React, { useRef } from 'react';
55
import PropTypes from 'prop-types';
66
import {
77
DndContext,
@@ -58,6 +58,22 @@ const RepeatableFieldList = ( {
5858
setFullValue,
5959
setHasBlurred,
6060
} ) => {
61+
// Stable unique IDs for React keys. These move with items during
62+
// drag-and-drop reorder so React correctly tracks component instances,
63+
// preventing both focus loss (text fields) and stale internal state
64+
// (datetime/currency/number fields).
65+
const nextIdRef = useRef( valuesArray.length );
66+
const itemIdsRef = useRef( valuesArray.map( ( _, i ) => i ) );
67+
68+
// Ensure IDs array stays in sync if valuesArray length changes externally.
69+
if ( itemIdsRef.current.length < valuesArray.length ) {
70+
while ( itemIdsRef.current.length < valuesArray.length ) {
71+
itemIdsRef.current.push( nextIdRef.current++ );
72+
}
73+
} else if ( itemIdsRef.current.length > valuesArray.length ) {
74+
itemIdsRef.current = itemIdsRef.current.slice( 0, valuesArray.length );
75+
}
76+
6177
// Helper functions for setting, moving, adding, and deleting the value
6278
// or subvalues.
6379
const createSetValueAtIndex = ( index ) => ( newValue ) => {
@@ -68,6 +84,11 @@ const RepeatableFieldList = ( {
6884
};
6985

7086
const deleteValueAtIndex = ( index ) => {
87+
itemIdsRef.current = [
88+
...itemIdsRef.current.slice( 0, index ),
89+
...itemIdsRef.current.slice( index + 1 ),
90+
];
91+
7192
const newValues = [
7293
...(
7394
valuesArray || []
@@ -81,6 +102,8 @@ const RepeatableFieldList = ( {
81102
};
82103

83104
const addValue = () => {
105+
itemIdsRef.current = [ ...itemIdsRef.current, nextIdRef.current++ ];
106+
84107
const newValues = [
85108
...valuesArray,
86109
// @todo does an empty string always work?
@@ -98,6 +121,12 @@ const RepeatableFieldList = ( {
98121
return;
99122
}
100123

124+
const newIds = [ ...itemIdsRef.current ];
125+
const tempId = newIds[ secondIndex ];
126+
newIds[ secondIndex ] = newIds[ firstIndex ];
127+
newIds[ firstIndex ] = tempId;
128+
itemIdsRef.current = newIds;
129+
101130
const newValues = [ ...valuesArray ];
102131
const tempValue = newValues[ secondIndex ];
103132

@@ -128,6 +157,8 @@ const RepeatableFieldList = ( {
128157
const oldIndex = parseInt( active.id, 10 );
129158
const newIndex = parseInt( over.id, 10 );
130159

160+
itemIdsRef.current = arrayMove( itemIdsRef.current, oldIndex, newIndex );
161+
131162
setFullValue(
132163
arrayMove( valuesArray, oldIndex, newIndex ),
133164
);
@@ -215,7 +246,7 @@ const RepeatableFieldList = ( {
215246
showTooltip
216247
/>
217248
) : null }
218-
key={ `${ fieldConfig.name }-${ JSON.stringify( valueItem ) }-${ index }` }
249+
key={ `${ fieldConfig.name }-${ itemIdsRef.current[ index ] }` }
219250
/>
220251
);
221252
} ) }

ui/js/dfv/src/fields/currency/index.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,6 @@ const Currency = ( {
5656
addValidationRules( [ numberValidationRule ] );
5757
}, [] );
5858

59-
// Sync local state with external value changes (e.g., after drag-and-drop reorder).
60-
useEffect( () => {
61-
setFormattedValue(
62-
formatNumberWithPodsFormat( value, format, false, decimalHandling, decimalMaxLength )
63-
);
64-
}, [ value ] );
65-
6659
const handleChange = ( event ) => {
6760
if ( isSlider ) {
6861
// The "range" (slider) input doesn't support the readonly attribute,

ui/js/dfv/src/fields/datetime/index.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -250,20 +250,6 @@ const DateTime = ( {
250250
},
251251
);
252252

253-
// Sync local state with external value changes (e.g., after drag-and-drop reorder).
254-
useEffect( () => {
255-
const isCurrentValueEmpty = [ '0000-00-00', '0000-00-00 00:00:00', '' ].includes( value );
256-
257-
if ( isCurrentValueEmpty ) {
258-
setLocalStringValue( '' );
259-
setLocalMomentValue( '' );
260-
} else {
261-
const momentParsed = moment( value, [ getDBFormat(), getFullFormat() ] );
262-
setLocalStringValue( formatMomentObject( momentParsed, value ) );
263-
setLocalMomentValue( momentParsed );
264-
}
265-
}, [ value ] );
266-
267253
const handleChange = ( newValue ) => {
268254
let momentObject = newValue;
269255

ui/js/dfv/src/fields/number-field/index.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@ const NumberField = ( {
6262
addValidationRules( [ numberValidationRule ] );
6363
}, [] );
6464

65-
// Sync local state with external value changes (e.g., after drag-and-drop reorder).
66-
useEffect( () => {
67-
setFormattedValue(
68-
formatNumberWithPodsFormat( value, format, softFormat, decimalHandlingForNumber, decimalMaxLength )
69-
);
70-
}, [ value ] );
71-
7265
const handleChange = ( event ) => {
7366
if ( isSlider ) {
7467
// The "range" (slider) input doesn't support the readonly attribute,

ui/js/react-jsx-runtime.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)