@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.
- package/CHANGELOG.md +10 -0
- package/dist/@types/react-augment.d.js +2 -0
- package/dist/@types/react-augment.d.js.map +1 -0
- package/dist/Autocomplete.js +34 -23
- package/dist/Autocomplete.js.map +1 -1
- package/dist/Checkbox.js +16 -7
- package/dist/Checkbox.js.map +1 -1
- package/dist/Field.js +2 -0
- package/dist/Field.js.map +1 -1
- package/dist/FieldComponentProps.js.map +1 -1
- package/dist/NativeSelect.js +24 -8
- package/dist/NativeSelect.js.map +1 -1
- package/dist/PasswordField.js +21 -5
- package/dist/PasswordField.js.map +1 -1
- package/dist/RadioGroup.js +11 -8
- package/dist/RadioGroup.js.map +1 -1
- package/dist/SearchField.js +19 -16
- package/dist/SearchField.js.map +1 -1
- package/dist/Select.js +36 -18
- package/dist/Select.js.map +1 -1
- package/dist/TextField.js +22 -6
- package/dist/TextField.js.map +1 -1
- package/dist/inputUtils.js +46 -0
- package/dist/inputUtils.js.map +1 -0
- package/dist/labs/VirtualizedAutocomplete.js +29 -23
- package/dist/labs/VirtualizedAutocomplete.js.map +1 -1
- package/dist/src/Autocomplete.d.ts +2 -3
- package/dist/src/Autocomplete.d.ts.map +1 -1
- package/dist/src/Checkbox.d.ts.map +1 -1
- package/dist/src/Field.d.ts +5 -1
- package/dist/src/Field.d.ts.map +1 -1
- package/dist/src/FieldComponentProps.d.ts +4 -0
- package/dist/src/FieldComponentProps.d.ts.map +1 -1
- package/dist/src/NativeSelect.d.ts +19 -44
- package/dist/src/NativeSelect.d.ts.map +1 -1
- package/dist/src/PasswordField.d.ts +10 -2
- package/dist/src/PasswordField.d.ts.map +1 -1
- package/dist/src/RadioGroup.d.ts.map +1 -1
- package/dist/src/SearchField.d.ts +12 -4
- package/dist/src/SearchField.d.ts.map +1 -1
- package/dist/src/Select.d.ts +6 -2
- package/dist/src/Select.d.ts.map +1 -1
- package/dist/src/TextField.d.ts +8 -0
- package/dist/src/TextField.d.ts.map +1 -1
- package/dist/src/inputUtils.d.ts +47 -0
- package/dist/src/inputUtils.d.ts.map +1 -0
- package/dist/src/labs/VirtualizedAutocomplete.d.ts.map +1 -1
- package/dist/tsconfig.production.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/@types/react-augment.d.ts +19 -0
- package/src/Autocomplete.tsx +52 -44
- package/src/Checkbox.tsx +16 -9
- package/src/Field.tsx +6 -0
- package/src/FieldComponentProps.ts +4 -0
- package/src/NativeSelect.tsx +81 -26
- package/src/PasswordField.tsx +34 -4
- package/src/RadioGroup.tsx +12 -10
- package/src/SearchField.tsx +27 -19
- package/src/Select.tsx +52 -26
- package/src/TextField.tsx +35 -5
- package/src/inputUtils.ts +76 -0
- package/src/labs/VirtualizedAutocomplete.tsx +48 -42
- package/dist/src/useControlledState.d.ts +0 -28
- package/dist/src/useControlledState.d.ts.map +0 -1
- package/dist/useControlledState.js +0 -33
- package/dist/useControlledState.js.map +0 -1
- package/src/useControlledState.ts +0 -56
package/src/SearchField.tsx
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (controlledStateRef.current === CONTROLLED) {
|
|
151
|
+
setInternalSelectedValues(value);
|
|
152
|
+
}
|
|
153
|
+
}, [value]);
|
|
128
154
|
|
|
129
|
-
const
|
|
130
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
[
|
|
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
|
|
237
|
+
<MuiCheckbox
|
|
238
|
+
checked={internalSelectedValues?.includes(option.value)}
|
|
239
|
+
/>
|
|
215
240
|
)}
|
|
216
241
|
{option.text}
|
|
217
|
-
{
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
|
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
|