@okta/odyssey-react-mui 1.8.0 → 1.8.2

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 (67) hide show
  1. package/CHANGELOG.md +10 -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 +34 -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/Field.js +2 -0
  9. package/dist/Field.js.map +1 -1
  10. package/dist/FieldComponentProps.js.map +1 -1
  11. package/dist/NativeSelect.js +24 -8
  12. package/dist/NativeSelect.js.map +1 -1
  13. package/dist/PasswordField.js +21 -5
  14. package/dist/PasswordField.js.map +1 -1
  15. package/dist/RadioGroup.js +11 -8
  16. package/dist/RadioGroup.js.map +1 -1
  17. package/dist/SearchField.js +19 -16
  18. package/dist/SearchField.js.map +1 -1
  19. package/dist/Select.js +36 -18
  20. package/dist/Select.js.map +1 -1
  21. package/dist/TextField.js +22 -6
  22. package/dist/TextField.js.map +1 -1
  23. package/dist/inputUtils.js +46 -0
  24. package/dist/inputUtils.js.map +1 -0
  25. package/dist/labs/VirtualizedAutocomplete.js +29 -23
  26. package/dist/labs/VirtualizedAutocomplete.js.map +1 -1
  27. package/dist/src/Autocomplete.d.ts +2 -3
  28. package/dist/src/Autocomplete.d.ts.map +1 -1
  29. package/dist/src/Checkbox.d.ts.map +1 -1
  30. package/dist/src/Field.d.ts +5 -1
  31. package/dist/src/Field.d.ts.map +1 -1
  32. package/dist/src/FieldComponentProps.d.ts +4 -0
  33. package/dist/src/FieldComponentProps.d.ts.map +1 -1
  34. package/dist/src/NativeSelect.d.ts +19 -44
  35. package/dist/src/NativeSelect.d.ts.map +1 -1
  36. package/dist/src/PasswordField.d.ts +10 -2
  37. package/dist/src/PasswordField.d.ts.map +1 -1
  38. package/dist/src/RadioGroup.d.ts.map +1 -1
  39. package/dist/src/SearchField.d.ts +12 -4
  40. package/dist/src/SearchField.d.ts.map +1 -1
  41. package/dist/src/Select.d.ts +6 -2
  42. package/dist/src/Select.d.ts.map +1 -1
  43. package/dist/src/TextField.d.ts +8 -0
  44. package/dist/src/TextField.d.ts.map +1 -1
  45. package/dist/src/inputUtils.d.ts +47 -0
  46. package/dist/src/inputUtils.d.ts.map +1 -0
  47. package/dist/src/labs/VirtualizedAutocomplete.d.ts.map +1 -1
  48. package/dist/tsconfig.production.tsbuildinfo +1 -1
  49. package/package.json +3 -3
  50. package/src/@types/react-augment.d.ts +19 -0
  51. package/src/Autocomplete.tsx +52 -44
  52. package/src/Checkbox.tsx +16 -9
  53. package/src/Field.tsx +6 -0
  54. package/src/FieldComponentProps.ts +4 -0
  55. package/src/NativeSelect.tsx +81 -26
  56. package/src/PasswordField.tsx +34 -4
  57. package/src/RadioGroup.tsx +12 -10
  58. package/src/SearchField.tsx +27 -19
  59. package/src/Select.tsx +52 -26
  60. package/src/TextField.tsx +35 -5
  61. package/src/inputUtils.ts +76 -0
  62. package/src/labs/VirtualizedAutocomplete.tsx +48 -42
  63. package/dist/src/useControlledState.d.ts +0 -28
  64. package/dist/src/useControlledState.d.ts.map +0 -1
  65. package/dist/useControlledState.js +0 -33
  66. package/dist/useControlledState.js.map +0 -1
  67. 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.2",
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.2",
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": "0b0034e9fa97a5dd852f01249aa5093484040ec9"
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,
@@ -158,7 +161,7 @@ export type AutocompleteProps<
158
161
  getIsOptionEqualToValue?: (option: OptionType, value: OptionType) => boolean;
159
162
  } & Pick<
160
163
  FieldComponentProps,
161
- "errorMessage" | "hint" | "id" | "isOptional" | "name"
164
+ "errorMessage" | "hint" | "id" | "isOptional" | "name" | "isFullWidth"
162
165
  > &
163
166
  SeleniumProps;
164
167
 
@@ -174,6 +177,7 @@ const Autocomplete = <
174
177
  inputValue,
175
178
  isCustomValueAllowed,
176
179
  isDisabled,
180
+ isFullWidth = false,
177
181
  isLoading,
178
182
  isOptional = false,
179
183
  isReadOnly,
@@ -189,6 +193,45 @@ const Autocomplete = <
189
193
  getIsOptionEqualToValue,
190
194
  testId,
191
195
  }: AutocompleteProps<OptionType, HasMultipleChoices, IsCustomValueAllowed>) => {
196
+ const controlledStateRef = useRef(
197
+ getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
198
+ );
199
+ const defaultValueProp = useMemo<
200
+ | AutocompleteValue<
201
+ OptionType,
202
+ HasMultipleChoices,
203
+ undefined,
204
+ IsCustomValueAllowed
205
+ >
206
+ | undefined
207
+ >(() => {
208
+ if (hasMultipleChoices) {
209
+ if (value === undefined) {
210
+ return defaultValue;
211
+ }
212
+ return [] as AutocompleteValue<
213
+ OptionType,
214
+ HasMultipleChoices,
215
+ undefined,
216
+ IsCustomValueAllowed
217
+ >;
218
+ }
219
+ return value === undefined ? defaultValue : undefined;
220
+ }, [defaultValue, hasMultipleChoices, value]);
221
+
222
+ const valueProps = useInputValues({
223
+ defaultValue: defaultValueProp,
224
+ value: value,
225
+ controlState: controlledStateRef.current,
226
+ });
227
+
228
+ const inputValueProp = useMemo(() => {
229
+ if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
230
+ return { inputValue };
231
+ }
232
+ return undefined;
233
+ }, [inputValue]);
234
+
192
235
  const renderInput = useCallback(
193
236
  ({ InputLabelProps, InputProps, ...params }) => (
194
237
  <Field
@@ -223,39 +266,6 @@ const Autocomplete = <
223
266
  ),
224
267
  [errorMessage, hint, isOptional, label, nameOverride]
225
268
  );
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
269
  const onChange = useCallback<
260
270
  NonNullable<
261
271
  UseAutocompleteProps<
@@ -267,10 +277,9 @@ const Autocomplete = <
267
277
  >
268
278
  >(
269
279
  (event, value, reason, details) => {
270
- setLocalValue(value);
271
280
  onChangeProp?.(event, value, reason, details);
272
281
  },
273
- [onChangeProp, setLocalValue]
282
+ [onChangeProp]
274
283
  );
275
284
 
276
285
  const onInputChange = useCallback<
@@ -284,23 +293,24 @@ const Autocomplete = <
284
293
  >
285
294
  >(
286
295
  (event, value, reason) => {
287
- setLocalInputValue(value);
288
296
  onInputChangeProp?.(event, value, reason);
289
297
  },
290
- [onInputChangeProp, setLocalInputValue]
298
+ [onInputChangeProp]
291
299
  );
292
300
 
293
301
  return (
294
302
  <MuiAutocomplete
303
+ {...valueProps}
304
+ {...inputValueProp}
295
305
  // 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
306
  aria-disabled={isDisabled}
297
307
  data-se={testId}
298
- defaultValue={defaultValuesProp}
299
308
  disableCloseOnSelect={hasMultipleChoices}
300
309
  disabled={isDisabled}
301
310
  freeSolo={isCustomValueAllowed}
302
311
  filterSelectedOptions={true}
303
312
  id={idOverride}
313
+ fullWidth={isFullWidth}
304
314
  loading={isLoading}
305
315
  multiple={hasMultipleChoices}
306
316
  onBlur={onBlur}
@@ -310,8 +320,6 @@ const Autocomplete = <
310
320
  options={options}
311
321
  readOnly={isReadOnly}
312
322
  renderInput={renderInput}
313
- value={localValue}
314
- inputValue={localInputValue}
315
323
  isOptionEqualToValue={getIsOptionEqualToValue}
316
324
  />
317
325
  );
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}
package/src/Field.tsx CHANGED
@@ -59,6 +59,10 @@ export type FieldProps = {
59
59
  * If `true`, the component is disabled.
60
60
  */
61
61
  isDisabled?: boolean;
62
+ /**
63
+ * If `true`, the component can stretch to fill the width of the container.
64
+ */
65
+ isFullWidth?: boolean;
62
66
  /**
63
67
  * If `true`, the `input` element is not required.
64
68
  */
@@ -96,6 +100,7 @@ const Field = ({
96
100
  hint,
97
101
  id: idOverride,
98
102
  isDisabled: isDisabledProp = false,
103
+ isFullWidth = false,
99
104
  isRadioGroup = false,
100
105
  isOptional = false,
101
106
  label,
@@ -126,6 +131,7 @@ const Field = ({
126
131
  disabled={isDisabled}
127
132
  error={Boolean(errorMessage)}
128
133
  role={isRadioGroup ? "radiogroup" : undefined}
134
+ fullWidth={isFullWidth}
129
135
  >
130
136
  {fieldType === "group" ? (
131
137
  <MuiFormLabel component="legend">
@@ -27,6 +27,10 @@ export type FieldComponentProps = {
27
27
  * If `true`, the component is disabled.
28
28
  */
29
29
  isDisabled?: boolean;
30
+ /**
31
+ * If `true`, the component can stretch to fill the width of the container.
32
+ */
33
+ isFullWidth?: boolean;
30
34
  /**
31
35
  * If `true`, the `input` element is not required.
32
36
  */
@@ -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,110 @@ 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
- "errorMessage" | "hint" | "id" | "isDisabled" | "isOptional"
84
+ "errorMessage" | "hint" | "id" | "isDisabled" | "isOptional" | "isFullWidth"
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
+ isFullWidth = false,
101
+ isMultiSelect,
74
102
  isOptional = false,
75
103
  label,
76
104
  onBlur,
77
- onChange,
105
+ onChange: onChangeProp,
78
106
  onFocus,
79
107
  testId,
80
108
  value,
81
109
  children,
82
- },
83
- ref
110
+ }: NativeSelectProps<Value, HasMultipleChoices>,
111
+ ref?: React.Ref<ReactElement>
84
112
  ) => {
113
+ const controlledStateRef = useRef(
114
+ getControlState({
115
+ controlledValue: value,
116
+ uncontrolledValue: defaultValue,
117
+ })
118
+ );
119
+ const inputValues = useInputValues({
120
+ defaultValue,
121
+ value,
122
+ controlState: controlledStateRef.current,
123
+ });
124
+
125
+ const onChange = useCallback<
126
+ NonNullable<MuiSelectProps<Value>["onChange"]>
127
+ >(
128
+ (event, child) => {
129
+ onChangeProp?.(event, child);
130
+ },
131
+ [onChangeProp]
132
+ );
133
+
134
+ const hasMultipleChoices = useMemo(
135
+ () =>
136
+ hasMultipleChoicesProp === undefined
137
+ ? isMultiSelect
138
+ : hasMultipleChoicesProp,
139
+ [hasMultipleChoicesProp, isMultiSelect]
140
+ );
85
141
  const renderFieldComponent = useCallback(
86
142
  ({ ariaDescribedBy, errorMessageElementId, labelElementId }) => (
87
143
  <MuiSelect
144
+ {...inputValues}
88
145
  aria-describedby={ariaDescribedBy}
89
146
  children={children}
90
147
  data-se={testId}
91
- defaultValue={defaultValue}
92
148
  id={idOverride}
93
149
  inputProps={{
94
150
  "aria-errormessage": errorMessageElementId,
95
151
  "aria-labelledby": labelElementId,
96
152
  }}
97
153
  name={idOverride}
98
- multiple={isMultiSelect}
154
+ multiple={hasMultipleChoices}
99
155
  native={true}
100
156
  onBlur={onBlur}
101
157
  onChange={onChange}
102
158
  onFocus={onFocus}
103
159
  ref={ref}
104
- value={value}
105
160
  />
106
161
  ),
107
162
  [
108
163
  children,
109
- defaultValue,
110
164
  idOverride,
111
- isMultiSelect,
165
+ inputValues,
166
+ hasMultipleChoices,
112
167
  onBlur,
113
168
  onChange,
114
169
  onFocus,
115
170
  ref,
116
171
  testId,
117
- value,
118
172
  ]
119
173
  );
120
174
 
@@ -126,6 +180,7 @@ const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
126
180
  hint={hint}
127
181
  id={idOverride}
128
182
  isDisabled={isDisabled}
183
+ isFullWidth={isFullWidth}
129
184
  isOptional={isOptional}
130
185
  label={label}
131
186
  renderFieldComponent={renderFieldComponent}
@@ -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,17 +78,19 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
72
78
  (
73
79
  {
74
80
  autoCompleteType,
81
+ defaultValue,
75
82
  errorMessage,
76
83
  hasInitialFocus,
77
84
  hint,
78
85
  id: idOverride,
79
86
  isDisabled = false,
87
+ isFullWidth = false,
80
88
  isOptional = false,
81
89
  hasShowPassword = true,
82
90
  isReadOnly,
83
91
  label,
84
92
  name: nameOverride,
85
- onChange,
93
+ onChange: onChangeProp,
86
94
  onFocus,
87
95
  onBlur,
88
96
  placeholder,
@@ -100,9 +108,31 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
100
108
  );
101
109
  }, []);
102
110
 
111
+ const controlledStateRef = useRef(
112
+ getControlState({
113
+ controlledValue: value,
114
+ uncontrolledValue: defaultValue,
115
+ })
116
+ );
117
+ const inputValues = useInputValues({
118
+ defaultValue,
119
+ value,
120
+ controlState: controlledStateRef.current,
121
+ });
122
+
123
+ const onChange = useCallback<
124
+ ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>
125
+ >(
126
+ (event) => {
127
+ onChangeProp?.(event);
128
+ },
129
+ [onChangeProp]
130
+ );
131
+
103
132
  const renderFieldComponent = useCallback(
104
133
  ({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
105
134
  <InputBase
135
+ {...inputValues}
106
136
  aria-describedby={ariaDescribedBy}
107
137
  autoComplete={inputType === "password" ? autoCompleteType : "off"}
108
138
  /* eslint-disable-next-line jsx-a11y/no-autofocus */
@@ -140,12 +170,12 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
140
170
  ref={ref}
141
171
  required={!isOptional}
142
172
  type={inputType}
143
- value={value}
144
173
  />
145
174
  ),
146
175
  [
147
176
  autoCompleteType,
148
177
  hasInitialFocus,
178
+ inputValues,
149
179
  t,
150
180
  togglePasswordVisibility,
151
181
  inputType,
@@ -159,7 +189,6 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
159
189
  hasShowPassword,
160
190
  ref,
161
191
  testId,
162
- value,
163
192
  ]
164
193
  );
165
194
 
@@ -171,6 +200,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
171
200
  hint={hint}
172
201
  id={idOverride}
173
202
  isDisabled={isDisabled}
203
+ isFullWidth={isFullWidth}
174
204
  isOptional={isOptional}
175
205
  label={label}
176
206
  renderFieldComponent={renderFieldComponent}
@@ -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 (