@okta/odyssey-react-mui 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/@types/react-augment.d.js +2 -0
  3. package/dist/@types/react-augment.d.js.map +1 -0
  4. package/dist/Autocomplete.js +32 -23
  5. package/dist/Autocomplete.js.map +1 -1
  6. package/dist/Checkbox.js +16 -7
  7. package/dist/Checkbox.js.map +1 -1
  8. package/dist/NativeSelect.js +22 -8
  9. package/dist/NativeSelect.js.map +1 -1
  10. package/dist/PasswordField.js +19 -5
  11. package/dist/PasswordField.js.map +1 -1
  12. package/dist/RadioGroup.js +11 -8
  13. package/dist/RadioGroup.js.map +1 -1
  14. package/dist/SearchField.js +17 -16
  15. package/dist/SearchField.js.map +1 -1
  16. package/dist/Select.js +34 -18
  17. package/dist/Select.js.map +1 -1
  18. package/dist/TextField.js +20 -6
  19. package/dist/TextField.js.map +1 -1
  20. package/dist/inputUtils.js +46 -0
  21. package/dist/inputUtils.js.map +1 -0
  22. package/dist/labs/VirtualizedAutocomplete.js +29 -23
  23. package/dist/labs/VirtualizedAutocomplete.js.map +1 -1
  24. package/dist/src/Autocomplete.d.ts +0 -1
  25. package/dist/src/Autocomplete.d.ts.map +1 -1
  26. package/dist/src/Checkbox.d.ts.map +1 -1
  27. package/dist/src/NativeSelect.d.ts +19 -44
  28. package/dist/src/NativeSelect.d.ts.map +1 -1
  29. package/dist/src/PasswordField.d.ts +10 -2
  30. package/dist/src/PasswordField.d.ts.map +1 -1
  31. package/dist/src/RadioGroup.d.ts.map +1 -1
  32. package/dist/src/SearchField.d.ts +10 -2
  33. package/dist/src/SearchField.d.ts.map +1 -1
  34. package/dist/src/Select.d.ts +5 -1
  35. package/dist/src/Select.d.ts.map +1 -1
  36. package/dist/src/TextField.d.ts +8 -0
  37. package/dist/src/TextField.d.ts.map +1 -1
  38. package/dist/src/inputUtils.d.ts +47 -0
  39. package/dist/src/inputUtils.d.ts.map +1 -0
  40. package/dist/src/labs/VirtualizedAutocomplete.d.ts.map +1 -1
  41. package/dist/tsconfig.production.tsbuildinfo +1 -1
  42. package/package.json +3 -3
  43. package/src/@types/react-augment.d.ts +19 -0
  44. package/src/Autocomplete.tsx +49 -43
  45. package/src/Checkbox.tsx +16 -9
  46. package/src/NativeSelect.tsx +78 -25
  47. package/src/PasswordField.tsx +32 -4
  48. package/src/RadioGroup.tsx +12 -10
  49. package/src/SearchField.tsx +24 -18
  50. package/src/Select.tsx +43 -25
  51. package/src/TextField.tsx +33 -5
  52. package/src/inputUtils.ts +76 -0
  53. package/src/labs/VirtualizedAutocomplete.tsx +48 -42
  54. package/dist/src/useControlledState.d.ts +0 -28
  55. package/dist/src/useControlledState.d.ts.map +0 -1
  56. package/dist/useControlledState.js +0 -33
  57. package/dist/useControlledState.js.map +0 -1
  58. package/src/useControlledState.ts +0 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okta/odyssey-react-mui",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "React MUI components for Odyssey, Okta's design system",
5
5
  "author": "Okta, Inc.",
6
6
  "license": "Apache-2.0",
@@ -51,7 +51,7 @@
51
51
  "@mui/system": "^5.14.9",
52
52
  "@mui/utils": "^5.11.2",
53
53
  "@mui/x-date-pickers": "^5.0.15",
54
- "@okta/odyssey-design-tokens": "1.8.0",
54
+ "@okta/odyssey-design-tokens": "1.8.1",
55
55
  "date-fns": "^2.30.0",
56
56
  "i18next": "^23.5.1",
57
57
  "material-react-table": "^2.0.2",
@@ -63,5 +63,5 @@
63
63
  "react": ">=17 <19",
64
64
  "react-dom": ">=17 <19"
65
65
  },
66
- "gitHead": "279ce97d18cf87719c03592ff66e341441b51c10"
66
+ "gitHead": "c1616886687301f9d71d0e7f1442f72b2f520ac1"
67
67
  }
@@ -0,0 +1,19 @@
1
+ /*!
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { FC } from "react";
14
+
15
+ export interface ForwardRefWithType extends FC<WithForwardRefProps<Option>> {
16
+ <T extends Option>(props: WithForwardRefProps<T>): ReturnType<
17
+ FC<WithForwardRefProps<T>>
18
+ >;
19
+ }
@@ -17,12 +17,16 @@ import {
17
17
  UseAutocompleteProps,
18
18
  AutocompleteValue,
19
19
  } from "@mui/material";
20
- import { memo, useCallback, useMemo } from "react";
20
+ import { memo, useCallback, useMemo, useRef } from "react";
21
21
 
22
22
  import { Field } from "./Field";
23
23
  import { FieldComponentProps } from "./FieldComponentProps";
24
24
  import type { SeleniumProps } from "./SeleniumProps";
25
- import { useControlledState } from "./useControlledState";
25
+ import {
26
+ ComponentControlledState,
27
+ useInputValues,
28
+ getControlState,
29
+ } from "./inputUtils";
26
30
 
27
31
  export type AutocompleteProps<
28
32
  OptionType,
@@ -31,7 +35,6 @@ export type AutocompleteProps<
31
35
  > = {
32
36
  /**
33
37
  * The default value. Use when the component is not controlled.
34
- * @default props.multiple ? [] : null
35
38
  */
36
39
  defaultValue?: UseAutocompleteProps<
37
40
  OptionType,
@@ -189,6 +192,45 @@ const Autocomplete = <
189
192
  getIsOptionEqualToValue,
190
193
  testId,
191
194
  }: AutocompleteProps<OptionType, HasMultipleChoices, IsCustomValueAllowed>) => {
195
+ const controlledStateRef = useRef(
196
+ getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
197
+ );
198
+ const defaultValueProp = useMemo<
199
+ | AutocompleteValue<
200
+ OptionType,
201
+ HasMultipleChoices,
202
+ undefined,
203
+ IsCustomValueAllowed
204
+ >
205
+ | undefined
206
+ >(() => {
207
+ if (hasMultipleChoices) {
208
+ if (value === undefined) {
209
+ return defaultValue;
210
+ }
211
+ return [] as AutocompleteValue<
212
+ OptionType,
213
+ HasMultipleChoices,
214
+ undefined,
215
+ IsCustomValueAllowed
216
+ >;
217
+ }
218
+ return value === undefined ? defaultValue : undefined;
219
+ }, [defaultValue, hasMultipleChoices, value]);
220
+
221
+ const valueProps = useInputValues({
222
+ defaultValue: defaultValueProp,
223
+ value: value,
224
+ controlState: controlledStateRef.current,
225
+ });
226
+
227
+ const inputValueProp = useMemo(() => {
228
+ if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
229
+ return { inputValue };
230
+ }
231
+ return undefined;
232
+ }, [inputValue]);
233
+
192
234
  const renderInput = useCallback(
193
235
  ({ InputLabelProps, InputProps, ...params }) => (
194
236
  <Field
@@ -223,39 +265,6 @@ const Autocomplete = <
223
265
  ),
224
266
  [errorMessage, hint, isOptional, label, nameOverride]
225
267
  );
226
-
227
- const defaultValuesProp = useMemo<
228
- | AutocompleteValue<
229
- OptionType,
230
- HasMultipleChoices,
231
- undefined,
232
- IsCustomValueAllowed
233
- >
234
- | undefined
235
- >(() => {
236
- if (hasMultipleChoices) {
237
- return defaultValue === undefined
238
- ? ([] as AutocompleteValue<
239
- OptionType,
240
- HasMultipleChoices,
241
- undefined,
242
- IsCustomValueAllowed
243
- >)
244
- : defaultValue;
245
- }
246
- return defaultValue ?? undefined;
247
- }, [defaultValue, hasMultipleChoices]);
248
-
249
- const [localValue, setLocalValue] = useControlledState({
250
- controlledValue: value,
251
- uncontrolledValue: defaultValuesProp,
252
- });
253
-
254
- const [localInputValue, setLocalInputValue] = useControlledState({
255
- controlledValue: inputValue,
256
- uncontrolledValue: undefined,
257
- });
258
-
259
268
  const onChange = useCallback<
260
269
  NonNullable<
261
270
  UseAutocompleteProps<
@@ -267,10 +276,9 @@ const Autocomplete = <
267
276
  >
268
277
  >(
269
278
  (event, value, reason, details) => {
270
- setLocalValue(value);
271
279
  onChangeProp?.(event, value, reason, details);
272
280
  },
273
- [onChangeProp, setLocalValue]
281
+ [onChangeProp]
274
282
  );
275
283
 
276
284
  const onInputChange = useCallback<
@@ -284,18 +292,18 @@ const Autocomplete = <
284
292
  >
285
293
  >(
286
294
  (event, value, reason) => {
287
- setLocalInputValue(value);
288
295
  onInputChangeProp?.(event, value, reason);
289
296
  },
290
- [onInputChangeProp, setLocalInputValue]
297
+ [onInputChangeProp]
291
298
  );
292
299
 
293
300
  return (
294
301
  <MuiAutocomplete
302
+ {...valueProps}
303
+ {...inputValueProp}
295
304
  // AutoComplete is wrapped in a div within MUI which does not get the disabled attr. So this aria-disabled gets set in the div
296
305
  aria-disabled={isDisabled}
297
306
  data-se={testId}
298
- defaultValue={defaultValuesProp}
299
307
  disableCloseOnSelect={hasMultipleChoices}
300
308
  disabled={isDisabled}
301
309
  freeSolo={isCustomValueAllowed}
@@ -310,8 +318,6 @@ const Autocomplete = <
310
318
  options={options}
311
319
  readOnly={isReadOnly}
312
320
  renderInput={renderInput}
313
- value={localValue}
314
- inputValue={localInputValue}
315
321
  isOptionEqualToValue={getIsOptionEqualToValue}
316
322
  />
317
323
  );
package/src/Checkbox.tsx CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { useTranslation } from "react-i18next";
14
- import { memo, useCallback, useMemo } from "react";
14
+ import { memo, useCallback, useMemo, useRef } from "react";
15
15
  import {
16
16
  Checkbox as MuiCheckbox,
17
17
  CheckboxProps as MuiCheckboxProps,
@@ -22,7 +22,7 @@ import {
22
22
  import { FieldComponentProps } from "./FieldComponentProps";
23
23
  import { Typography } from "./Typography";
24
24
  import type { SeleniumProps } from "./SeleniumProps";
25
- import { useControlledState } from "./useControlledState";
25
+ import { ComponentControlledState, getControlState } from "./inputUtils";
26
26
  import { CheckedFieldProps } from "./FormCheckedProps";
27
27
 
28
28
  export const checkboxValidityValues = ["valid", "invalid", "inherit"] as const;
@@ -90,10 +90,18 @@ const Checkbox = ({
90
90
  value,
91
91
  }: CheckboxProps) => {
92
92
  const { t } = useTranslation();
93
- const [isLocalChecked, setIsLocalChecked] = useControlledState({
94
- controlledValue: isChecked,
95
- uncontrolledValue: isDefaultChecked,
96
- });
93
+ const controlledStateRef = useRef(
94
+ getControlState({
95
+ controlledValue: isChecked,
96
+ uncontrolledValue: isDefaultChecked,
97
+ })
98
+ );
99
+ const inputValues = useMemo(() => {
100
+ if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
101
+ return { checked: isChecked };
102
+ }
103
+ return { defaultChecked: isDefaultChecked };
104
+ }, [isDefaultChecked, isChecked]);
97
105
 
98
106
  const label = useMemo(() => {
99
107
  return (
@@ -114,10 +122,9 @@ const Checkbox = ({
114
122
 
115
123
  const onChange = useCallback<NonNullable<MuiCheckboxProps["onChange"]>>(
116
124
  (event, checked) => {
117
- setIsLocalChecked(checked);
118
125
  onChangeProp?.(event, checked);
119
126
  },
120
- [onChangeProp, setIsLocalChecked]
127
+ [onChangeProp]
121
128
  );
122
129
 
123
130
  return (
@@ -134,7 +141,7 @@ const Checkbox = ({
134
141
  }
135
142
  control={
136
143
  <MuiCheckbox
137
- checked={isLocalChecked}
144
+ {...inputValues}
138
145
  indeterminate={isIndeterminate}
139
146
  onChange={onChange}
140
147
  required={isRequired}
@@ -10,12 +10,23 @@
10
10
  * See the License for the specific language governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { ReactElement, forwardRef, memo, useCallback } from "react";
14
- import { Select as MuiSelect } from "@mui/material";
15
- import { SelectProps as MuiSelectProps } from "@mui/material";
13
+ import React, {
14
+ ReactElement,
15
+ forwardRef,
16
+ memo,
17
+ useCallback,
18
+ useMemo,
19
+ useRef,
20
+ } from "react";
21
+ import {
22
+ Select as MuiSelect,
23
+ SelectProps as MuiSelectProps,
24
+ } from "@mui/material";
16
25
  import { Field } from "./Field";
17
26
  import { FieldComponentProps } from "./FieldComponentProps";
18
27
  import type { SeleniumProps } from "./SeleniumProps";
28
+ import { getControlState, useInputValues } from "./inputUtils";
29
+ import { ForwardRefWithType } from "./@types/react-augment";
19
30
 
20
31
  export type NativeSelectOption = {
21
32
  text: string;
@@ -23,19 +34,30 @@ export type NativeSelectOption = {
23
34
  type?: "heading" | "option";
24
35
  };
25
36
 
26
- export type NativeSelectProps = {
37
+ export type NativeSelectValueType<HasMultipleChoices> =
38
+ HasMultipleChoices extends true ? string[] : string;
39
+
40
+ export type NativeSelectProps<
41
+ Value extends NativeSelectValueType<HasMultipleChoices>,
42
+ HasMultipleChoices extends boolean
43
+ > = {
27
44
  /**
28
45
  * The options or optgroup elements within the NativeSelect
29
46
  */
30
47
  children?: ReactElement<"option"> | ReactElement<"optgroup">;
31
48
  /**
32
- * The default value of the NativeSelect. Only applicable if `value` is not provided
49
+ * The default value of the NativeSelect. Use when component is uncontrolled
50
+ */
51
+ defaultValue?: Value;
52
+ /**
53
+ * If `true`, the Select allows multiple selections
33
54
  */
34
- defaultValue?: string;
55
+ hasMultipleChoices?: HasMultipleChoices;
35
56
  /**
36
- * If `true`, the NativeSelect allows multiple selections
57
+ * @deprecated Use `hasMultipleChoices` instead
37
58
  */
38
- isMultiSelect?: boolean;
59
+ /** **Deprecated:** use `hasMultipleChoices` */
60
+ isMultiSelect?: HasMultipleChoices;
39
61
  /**
40
62
  * The label text for the NativeSelect
41
63
  */
@@ -43,78 +65,109 @@ export type NativeSelectProps = {
43
65
  /**
44
66
  * Callback fired when the NativeSelect loses focus
45
67
  */
46
- onBlur?: MuiSelectProps["onBlur"];
68
+ onBlur?: MuiSelectProps<Value>["onBlur"];
47
69
  /**
48
70
  * Callback fired when the value of the NativeSelect changes
49
71
  */
50
- onChange?: MuiSelectProps["onChange"];
72
+ onChange?: MuiSelectProps<Value>["onChange"];
51
73
  /**
52
74
  * Callback fired when the NativeSelect gains focus
53
75
  */
54
- onFocus?: MuiSelectProps["onFocus"];
76
+ onFocus?: MuiSelectProps<Value>["onFocus"];
77
+ options: Value;
55
78
  /**
56
- * The value or values selected in the NativeSelect
79
+ * The value or values selected in the NativeSelect. Use when component is controlled
57
80
  */
58
- value?: string | string[];
81
+ value?: Value;
59
82
  } & Pick<
60
83
  FieldComponentProps,
61
84
  "errorMessage" | "hint" | "id" | "isDisabled" | "isOptional"
62
85
  > &
63
86
  SeleniumProps;
64
87
 
65
- const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
66
- (
88
+ const NativeSelect: ForwardRefWithType = forwardRef(
89
+ <
90
+ Value extends NativeSelectValueType<HasMultipleChoices>,
91
+ HasMultipleChoices extends boolean
92
+ >(
67
93
  {
68
94
  defaultValue,
69
95
  errorMessage,
96
+ hasMultipleChoices: hasMultipleChoicesProp,
70
97
  hint,
71
98
  id: idOverride,
72
99
  isDisabled = false,
73
- isMultiSelect = false,
100
+ isMultiSelect,
74
101
  isOptional = false,
75
102
  label,
76
103
  onBlur,
77
- onChange,
104
+ onChange: onChangeProp,
78
105
  onFocus,
79
106
  testId,
80
107
  value,
81
108
  children,
82
- },
83
- ref
109
+ }: NativeSelectProps<Value, HasMultipleChoices>,
110
+ ref?: React.Ref<ReactElement>
84
111
  ) => {
112
+ const controlledStateRef = useRef(
113
+ getControlState({
114
+ controlledValue: value,
115
+ uncontrolledValue: defaultValue,
116
+ })
117
+ );
118
+ const inputValues = useInputValues({
119
+ defaultValue,
120
+ value,
121
+ controlState: controlledStateRef.current,
122
+ });
123
+
124
+ const onChange = useCallback<
125
+ NonNullable<MuiSelectProps<Value>["onChange"]>
126
+ >(
127
+ (event, child) => {
128
+ onChangeProp?.(event, child);
129
+ },
130
+ [onChangeProp]
131
+ );
132
+
133
+ const hasMultipleChoices = useMemo(
134
+ () =>
135
+ hasMultipleChoicesProp === undefined
136
+ ? isMultiSelect
137
+ : hasMultipleChoicesProp,
138
+ [hasMultipleChoicesProp, isMultiSelect]
139
+ );
85
140
  const renderFieldComponent = useCallback(
86
141
  ({ ariaDescribedBy, errorMessageElementId, labelElementId }) => (
87
142
  <MuiSelect
143
+ {...inputValues}
88
144
  aria-describedby={ariaDescribedBy}
89
145
  children={children}
90
146
  data-se={testId}
91
- defaultValue={defaultValue}
92
147
  id={idOverride}
93
148
  inputProps={{
94
149
  "aria-errormessage": errorMessageElementId,
95
150
  "aria-labelledby": labelElementId,
96
151
  }}
97
152
  name={idOverride}
98
- multiple={isMultiSelect}
153
+ multiple={hasMultipleChoices}
99
154
  native={true}
100
155
  onBlur={onBlur}
101
156
  onChange={onChange}
102
157
  onFocus={onFocus}
103
158
  ref={ref}
104
- value={value}
105
159
  />
106
160
  ),
107
161
  [
108
162
  children,
109
- defaultValue,
110
163
  idOverride,
111
- isMultiSelect,
164
+ inputValues,
165
+ hasMultipleChoices,
112
166
  onBlur,
113
167
  onChange,
114
168
  onFocus,
115
169
  ref,
116
170
  testId,
117
- value,
118
171
  ]
119
172
  );
120
173
 
@@ -17,6 +17,7 @@ import {
17
17
  forwardRef,
18
18
  memo,
19
19
  useCallback,
20
+ useRef,
20
21
  useState,
21
22
  } from "react";
22
23
 
@@ -25,6 +26,7 @@ import { Field } from "./Field";
25
26
  import { FieldComponentProps } from "./FieldComponentProps";
26
27
  import type { SeleniumProps } from "./SeleniumProps";
27
28
  import { useTranslation } from "react-i18next";
29
+ import { getControlState, useInputValues } from "./inputUtils";
28
30
 
29
31
  export type PasswordFieldProps = {
30
32
  /**
@@ -33,6 +35,10 @@ export type PasswordFieldProps = {
33
35
  * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
34
36
  */
35
37
  autoCompleteType?: "current-password" | "new-password";
38
+ /**
39
+ * initial value for input. Use when component in uncontrolled.
40
+ */
41
+ defaultValue?: string;
36
42
  /**
37
43
  * If `true`, the component will receive focus automatically.
38
44
  */
@@ -62,7 +68,7 @@ export type PasswordFieldProps = {
62
68
  */
63
69
  placeholder?: string;
64
70
  /**
65
- * The value of the `input` element, required for a controlled component.
71
+ * The value of the `input` element. Use when component is controlled.
66
72
  */
67
73
  value?: string;
68
74
  } & FieldComponentProps &
@@ -72,6 +78,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
72
78
  (
73
79
  {
74
80
  autoCompleteType,
81
+ defaultValue,
75
82
  errorMessage,
76
83
  hasInitialFocus,
77
84
  hint,
@@ -82,7 +89,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
82
89
  isReadOnly,
83
90
  label,
84
91
  name: nameOverride,
85
- onChange,
92
+ onChange: onChangeProp,
86
93
  onFocus,
87
94
  onBlur,
88
95
  placeholder,
@@ -100,9 +107,31 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
100
107
  );
101
108
  }, []);
102
109
 
110
+ const controlledStateRef = useRef(
111
+ getControlState({
112
+ controlledValue: value,
113
+ uncontrolledValue: defaultValue,
114
+ })
115
+ );
116
+ const inputValues = useInputValues({
117
+ defaultValue,
118
+ value,
119
+ controlState: controlledStateRef.current,
120
+ });
121
+
122
+ const onChange = useCallback<
123
+ ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>
124
+ >(
125
+ (event) => {
126
+ onChangeProp?.(event);
127
+ },
128
+ [onChangeProp]
129
+ );
130
+
103
131
  const renderFieldComponent = useCallback(
104
132
  ({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
105
133
  <InputBase
134
+ {...inputValues}
106
135
  aria-describedby={ariaDescribedBy}
107
136
  autoComplete={inputType === "password" ? autoCompleteType : "off"}
108
137
  /* eslint-disable-next-line jsx-a11y/no-autofocus */
@@ -140,12 +169,12 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
140
169
  ref={ref}
141
170
  required={!isOptional}
142
171
  type={inputType}
143
- value={value}
144
172
  />
145
173
  ),
146
174
  [
147
175
  autoCompleteType,
148
176
  hasInitialFocus,
177
+ inputValues,
149
178
  t,
150
179
  togglePasswordVisibility,
151
180
  inputType,
@@ -159,7 +188,6 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
159
188
  hasShowPassword,
160
189
  ref,
161
190
  testId,
162
- value,
163
191
  ]
164
192
  );
165
193
 
@@ -14,13 +14,13 @@ import {
14
14
  RadioGroup as MuiRadioGroup,
15
15
  type RadioGroupProps as MuiRadioGroupProps,
16
16
  } from "@mui/material";
17
- import { memo, ReactElement, useCallback } from "react";
17
+ import { memo, ReactElement, useCallback, useRef } from "react";
18
18
 
19
19
  import { Radio, RadioProps } from "./Radio";
20
20
  import { Field } from "./Field";
21
21
  import { FieldComponentProps } from "./FieldComponentProps";
22
22
  import type { SeleniumProps } from "./SeleniumProps";
23
- import { useControlledState } from "./useControlledState";
23
+ import { getControlState, useInputValues } from "./inputUtils";
24
24
 
25
25
  export type RadioGroupProps = {
26
26
  /**
@@ -62,35 +62,37 @@ const RadioGroup = ({
62
62
  testId,
63
63
  value,
64
64
  }: RadioGroupProps) => {
65
- const [localValue, setLocalValue] = useControlledState({
66
- controlledValue: value,
67
- uncontrolledValue: defaultValue,
65
+ const controlledStateRef = useRef(
66
+ getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
67
+ );
68
+ const inputValues = useInputValues({
69
+ defaultValue,
70
+ value,
71
+ controlState: controlledStateRef.current,
68
72
  });
69
73
 
70
74
  const onChange = useCallback<NonNullable<MuiRadioGroupProps["onChange"]>>(
71
75
  (event, value) => {
72
- setLocalValue(value);
73
76
  onChangeProp?.(event, value);
74
77
  },
75
- [onChangeProp, setLocalValue]
78
+ [onChangeProp]
76
79
  );
77
80
  const renderFieldComponent = useCallback(
78
81
  ({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
79
82
  <MuiRadioGroup
83
+ {...inputValues}
80
84
  aria-describedby={ariaDescribedBy}
81
85
  aria-errormessage={errorMessageElementId}
82
86
  aria-labelledby={labelElementId}
83
87
  data-se={testId}
84
- defaultValue={defaultValue}
85
88
  id={id}
86
89
  name={nameOverride ?? id}
87
90
  onChange={onChange}
88
- value={localValue}
89
91
  >
90
92
  {children}
91
93
  </MuiRadioGroup>
92
94
  ),
93
- [children, defaultValue, nameOverride, onChange, testId, localValue]
95
+ [children, inputValues, nameOverride, onChange, testId]
94
96
  );
95
97
 
96
98
  return (