Skip to content

Commit 557d562

Browse files
committed
fix(SelectWidget): use real enum values in <option value> with data-index for typed resolution
Replace index-based <option value="0"> with real enum values <option value="real_value"> so that native form submission sends meaningful values. Add data-index attribute to preserve typed value resolution via enumOptionsValueForIndex in JS handlers. Scope limited to core and react-bootstrap (native <select> packages). Custom component packages (chakra, mui, mantine, etc.) are unchanged as they don't participate in native form submission.
1 parent e783e17 commit 557d562

26 files changed

Lines changed: 2402 additions & 1515 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,48 +37,16 @@ should change the heading of the (upcoming) version to include a major version b
3737

3838
- Forward `required` to `BaseInputTemplate` so that required string and number fields render the HTML `required` attribute, fixing [#3743](https://github.com/rjsf-team/react-jsonschema-form/issues/3743)
3939

40-
## @rjsf/chakra-ui
41-
42-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
43-
4440
## @rjsf/core
4541

4642
- Fixed a breaking change introduced in v6.4.0 where array properties with enum items no longer used `ui:enumNames` from the array property's uiSchema ([#4985](https://github.com/rjsf-team/react-jsonschema-form/issues/4985))
4743
- Passed `parentUiSchema` to `ArrayFieldItem` to maintain `uiSchema`, this prop was introduced recently but was not being passed
4844
- Fixed `extraErrors` not displaying when `customErrors` are also present (e.g., with array fields and controlled `formData`), fixing [#4982](https://github.com/rjsf-team/react-jsonschema-form/issues/4982)
49-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
50-
51-
## @rjsf/daisyui
52-
53-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
54-
55-
## @rjsf/fluentui-rc
56-
57-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
58-
59-
## @rjsf/mantine
60-
61-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
62-
63-
## @rjsf/mui
64-
65-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
66-
67-
## @rjsf/primereact
68-
69-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
45+
- Fixed `SelectWidget` rendering array indices instead of real enum values in `<option value>`, breaking native form submission. Uses `data-index` for internal typed value resolution ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
7046

7147
## @rjsf/react-bootstrap
7248

73-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
74-
75-
## @rjsf/semantic-ui
76-
77-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
78-
79-
## @rjsf/shadcn
80-
81-
- Fixed `SelectWidget` rendering array indices instead of real enum values when `nameGenerator` is active, breaking native form submission ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
49+
- Fixed `SelectWidget` rendering array indices instead of real enum values in `<option value>`, breaking native form submission. Uses `data-index` for internal typed value resolution ([#4693](https://github.com/rjsf-team/react-jsonschema-form/issues/4693))
8250

8351
## Dev / docs / playground
8452

packages/chakra-ui/src/SelectWidget/SelectWidget.tsx

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,21 @@ export default function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFS
4242
uiSchema,
4343
} = props;
4444
const { enumOptions, enumDisabled, emptyValue } = options;
45-
const useRealValues = !!htmlName;
4645

4746
const _onMultiChange = ({ value }: SelectValueChangeDetails) => {
48-
const newValue = useRealValues ? value : enumOptionsValueForIndex<S>(value, enumOptions, emptyValue);
49-
return onChange(newValue);
47+
return onChange(enumOptionsValueForIndex<S>(value, enumOptions, emptyValue));
5048
};
5149

5250
const _onSingleChange = ({ value }: SelectValueChangeDetails) => {
53-
const resolved = useRealValues ? value : enumOptionsValueForIndex<S>(value, enumOptions, emptyValue);
54-
const selected = Array.isArray(resolved) && resolved.length === 1 ? resolved[0] : resolved;
55-
return onChange(selected || emptyValue);
51+
const selected = enumOptionsValueForIndex<S>(value, enumOptions, emptyValue);
52+
return onChange(Array.isArray(selected) && selected.length === 1 ? selected[0] : selected);
5653
};
5754

58-
const _onBlur = ({ target }: FocusEvent<HTMLInputElement>) => {
59-
const newValue = useRealValues
60-
? target && target.value
61-
: enumOptionsValueForIndex<S>(target && target.value, enumOptions, emptyValue);
62-
onBlur(id, newValue);
63-
};
55+
const _onBlur = ({ target }: FocusEvent<HTMLInputElement>) =>
56+
onBlur(id, enumOptionsValueForIndex<S>(target && target.value, enumOptions, emptyValue));
6457

65-
const _onFocus = ({ target }: FocusEvent<HTMLInputElement>) => {
66-
const newValue = useRealValues
67-
? target && target.value
68-
: enumOptionsValueForIndex<S>(target && target.value, enumOptions, emptyValue);
69-
onFocus(id, newValue);
70-
};
58+
const _onFocus = ({ target }: FocusEvent<HTMLInputElement>) =>
59+
onFocus(id, enumOptionsValueForIndex<S>(target && target.value, enumOptions, emptyValue));
7160

7261
const showPlaceholderOption = !multiple && schema.default === undefined;
7362
const { valueLabelMap, displayEnumOptions } = useMemo((): {
@@ -79,11 +68,10 @@ export default function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFS
7968
if (Array.isArray(enumOptions)) {
8069
displayEnumOptions = enumOptions.map((option: EnumOptionsType<S>, index: number) => {
8170
const { value, label } = option;
82-
const optionKey = useRealValues ? String(value) : index;
83-
valueLabelMap[optionKey] = label || String(value);
71+
valueLabelMap[index] = label || String(value);
8472
return {
8573
label,
86-
value: useRealValues ? String(value) : String(index),
74+
value: String(index),
8775
disabled: Array.isArray(enumDisabled) && enumDisabled.indexOf(value) !== -1,
8876
};
8977
});
@@ -92,40 +80,30 @@ export default function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFS
9280
}
9381
}
9482
return { valueLabelMap: valueLabelMap, displayEnumOptions: displayEnumOptions };
95-
}, [enumDisabled, enumOptions, placeholder, showPlaceholderOption, useRealValues]);
83+
}, [enumDisabled, enumOptions, placeholder, showPlaceholderOption]);
9684

9785
const isMultiple = typeof multiple !== 'undefined' && multiple !== false && Boolean(enumOptions);
9886
const selectedIndex = enumOptionsIndexForValue<S>(value, enumOptions, isMultiple);
9987

10088
const getMultiValue = () =>
10189
((selectedIndex as string[]) || []).map((i: string) => {
102-
const key = useRealValues ? String(value[Number(i)]) : i;
10390
return {
104-
label: valueLabelMap[useRealValues ? key : i],
105-
value: useRealValues ? key : i.toString(),
91+
label: valueLabelMap[i],
92+
value: i.toString(),
10693
};
10794
});
10895

10996
const getSingleValue = () =>
11097
typeof selectedIndex !== 'undefined'
11198
? [
11299
{
113-
label: valueLabelMap[useRealValues ? String(value) : (selectedIndex as string)] || '',
114-
value: useRealValues ? String(value) : selectedIndex.toString(),
100+
label: valueLabelMap[selectedIndex as string] || '',
101+
value: selectedIndex.toString(),
115102
},
116103
]
117104
: [];
118105

119-
let formValue: string[];
120-
if (useRealValues) {
121-
if (typeof value === 'undefined') {
122-
formValue = [];
123-
} else {
124-
formValue = Array.isArray(value) ? value.map(String) : [String(value)];
125-
}
126-
} else {
127-
formValue = (isMultiple ? getMultiValue() : getSingleValue()).map((item) => item.value);
128-
}
106+
const formValue = (isMultiple ? getMultiValue() : getSingleValue()).map((item) => item.value);
129107

130108
const selectOptions = createListCollection({
131109
items: displayEnumOptions.filter((item) => item.value),

packages/chakra-ui/test/__snapshots__/Form.test.tsx.snap

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4087,17 +4087,17 @@ exports[`nameGenerator bracketNameGenerator select field with enum 1`] = `
40874087
value=""
40884088
/>
40894089
<option
4090-
value="red"
4090+
value="0"
40914091
>
40924092
red
40934093
</option>
40944094
<option
4095-
value="green"
4095+
value="1"
40964096
>
40974097
green
40984098
</option>
40994099
<option
4100-
value="blue"
4100+
value="2"
41014101
>
41024102
blue
41034103
</option>
@@ -4191,9 +4191,9 @@ exports[`nameGenerator bracketNameGenerator select field with enum 1`] = `
41914191
data-part="item"
41924192
data-scope="select"
41934193
data-state="unchecked"
4194-
data-value="red"
4194+
data-value="0"
41954195
dir="ltr"
4196-
id="select:root_color:option:red"
4196+
id="select:root_color:option:0"
41974197
role="option"
41984198
>
41994199
red
@@ -4221,9 +4221,9 @@ exports[`nameGenerator bracketNameGenerator select field with enum 1`] = `
42214221
data-part="item"
42224222
data-scope="select"
42234223
data-state="unchecked"
4224-
data-value="green"
4224+
data-value="1"
42254225
dir="ltr"
4226-
id="select:root_color:option:green"
4226+
id="select:root_color:option:1"
42274227
role="option"
42284228
>
42294229
green
@@ -4251,9 +4251,9 @@ exports[`nameGenerator bracketNameGenerator select field with enum 1`] = `
42514251
data-part="item"
42524252
data-scope="select"
42534253
data-state="unchecked"
4254-
data-value="blue"
4254+
data-value="2"
42554255
dir="ltr"
4256-
id="select:root_color:option:blue"
4256+
id="select:root_color:option:2"
42574257
role="option"
42584258
>
42594259
blue
@@ -8243,17 +8243,17 @@ exports[`nameGenerator dotNotationNameGenerator select field with enum 1`] = `
82438243
value=""
82448244
/>
82458245
<option
8246-
value="red"
8246+
value="0"
82478247
>
82488248
red
82498249
</option>
82508250
<option
8251-
value="green"
8251+
value="1"
82528252
>
82538253
green
82548254
</option>
82558255
<option
8256-
value="blue"
8256+
value="2"
82578257
>
82588258
blue
82598259
</option>
@@ -8347,9 +8347,9 @@ exports[`nameGenerator dotNotationNameGenerator select field with enum 1`] = `
83478347
data-part="item"
83488348
data-scope="select"
83498349
data-state="unchecked"
8350-
data-value="red"
8350+
data-value="0"
83518351
dir="ltr"
8352-
id="select:root_color:option:red"
8352+
id="select:root_color:option:0"
83538353
role="option"
83548354
>
83558355
red
@@ -8377,9 +8377,9 @@ exports[`nameGenerator dotNotationNameGenerator select field with enum 1`] = `
83778377
data-part="item"
83788378
data-scope="select"
83798379
data-state="unchecked"
8380-
data-value="green"
8380+
data-value="1"
83818381
dir="ltr"
8382-
id="select:root_color:option:green"
8382+
id="select:root_color:option:1"
83838383
role="option"
83848384
>
83858385
green
@@ -8407,9 +8407,9 @@ exports[`nameGenerator dotNotationNameGenerator select field with enum 1`] = `
84078407
data-part="item"
84088408
data-scope="select"
84098409
data-state="unchecked"
8410-
data-value="blue"
8410+
data-value="2"
84118411
dir="ltr"
8412-
id="select:root_color:option:blue"
8412+
id="select:root_color:option:2"
84138413
role="option"
84148414
>
84158415
blue

packages/core/src/components/widgets/SelectWidget.tsx

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import { ChangeEvent, FocusEvent, SyntheticEvent, useCallback } from 'react';
22
import {
33
ariaDescribedByIds,
4-
enumOptionsIndexForValue,
54
enumOptionsValueForIndex,
65
FormContextType,
76
RJSFSchema,
87
StrictRJSFSchema,
98
WidgetProps,
109
} from '@rjsf/utils';
1110

12-
function getValue(event: SyntheticEvent<HTMLSelectElement>, multiple: boolean) {
11+
/** Reads the data-index attribute from selected options to resolve typed enum values
12+
* via enumOptionsValueForIndex, while keeping real values in the standard value attribute
13+
* for native form submission.
14+
*/
15+
function getSelectedIndex(event: SyntheticEvent<HTMLSelectElement>, multiple: boolean) {
16+
const select = event.target as HTMLSelectElement;
1317
if (multiple) {
14-
return Array.from((event.target as HTMLSelectElement).options)
18+
return Array.from(select.options)
1519
.slice()
1620
.filter((o) => o.selected)
17-
.map((o) => o.value);
21+
.map((o) => o.dataset.index ?? o.value);
1822
}
19-
return (event.target as HTMLSelectElement).value;
23+
const selectedOption = select.options[select.selectedIndex];
24+
return selectedOption?.dataset.index ?? select.value;
2025
}
2126

2227
/** The `SelectWidget` is a widget for rendering dropdowns.
@@ -42,56 +47,36 @@ function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
4247
}: WidgetProps<T, S, F>) {
4348
const { enumOptions, enumDisabled, emptyValue: optEmptyVal } = options;
4449
const emptyValue = multiple ? [] : '';
45-
const useRealValues = !!htmlName;
4650

4751
const handleFocus = useCallback(
4852
(event: FocusEvent<HTMLSelectElement>) => {
49-
const newValue = getValue(event, multiple);
50-
const resolved = useRealValues
51-
? multiple
52-
? newValue
53-
: newValue || optEmptyVal
54-
: enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal);
55-
return onFocus(id, resolved);
53+
const newValue = getSelectedIndex(event, multiple);
54+
return onFocus(id, enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
5655
},
57-
[onFocus, id, multiple, enumOptions, optEmptyVal, useRealValues],
56+
[onFocus, id, multiple, enumOptions, optEmptyVal],
5857
);
5958

6059
const handleBlur = useCallback(
6160
(event: FocusEvent<HTMLSelectElement>) => {
62-
const newValue = getValue(event, multiple);
63-
const resolved = useRealValues
64-
? multiple
65-
? newValue
66-
: newValue || optEmptyVal
67-
: enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal);
68-
return onBlur(id, resolved);
61+
const newValue = getSelectedIndex(event, multiple);
62+
return onBlur(id, enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
6963
},
70-
[onBlur, id, multiple, enumOptions, optEmptyVal, useRealValues],
64+
[onBlur, id, multiple, enumOptions, optEmptyVal],
7165
);
7266

7367
const handleChange = useCallback(
7468
(event: ChangeEvent<HTMLSelectElement>) => {
75-
const newValue = getValue(event, multiple);
76-
const resolved = useRealValues
77-
? multiple
78-
? newValue
79-
: newValue || optEmptyVal
80-
: enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal);
81-
return onChange(resolved);
69+
const newValue = getSelectedIndex(event, multiple);
70+
return onChange(enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
8271
},
83-
[onChange, multiple, enumOptions, optEmptyVal, useRealValues],
72+
[onChange, multiple, enumOptions, optEmptyVal],
8473
);
8574

86-
const selectedIndexes = enumOptionsIndexForValue<S>(value, enumOptions, multiple);
8775
const isEmpty = typeof value === 'undefined' || (multiple && value.length < 1) || (!multiple && value === emptyValue);
8876
const showPlaceholderOption = !multiple && schema.default === undefined;
89-
90-
let selectValue;
91-
if (useRealValues) {
92-
selectValue = isEmpty ? emptyValue : multiple ? value.map(String) : String(value);
93-
} else {
94-
selectValue = typeof selectedIndexes === 'undefined' ? emptyValue : selectedIndexes;
77+
let selectValue = emptyValue;
78+
if (!isEmpty) {
79+
selectValue = multiple ? value.map(String) : String(value);
9580
}
9681

9782
return (
@@ -115,7 +100,7 @@ function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
115100
enumOptions.map(({ value, label }, i) => {
116101
const disabled = enumDisabled && enumDisabled.indexOf(value) !== -1;
117102
return (
118-
<option key={i} value={useRealValues ? String(value) : String(i)} disabled={disabled}>
103+
<option key={i} value={String(value)} data-index={String(i)} disabled={disabled}>
119104
{label}
120105
</option>
121106
);

packages/core/test/ArrayField.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,7 +1367,7 @@ describe('ArrayField', () => {
13671367
},
13681368
});
13691369

1370-
expect(node.querySelector("option[value='1']")).toBeDisabled(); // use index
1370+
expect(node.querySelector("option[value='bar']")).toBeDisabled(); // use real value
13711371
});
13721372

13731373
it('should use ui:enumNames from items uiSchema as option labels', () => {

0 commit comments

Comments
 (0)