@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
@@ -10,7 +10,6 @@
10
10
  * See the License for the specific language governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { useState, useEffect } from "react";
14
13
  import { InputAdornment, InputBase, IconButton } from "@mui/material";
15
14
  import {
16
15
  ChangeEventHandler,
@@ -19,12 +18,14 @@ import {
19
18
  InputHTMLAttributes,
20
19
  memo,
21
20
  useCallback,
21
+ useRef,
22
22
  } from "react";
23
23
 
24
24
  import { CloseCircleFilledIcon, SearchIcon } from "./icons.generated";
25
25
  import { Field } from "./Field";
26
26
  import { FieldComponentProps } from "./FieldComponentProps";
27
27
  import type { SeleniumProps } from "./SeleniumProps";
28
+ import { getControlState, useInputValues } from "./inputUtils";
28
29
 
29
30
  export type SearchFieldProps = {
30
31
  /**
@@ -33,6 +34,10 @@ export type SearchFieldProps = {
33
34
  * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
34
35
  */
35
36
  autoCompleteType?: InputHTMLAttributes<HTMLInputElement>["autoComplete"];
37
+ /**
38
+ * The value of the `input` element to use when uncontrolled.
39
+ */
40
+ defaultValue?: string;
36
41
  /**
37
42
  * If `true`, the component will receive focus automatically.
38
43
  */
@@ -70,19 +75,21 @@ export type SearchFieldProps = {
70
75
  */
71
76
  placeholder?: string;
72
77
  /**
73
- * The value of the `input` element, required for a controlled component.
78
+ * The value of the `input` element, to use when controlled.
74
79
  */
75
80
  value?: string;
76
- } & Pick<FieldComponentProps, "id" | "isDisabled" | "name"> &
81
+ } & Pick<FieldComponentProps, "id" | "isDisabled" | "name" | "isFullWidth"> &
77
82
  SeleniumProps;
78
83
 
79
84
  const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
80
85
  (
81
86
  {
82
87
  autoCompleteType,
88
+ defaultValue,
83
89
  hasInitialFocus,
84
90
  id: idOverride,
85
91
  isDisabled = false,
92
+ isFullWidth = false,
86
93
  label,
87
94
  name: nameOverride,
88
95
  onChange: onChangeProp,
@@ -91,42 +98,45 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
91
98
  onClear: onClearProp,
92
99
  placeholder,
93
100
  testId,
94
- value: controlledValue,
101
+ value,
95
102
  },
96
103
  ref
97
104
  ) => {
98
- const [uncontrolledValue, setUncontrolledValue] = useState("");
99
-
100
105
  const onChange: ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> =
101
106
  useCallback(
102
107
  (event) => {
103
- setUncontrolledValue(event.currentTarget.value);
104
108
  onChangeProp?.(event);
105
109
  },
106
110
  [onChangeProp]
107
111
  );
108
112
 
109
113
  const onClear = useCallback(() => {
110
- setUncontrolledValue("");
111
114
  onClearProp?.();
112
115
  }, [onClearProp]);
113
116
 
114
- useEffect(() => {
115
- if (controlledValue !== undefined) {
116
- setUncontrolledValue(controlledValue);
117
- }
118
- }, [controlledValue]);
117
+ const controlledStateRef = useRef(
118
+ getControlState({
119
+ controlledValue: value,
120
+ uncontrolledValue: defaultValue,
121
+ })
122
+ );
123
+ const inputValues = useInputValues({
124
+ defaultValue,
125
+ value,
126
+ controlState: controlledStateRef.current,
127
+ });
119
128
 
120
129
  const renderFieldComponent = useCallback(
121
130
  ({ ariaDescribedBy, id }) => (
122
131
  <InputBase
132
+ {...inputValues}
123
133
  aria-describedby={ariaDescribedBy}
124
134
  autoComplete={autoCompleteType}
125
135
  /* eslint-disable-next-line jsx-a11y/no-autofocus */
126
136
  autoFocus={hasInitialFocus}
127
137
  data-se={testId}
128
138
  endAdornment={
129
- uncontrolledValue && (
139
+ defaultValue && (
130
140
  <InputAdornment position="end">
131
141
  <IconButton
132
142
  aria-label="Clear"
@@ -152,15 +162,13 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
152
162
  </InputAdornment>
153
163
  }
154
164
  type="search"
155
- value={
156
- controlledValue === undefined ? uncontrolledValue : controlledValue
157
- }
158
165
  />
159
166
  ),
160
167
  [
161
168
  autoCompleteType,
162
- controlledValue,
169
+ defaultValue,
163
170
  hasInitialFocus,
171
+ inputValues,
164
172
  isDisabled,
165
173
  nameOverride,
166
174
  onBlur,
@@ -170,7 +178,6 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
170
178
  placeholder,
171
179
  ref,
172
180
  testId,
173
- uncontrolledValue,
174
181
  ]
175
182
  );
176
183
 
@@ -180,6 +187,7 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
180
187
  hasVisibleLabel={false}
181
188
  id={idOverride}
182
189
  isDisabled={isDisabled}
190
+ isFullWidth={isFullWidth}
183
191
  isOptional={true}
184
192
  label={label}
185
193
  renderFieldComponent={renderFieldComponent}
package/src/Select.tsx CHANGED
@@ -10,7 +10,7 @@
10
10
  * See the License for the specific language governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { memo, useCallback, useMemo, useState } from "react";
13
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
14
14
  import {
15
15
  Box,
16
16
  Checkbox as MuiCheckbox,
@@ -26,6 +26,11 @@ import { Field } from "./Field";
26
26
  import { FieldComponentProps } from "./FieldComponentProps";
27
27
  import { CheckIcon } from "./icons.generated";
28
28
  import type { SeleniumProps } from "./SeleniumProps";
29
+ import {
30
+ ComponentControlledState,
31
+ useInputValues,
32
+ getControlState,
33
+ } from "./inputUtils";
29
34
 
30
35
  export type SelectOption = {
31
36
  text: string;
@@ -40,6 +45,10 @@ export type SelectProps<
40
45
  Value extends SelectValueType<HasMultipleChoices>,
41
46
  HasMultipleChoices extends boolean
42
47
  > = {
48
+ /**
49
+ * The default value. Use when the component is not controlled.
50
+ */
51
+ defaultValue?: MuiSelectProps<Value>["defaultValue"];
43
52
  /**
44
53
  * If `true`, the Select allows multiple selections
45
54
  */
@@ -75,7 +84,13 @@ export type SelectProps<
75
84
  value?: Value;
76
85
  } & Pick<
77
86
  FieldComponentProps,
78
- "errorMessage" | "hint" | "id" | "isDisabled" | "isOptional" | "name"
87
+ | "errorMessage"
88
+ | "hint"
89
+ | "id"
90
+ | "isDisabled"
91
+ | "isOptional"
92
+ | "name"
93
+ | "isFullWidth"
79
94
  > &
80
95
  SeleniumProps;
81
96
 
@@ -94,15 +109,18 @@ export type SelectProps<
94
109
  * - { text: string, type: "heading" } — Used to display a group heading with the text
95
110
  */
96
111
 
112
+ const { CONTROLLED } = ComponentControlledState;
97
113
  const Select = <
98
114
  Value extends SelectValueType<HasMultipleChoices>,
99
115
  HasMultipleChoices extends boolean
100
116
  >({
117
+ defaultValue,
101
118
  errorMessage,
102
119
  hasMultipleChoices: hasMultipleChoicesProp,
103
120
  hint,
104
121
  id: idOverride,
105
122
  isDisabled = false,
123
+ isFullWidth = false,
106
124
  isMultiSelect,
107
125
  isOptional = false,
108
126
  label,
@@ -121,32 +139,38 @@ const Select = <
121
139
  : hasMultipleChoicesProp,
122
140
  [hasMultipleChoicesProp, isMultiSelect]
123
141
  );
142
+ const controlledStateRef = useRef(
143
+ getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
144
+ );
145
+ const [internalSelectedValues, setInternalSelectedValues] = useState(
146
+ controlledStateRef.current === CONTROLLED ? value : defaultValue
147
+ );
124
148
 
125
- const formattedValueForMultiSelect = isMultiSelect
126
- ? ([] as string[] as Value)
127
- : ("" as string as Value);
149
+ useEffect(() => {
150
+ if (controlledStateRef.current === CONTROLLED) {
151
+ setInternalSelectedValues(value);
152
+ }
153
+ }, [value]);
128
154
 
129
- const [selectedValue, setSelectedValue] = useState(
130
- value === undefined ? formattedValueForMultiSelect : value
131
- );
155
+ const inputValues = useInputValues({
156
+ defaultValue,
157
+ value,
158
+ controlState: controlledStateRef.current,
159
+ });
132
160
 
133
161
  const onChange = useCallback<NonNullable<MuiSelectProps<Value>["onChange"]>>(
134
162
  (event, child) => {
135
- const valueFromEvent = event.target.value;
136
-
137
- if (typeof valueFromEvent === "string") {
138
- if (hasMultipleChoices) {
139
- setSelectedValue(valueFromEvent.split(",") as Value);
140
- } else {
141
- setSelectedValue(valueFromEvent as Value);
142
- }
143
- } else {
144
- setSelectedValue(valueFromEvent);
163
+ const {
164
+ target: { value },
165
+ } = event;
166
+ if (controlledStateRef.current !== CONTROLLED) {
167
+ setInternalSelectedValues(
168
+ (typeof value === "string" ? value.split(",") : value) as Value
169
+ );
145
170
  }
146
-
147
171
  onChangeProp?.(event, child);
148
172
  },
149
- [hasMultipleChoices, onChangeProp, setSelectedValue]
173
+ [onChangeProp]
150
174
  );
151
175
 
152
176
  // Normalize the options array to accommodate the various
@@ -207,14 +231,15 @@ const Select = <
207
231
  if (option.type === "heading") {
208
232
  return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
209
233
  }
210
-
211
234
  return (
212
235
  <MenuItem key={option.value} value={option.value}>
213
236
  {hasMultipleChoices && (
214
- <MuiCheckbox checked={selectedValue.includes(option.value)} />
237
+ <MuiCheckbox
238
+ checked={internalSelectedValues?.includes(option.value)}
239
+ />
215
240
  )}
216
241
  {option.text}
217
- {selectedValue == option.value && (
242
+ {internalSelectedValues === option.value && (
218
243
  <ListItemSecondaryAction>
219
244
  <CheckIcon />
220
245
  </ListItemSecondaryAction>
@@ -222,12 +247,13 @@ const Select = <
222
247
  </MenuItem>
223
248
  );
224
249
  }),
225
- [hasMultipleChoices, normalizedOptions, selectedValue]
250
+ [hasMultipleChoices, normalizedOptions, internalSelectedValues]
226
251
  );
227
252
 
228
253
  const renderFieldComponent = useCallback(
229
254
  ({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
230
255
  <MuiSelect
256
+ {...inputValues}
231
257
  aria-describedby={ariaDescribedBy}
232
258
  aria-errormessage={errorMessageElementId}
233
259
  children={children}
@@ -240,18 +266,17 @@ const Select = <
240
266
  onChange={onChange}
241
267
  onFocus={onFocus}
242
268
  renderValue={hasMultipleChoices ? renderValue : undefined}
243
- value={selectedValue}
244
269
  />
245
270
  ),
246
271
  [
247
272
  children,
273
+ inputValues,
248
274
  hasMultipleChoices,
249
275
  nameOverride,
250
276
  onBlur,
251
277
  onChange,
252
278
  onFocus,
253
279
  renderValue,
254
- selectedValue,
255
280
  testId,
256
281
  ]
257
282
  );
@@ -264,6 +289,7 @@ const Select = <
264
289
  hint={hint}
265
290
  id={idOverride}
266
291
  isDisabled={isDisabled}
292
+ isFullWidth={isFullWidth}
267
293
  isOptional={isOptional}
268
294
  label={label}
269
295
  renderFieldComponent={renderFieldComponent}
package/src/TextField.tsx CHANGED
@@ -18,12 +18,14 @@ import {
18
18
  memo,
19
19
  ReactElement,
20
20
  useCallback,
21
+ useRef,
21
22
  } from "react";
22
23
  import { InputAdornment, InputBase } from "@mui/material";
23
24
 
24
25
  import { FieldComponentProps } from "./FieldComponentProps";
25
26
  import { Field } from "./Field";
26
27
  import { SeleniumProps } from "./SeleniumProps";
28
+ import { useInputValues, getControlState } from "./inputUtils";
27
29
 
28
30
  export const textFieldTypeValues = [
29
31
  "email",
@@ -40,6 +42,10 @@ export type TextFieldProps = {
40
42
  * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
41
43
  */
42
44
  autoCompleteType?: InputHTMLAttributes<HTMLInputElement>["autoComplete"];
45
+ /**
46
+ * The default value. Use when the component is not controlled.
47
+ */
48
+ defaultValue?: string;
43
49
  /**
44
50
  * End `InputAdornment` for this component.
45
51
  */
@@ -91,31 +97,55 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
91
97
  (
92
98
  {
93
99
  autoCompleteType,
100
+ defaultValue,
94
101
  hasInitialFocus,
95
102
  endAdornment,
96
103
  errorMessage,
97
104
  hint,
98
105
  id: idOverride,
99
106
  isDisabled = false,
107
+ isFullWidth = false,
100
108
  isMultiline = false,
101
109
  isOptional = false,
102
110
  isReadOnly,
103
111
  label,
104
112
  name: nameOverride,
105
113
  onBlur,
106
- onChange,
114
+ onChange: onChangeProp,
107
115
  onFocus,
108
116
  placeholder,
109
117
  startAdornment,
110
118
  testId,
111
119
  type = "text",
112
- value,
120
+ value: value,
113
121
  },
114
122
  ref
115
123
  ) => {
124
+ const controlledStateRef = useRef(
125
+ getControlState({
126
+ controlledValue: value,
127
+ uncontrolledValue: defaultValue,
128
+ })
129
+ );
130
+ const inputValues = useInputValues({
131
+ defaultValue,
132
+ value,
133
+ controlState: controlledStateRef.current,
134
+ });
135
+
136
+ const onChange = useCallback<
137
+ NonNullable<ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>>
138
+ >(
139
+ (event) => {
140
+ onChangeProp?.(event);
141
+ },
142
+ [onChangeProp]
143
+ );
144
+
116
145
  const renderFieldComponent = useCallback(
117
146
  ({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
118
147
  <InputBase
148
+ {...inputValues}
119
149
  inputProps={{
120
150
  "aria-errormessage": errorMessageElementId,
121
151
  "aria-labelledby": labelElementId,
@@ -146,18 +176,18 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
146
176
  )
147
177
  }
148
178
  type={type}
149
- value={value}
150
179
  />
151
180
  ),
152
181
  [
153
182
  autoCompleteType,
183
+ inputValues,
154
184
  hasInitialFocus,
155
185
  endAdornment,
156
186
  isMultiline,
157
187
  nameOverride,
188
+ onBlur,
158
189
  onChange,
159
190
  onFocus,
160
- onBlur,
161
191
  placeholder,
162
192
  isOptional,
163
193
  isReadOnly,
@@ -165,7 +195,6 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
165
195
  startAdornment,
166
196
  testId,
167
197
  type,
168
- value,
169
198
  ]
170
199
  );
171
200
 
@@ -177,6 +206,7 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
177
206
  hint={hint}
178
207
  id={idOverride}
179
208
  isDisabled={isDisabled}
209
+ isFullWidth={isFullWidth}
180
210
  isOptional={isOptional}
181
211
  label={label}
182
212
  renderFieldComponent={renderFieldComponent}
@@ -0,0 +1,76 @@
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 { useMemo } from "react";
14
+
15
+ type UseControlledStateProps<Value> = {
16
+ controlledValue?: Value;
17
+ uncontrolledValue?: Value;
18
+ };
19
+
20
+ export const ComponentControlledState = {
21
+ CONTROLLED: "CONTROLLED",
22
+ UNCONTROLLED: "UNCONTROLLED",
23
+ };
24
+
25
+ export type ModeType = keyof typeof ComponentControlledState;
26
+ export type ModeTypeValue = (typeof ComponentControlledState)[ModeType];
27
+
28
+ export const getControlState = <Value>({
29
+ controlledValue,
30
+ uncontrolledValue,
31
+ }: UseControlledStateProps<Value>): ModeTypeValue => {
32
+ if (uncontrolledValue !== undefined || controlledValue === undefined) {
33
+ return ComponentControlledState.UNCONTROLLED;
34
+ }
35
+ return ComponentControlledState.CONTROLLED;
36
+ };
37
+
38
+ type InputValueProps<Value> = {
39
+ defaultValue?: Value;
40
+ value?: Value;
41
+ controlState: ModeTypeValue;
42
+ };
43
+
44
+ type InputValue<Value> =
45
+ | {
46
+ defaultValue: Value | undefined;
47
+ value?: undefined;
48
+ }
49
+ | {
50
+ value: Value | undefined;
51
+ defaultValue?: undefined;
52
+ };
53
+
54
+ /**
55
+ * In components that support being used in a controlled or uncontrolled way, the defaultValue and value props need
56
+ * to be suppled values in a mutually exclusive way.
57
+ * If a `value` is being provided to the component, then it is being used in a controlled manner and `defaultValue` needs to be undefined.
58
+ * If `value` is undefined, then that means the component is being used in an uncontrolled way and `defaultValue` is either Value or undefined.
59
+ * This helper helps ensure this mutual exclusivity between the 2 props so the component can operate as expected.
60
+ *
61
+ * @param {InputValueProps<Value>}: { defaultValue: Value | undefined, value: Value | undefined }
62
+ * @returns {InputValue<Value>}: { defaultValue: Value | undefined, value?: undefined } | { defaultValue?: undefined, value: Value }
63
+ */
64
+ export const useInputValues = <Value>({
65
+ defaultValue,
66
+ value,
67
+ controlState,
68
+ }: InputValueProps<Value>): InputValue<Value> => {
69
+ const inputValues = useMemo(() => {
70
+ if (controlState === ComponentControlledState.CONTROLLED) {
71
+ return { value };
72
+ }
73
+ return { defaultValue };
74
+ }, [defaultValue, value]);
75
+ return inputValues;
76
+ };
@@ -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
+ getControlState,
28
+ useInputValues,
29
+ } from "../inputUtils";
26
30
 
27
31
  export type AutocompleteProps<
28
32
  OptionType,
@@ -196,6 +200,44 @@ const VirtualizedAutocomplete = <
196
200
  getIsOptionEqualToValue,
197
201
  testId,
198
202
  }: AutocompleteProps<OptionType, HasMultipleChoices, IsCustomValueAllowed>) => {
203
+ const controlledStateRef = useRef(
204
+ getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
205
+ );
206
+ const defaultValueProp = useMemo<
207
+ | AutocompleteValue<
208
+ OptionType,
209
+ HasMultipleChoices,
210
+ undefined,
211
+ IsCustomValueAllowed
212
+ >
213
+ | undefined
214
+ >(() => {
215
+ if (hasMultipleChoices) {
216
+ return defaultValue === undefined
217
+ ? ([] as AutocompleteValue<
218
+ OptionType,
219
+ HasMultipleChoices,
220
+ undefined,
221
+ IsCustomValueAllowed
222
+ >)
223
+ : defaultValue;
224
+ }
225
+ return defaultValue ?? undefined;
226
+ }, [defaultValue, hasMultipleChoices]);
227
+
228
+ const valueProps = useInputValues({
229
+ defaultValue: defaultValueProp,
230
+ value: value,
231
+ controlState: controlledStateRef.current,
232
+ });
233
+
234
+ const inputValueProp = useMemo(() => {
235
+ if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
236
+ return { inputValue };
237
+ }
238
+ return undefined;
239
+ }, [inputValue]);
240
+
199
241
  const renderInput = useCallback(
200
242
  ({ InputLabelProps, InputProps, ...params }) => (
201
243
  <Field
@@ -230,39 +272,6 @@ const VirtualizedAutocomplete = <
230
272
  ),
231
273
  [errorMessage, hint, isOptional, label, nameOverride]
232
274
  );
233
-
234
- const defaultValuesProp = useMemo<
235
- | AutocompleteValue<
236
- OptionType,
237
- HasMultipleChoices,
238
- undefined,
239
- IsCustomValueAllowed
240
- >
241
- | undefined
242
- >(() => {
243
- if (hasMultipleChoices) {
244
- return defaultValue === undefined
245
- ? ([] as AutocompleteValue<
246
- OptionType,
247
- HasMultipleChoices,
248
- undefined,
249
- IsCustomValueAllowed
250
- >)
251
- : defaultValue;
252
- }
253
- return defaultValue ?? undefined;
254
- }, [defaultValue, hasMultipleChoices]);
255
-
256
- const [localValue, setLocalValue] = useControlledState({
257
- controlledValue: value,
258
- uncontrolledValue: defaultValuesProp,
259
- });
260
-
261
- const [localInputValue, setLocalInputValue] = useControlledState({
262
- controlledValue: inputValue,
263
- uncontrolledValue: undefined,
264
- });
265
-
266
275
  const onChange = useCallback<
267
276
  NonNullable<
268
277
  UseAutocompleteProps<
@@ -274,10 +283,9 @@ const VirtualizedAutocomplete = <
274
283
  >
275
284
  >(
276
285
  (event, value, reason, details) => {
277
- setLocalValue(value);
278
286
  onChangeProp?.(event, value, reason, details);
279
287
  },
280
- [onChangeProp, setLocalValue]
288
+ [onChangeProp]
281
289
  );
282
290
 
283
291
  const onInputChange = useCallback<
@@ -291,18 +299,18 @@ const VirtualizedAutocomplete = <
291
299
  >
292
300
  >(
293
301
  (event, value, reason) => {
294
- setLocalInputValue(value);
295
302
  onInputChangeProp?.(event, value, reason);
296
303
  },
297
- [onInputChangeProp, setLocalInputValue]
304
+ [onInputChangeProp]
298
305
  );
299
306
 
300
307
  return (
301
308
  <MuiAutocomplete
309
+ {...valueProps}
310
+ {...inputValueProp}
302
311
  // AutoComplete is wrapped in a div within MUI which does not get the disabled attr. So this aria-disabled gets set in the div
303
312
  aria-disabled={isDisabled}
304
313
  data-se={testId}
305
- defaultValue={defaultValuesProp}
306
314
  disableCloseOnSelect={hasMultipleChoices}
307
315
  disabled={isDisabled}
308
316
  freeSolo={isCustomValueAllowed}
@@ -318,8 +326,6 @@ const VirtualizedAutocomplete = <
318
326
  options={options}
319
327
  readOnly={isReadOnly}
320
328
  renderInput={renderInput}
321
- value={localValue}
322
- inputValue={localInputValue}
323
329
  isOptionEqualToValue={getIsOptionEqualToValue}
324
330
  />
325
331
  );
@@ -1,28 +0,0 @@
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
- /// <reference types="react" />
13
- type UseControlledStateProps<Value> = {
14
- controlledValue?: Value;
15
- uncontrolledValue?: Value;
16
- };
17
- /**
18
- * Use the same way as `useState`. Returns a stateful value, and a function to update it.
19
- * When `initialState` is passed, the returned function to update it does nothing. This is
20
- * useful to handle values in components that may be controlled externally when that value is
21
- * passed in props and thus wish to prevent internal updates of the same value.
22
- *
23
- * @param initialState
24
- * @see https://react.dev/reference/react/useState
25
- */
26
- export declare const useControlledState: <Value>({ controlledValue, uncontrolledValue, }: UseControlledStateProps<Value>) => readonly [Value | undefined, import("react").Dispatch<import("react").SetStateAction<Value | undefined>>];
27
- export {};
28
- //# sourceMappingURL=useControlledState.d.ts.map