@okta/odyssey-react-mui 1.7.1 → 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.
- package/CHANGELOG.md +12 -0
- package/dist/@types/react-augment.d.js +2 -0
- package/dist/@types/react-augment.d.js.map +1 -0
- package/dist/Autocomplete.js +32 -23
- package/dist/Autocomplete.js.map +1 -1
- package/dist/Checkbox.js +33 -18
- package/dist/Checkbox.js.map +1 -1
- package/dist/NativeSelect.js +22 -8
- package/dist/NativeSelect.js.map +1 -1
- package/dist/PasswordField.js +19 -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 +17 -16
- package/dist/SearchField.js.map +1 -1
- package/dist/Select.js +34 -18
- package/dist/Select.js.map +1 -1
- package/dist/TextField.js +20 -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 +0 -1
- package/dist/src/Autocomplete.d.ts.map +1 -1
- package/dist/src/Checkbox.d.ts +5 -1
- package/dist/src/Checkbox.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 +10 -2
- package/dist/src/SearchField.d.ts.map +1 -1
- package/dist/src/Select.d.ts +5 -1
- 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 +49 -43
- package/src/Checkbox.tsx +41 -22
- package/src/NativeSelect.tsx +78 -25
- package/src/PasswordField.tsx +32 -4
- package/src/RadioGroup.tsx +12 -10
- package/src/SearchField.tsx +24 -18
- package/src/Select.tsx +43 -25
- package/src/TextField.tsx +33 -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,7 +75,7 @@ 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
81
|
} & Pick<FieldComponentProps, "id" | "isDisabled" | "name"> &
|
|
@@ -80,6 +85,7 @@ 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,
|
|
@@ -91,42 +97,45 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
|
|
|
91
97
|
onClear: onClearProp,
|
|
92
98
|
placeholder,
|
|
93
99
|
testId,
|
|
94
|
-
value
|
|
100
|
+
value,
|
|
95
101
|
},
|
|
96
102
|
ref
|
|
97
103
|
) => {
|
|
98
|
-
const [uncontrolledValue, setUncontrolledValue] = useState("");
|
|
99
|
-
|
|
100
104
|
const onChange: ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> =
|
|
101
105
|
useCallback(
|
|
102
106
|
(event) => {
|
|
103
|
-
setUncontrolledValue(event.currentTarget.value);
|
|
104
107
|
onChangeProp?.(event);
|
|
105
108
|
},
|
|
106
109
|
[onChangeProp]
|
|
107
110
|
);
|
|
108
111
|
|
|
109
112
|
const onClear = useCallback(() => {
|
|
110
|
-
setUncontrolledValue("");
|
|
111
113
|
onClearProp?.();
|
|
112
114
|
}, [onClearProp]);
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const controlledStateRef = useRef(
|
|
117
|
+
getControlState({
|
|
118
|
+
controlledValue: value,
|
|
119
|
+
uncontrolledValue: defaultValue,
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
const inputValues = useInputValues({
|
|
123
|
+
defaultValue,
|
|
124
|
+
value,
|
|
125
|
+
controlState: controlledStateRef.current,
|
|
126
|
+
});
|
|
119
127
|
|
|
120
128
|
const renderFieldComponent = useCallback(
|
|
121
129
|
({ ariaDescribedBy, id }) => (
|
|
122
130
|
<InputBase
|
|
131
|
+
{...inputValues}
|
|
123
132
|
aria-describedby={ariaDescribedBy}
|
|
124
133
|
autoComplete={autoCompleteType}
|
|
125
134
|
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
|
126
135
|
autoFocus={hasInitialFocus}
|
|
127
136
|
data-se={testId}
|
|
128
137
|
endAdornment={
|
|
129
|
-
|
|
138
|
+
defaultValue && (
|
|
130
139
|
<InputAdornment position="end">
|
|
131
140
|
<IconButton
|
|
132
141
|
aria-label="Clear"
|
|
@@ -152,15 +161,13 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
|
|
|
152
161
|
</InputAdornment>
|
|
153
162
|
}
|
|
154
163
|
type="search"
|
|
155
|
-
value={
|
|
156
|
-
controlledValue === undefined ? uncontrolledValue : controlledValue
|
|
157
|
-
}
|
|
158
164
|
/>
|
|
159
165
|
),
|
|
160
166
|
[
|
|
161
167
|
autoCompleteType,
|
|
162
|
-
|
|
168
|
+
defaultValue,
|
|
163
169
|
hasInitialFocus,
|
|
170
|
+
inputValues,
|
|
164
171
|
isDisabled,
|
|
165
172
|
nameOverride,
|
|
166
173
|
onBlur,
|
|
@@ -170,7 +177,6 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
|
|
|
170
177
|
placeholder,
|
|
171
178
|
ref,
|
|
172
179
|
testId,
|
|
173
|
-
uncontrolledValue,
|
|
174
180
|
]
|
|
175
181
|
);
|
|
176
182
|
|
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
|
*/
|
|
@@ -94,10 +103,12 @@ export type SelectProps<
|
|
|
94
103
|
* - { text: string, type: "heading" } — Used to display a group heading with the text
|
|
95
104
|
*/
|
|
96
105
|
|
|
106
|
+
const { CONTROLLED } = ComponentControlledState;
|
|
97
107
|
const Select = <
|
|
98
108
|
Value extends SelectValueType<HasMultipleChoices>,
|
|
99
109
|
HasMultipleChoices extends boolean
|
|
100
110
|
>({
|
|
111
|
+
defaultValue,
|
|
101
112
|
errorMessage,
|
|
102
113
|
hasMultipleChoices: hasMultipleChoicesProp,
|
|
103
114
|
hint,
|
|
@@ -121,32 +132,38 @@ const Select = <
|
|
|
121
132
|
: hasMultipleChoicesProp,
|
|
122
133
|
[hasMultipleChoicesProp, isMultiSelect]
|
|
123
134
|
);
|
|
135
|
+
const controlledStateRef = useRef(
|
|
136
|
+
getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
|
|
137
|
+
);
|
|
138
|
+
const [internalSelectedValues, setInternalSelectedValues] = useState(
|
|
139
|
+
controlledStateRef.current === CONTROLLED ? value : defaultValue
|
|
140
|
+
);
|
|
124
141
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (controlledStateRef.current === CONTROLLED) {
|
|
144
|
+
setInternalSelectedValues(value);
|
|
145
|
+
}
|
|
146
|
+
}, [value]);
|
|
128
147
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
const inputValues = useInputValues({
|
|
149
|
+
defaultValue,
|
|
150
|
+
value,
|
|
151
|
+
controlState: controlledStateRef.current,
|
|
152
|
+
});
|
|
132
153
|
|
|
133
154
|
const onChange = useCallback<NonNullable<MuiSelectProps<Value>["onChange"]>>(
|
|
134
155
|
(event, child) => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
setSelectedValue(valueFromEvent);
|
|
156
|
+
const {
|
|
157
|
+
target: { value },
|
|
158
|
+
} = event;
|
|
159
|
+
if (controlledStateRef.current !== CONTROLLED) {
|
|
160
|
+
setInternalSelectedValues(
|
|
161
|
+
(typeof value === "string" ? value.split(",") : value) as Value
|
|
162
|
+
);
|
|
145
163
|
}
|
|
146
|
-
|
|
147
164
|
onChangeProp?.(event, child);
|
|
148
165
|
},
|
|
149
|
-
[
|
|
166
|
+
[onChangeProp]
|
|
150
167
|
);
|
|
151
168
|
|
|
152
169
|
// Normalize the options array to accommodate the various
|
|
@@ -207,14 +224,15 @@ const Select = <
|
|
|
207
224
|
if (option.type === "heading") {
|
|
208
225
|
return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
|
|
209
226
|
}
|
|
210
|
-
|
|
211
227
|
return (
|
|
212
228
|
<MenuItem key={option.value} value={option.value}>
|
|
213
229
|
{hasMultipleChoices && (
|
|
214
|
-
<MuiCheckbox
|
|
230
|
+
<MuiCheckbox
|
|
231
|
+
checked={internalSelectedValues?.includes(option.value)}
|
|
232
|
+
/>
|
|
215
233
|
)}
|
|
216
234
|
{option.text}
|
|
217
|
-
{
|
|
235
|
+
{internalSelectedValues === option.value && (
|
|
218
236
|
<ListItemSecondaryAction>
|
|
219
237
|
<CheckIcon />
|
|
220
238
|
</ListItemSecondaryAction>
|
|
@@ -222,12 +240,13 @@ const Select = <
|
|
|
222
240
|
</MenuItem>
|
|
223
241
|
);
|
|
224
242
|
}),
|
|
225
|
-
[hasMultipleChoices, normalizedOptions,
|
|
243
|
+
[hasMultipleChoices, normalizedOptions, internalSelectedValues]
|
|
226
244
|
);
|
|
227
245
|
|
|
228
246
|
const renderFieldComponent = useCallback(
|
|
229
247
|
({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
|
|
230
248
|
<MuiSelect
|
|
249
|
+
{...inputValues}
|
|
231
250
|
aria-describedby={ariaDescribedBy}
|
|
232
251
|
aria-errormessage={errorMessageElementId}
|
|
233
252
|
children={children}
|
|
@@ -240,18 +259,17 @@ const Select = <
|
|
|
240
259
|
onChange={onChange}
|
|
241
260
|
onFocus={onFocus}
|
|
242
261
|
renderValue={hasMultipleChoices ? renderValue : undefined}
|
|
243
|
-
value={selectedValue}
|
|
244
262
|
/>
|
|
245
263
|
),
|
|
246
264
|
[
|
|
247
265
|
children,
|
|
266
|
+
inputValues,
|
|
248
267
|
hasMultipleChoices,
|
|
249
268
|
nameOverride,
|
|
250
269
|
onBlur,
|
|
251
270
|
onChange,
|
|
252
271
|
onFocus,
|
|
253
272
|
renderValue,
|
|
254
|
-
selectedValue,
|
|
255
273
|
testId,
|
|
256
274
|
]
|
|
257
275
|
);
|
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,6 +97,7 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
91
97
|
(
|
|
92
98
|
{
|
|
93
99
|
autoCompleteType,
|
|
100
|
+
defaultValue,
|
|
94
101
|
hasInitialFocus,
|
|
95
102
|
endAdornment,
|
|
96
103
|
errorMessage,
|
|
@@ -103,19 +110,41 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
103
110
|
label,
|
|
104
111
|
name: nameOverride,
|
|
105
112
|
onBlur,
|
|
106
|
-
onChange,
|
|
113
|
+
onChange: onChangeProp,
|
|
107
114
|
onFocus,
|
|
108
115
|
placeholder,
|
|
109
116
|
startAdornment,
|
|
110
117
|
testId,
|
|
111
118
|
type = "text",
|
|
112
|
-
value,
|
|
119
|
+
value: value,
|
|
113
120
|
},
|
|
114
121
|
ref
|
|
115
122
|
) => {
|
|
123
|
+
const controlledStateRef = useRef(
|
|
124
|
+
getControlState({
|
|
125
|
+
controlledValue: value,
|
|
126
|
+
uncontrolledValue: defaultValue,
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
const inputValues = useInputValues({
|
|
130
|
+
defaultValue,
|
|
131
|
+
value,
|
|
132
|
+
controlState: controlledStateRef.current,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const onChange = useCallback<
|
|
136
|
+
NonNullable<ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>>
|
|
137
|
+
>(
|
|
138
|
+
(event) => {
|
|
139
|
+
onChangeProp?.(event);
|
|
140
|
+
},
|
|
141
|
+
[onChangeProp]
|
|
142
|
+
);
|
|
143
|
+
|
|
116
144
|
const renderFieldComponent = useCallback(
|
|
117
145
|
({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
|
|
118
146
|
<InputBase
|
|
147
|
+
{...inputValues}
|
|
119
148
|
inputProps={{
|
|
120
149
|
"aria-errormessage": errorMessageElementId,
|
|
121
150
|
"aria-labelledby": labelElementId,
|
|
@@ -146,18 +175,18 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
146
175
|
)
|
|
147
176
|
}
|
|
148
177
|
type={type}
|
|
149
|
-
value={value}
|
|
150
178
|
/>
|
|
151
179
|
),
|
|
152
180
|
[
|
|
153
181
|
autoCompleteType,
|
|
182
|
+
inputValues,
|
|
154
183
|
hasInitialFocus,
|
|
155
184
|
endAdornment,
|
|
156
185
|
isMultiline,
|
|
157
186
|
nameOverride,
|
|
187
|
+
onBlur,
|
|
158
188
|
onChange,
|
|
159
189
|
onFocus,
|
|
160
|
-
onBlur,
|
|
161
190
|
placeholder,
|
|
162
191
|
isOptional,
|
|
163
192
|
isReadOnly,
|
|
@@ -165,7 +194,6 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
165
194
|
startAdornment,
|
|
166
195
|
testId,
|
|
167
196
|
type,
|
|
168
|
-
value,
|
|
169
197
|
]
|
|
170
198
|
);
|
|
171
199
|
|
|
@@ -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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useControlledState.d.ts","sourceRoot":"","sources":["../../src/useControlledState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;;AAIH,KAAK,uBAAuB,CAAC,KAAK,IAAI;IACpC,eAAe,CAAC,EAAE,KAAK,CAAC;IACxB,iBAAiB,CAAC,EAAE,KAAK,CAAC;CAC3B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,+LA2B9B,CAAC"}
|
|
@@ -1,33 +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
|
-
|
|
13
|
-
import { useEffect, useRef, useState } from "react";
|
|
14
|
-
export const useControlledState = _ref => {
|
|
15
|
-
let {
|
|
16
|
-
controlledValue,
|
|
17
|
-
uncontrolledValue
|
|
18
|
-
} = _ref;
|
|
19
|
-
const isControlledMode = useRef(controlledValue !== undefined);
|
|
20
|
-
const [stateValue, setStateValue] = useState(isControlledMode.current ? controlledValue : uncontrolledValue);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (isControlledMode.current) {
|
|
23
|
-
setStateValue(controlledValue);
|
|
24
|
-
}
|
|
25
|
-
}, [controlledValue]);
|
|
26
|
-
const setState = value => {
|
|
27
|
-
if (!isControlledMode.current) {
|
|
28
|
-
setStateValue(value);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
return [stateValue, setState];
|
|
32
|
-
};
|
|
33
|
-
//# sourceMappingURL=useControlledState.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useControlledState.js","names":["useEffect","useRef","useState","useControlledState","_ref","controlledValue","uncontrolledValue","isControlledMode","undefined","stateValue","setStateValue","current","setState","value"],"sources":["../src/useControlledState.ts"],"sourcesContent":["/*!\n * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.\n * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the \"License.\")\n *\n * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *\n * See the License for the specific language governing permissions and limitations under the License.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\ntype UseControlledStateProps<Value> = {\n controlledValue?: Value; // isChecked\n uncontrolledValue?: Value; // isDefaultChecked\n};\n\n/**\n * Use the same way as `useState`. Returns a stateful value, and a function to update it.\n * When `initialState` is passed, the returned function to update it does nothing. This is\n * useful to handle values in components that may be controlled externally when that value is\n * passed in props and thus wish to prevent internal updates of the same value.\n *\n * @param initialState\n * @see https://react.dev/reference/react/useState\n */\nexport const useControlledState = <Value>({\n controlledValue,\n uncontrolledValue,\n}: UseControlledStateProps<Value>) => {\n const isControlledMode = useRef(controlledValue !== undefined);\n const [stateValue, setStateValue] = useState(\n isControlledMode.current ? controlledValue : uncontrolledValue\n );\n\n useEffect(() => {\n if (isControlledMode.current) {\n setStateValue(controlledValue);\n }\n }, [controlledValue]);\n\n const setState: typeof setStateValue = (value) => {\n if (!isControlledMode.current) {\n setStateValue(value);\n }\n };\n\n return [\n stateValue,\n // If `value` is controlled externally, ignore calls to the setter.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n setState,\n ] as const;\n};\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAgBnD,OAAO,MAAMC,kBAAkB,GAAGC,IAAA,IAGI;EAAA,IAHI;IACxCC,eAAe;IACfC;EAC8B,CAAC,GAAAF,IAAA;EAC/B,MAAMG,gBAAgB,GAAGN,MAAM,CAACI,eAAe,KAAKG,SAAS,CAAC;EAC9D,MAAM,CAACC,UAAU,EAAEC,aAAa,CAAC,GAAGR,QAAQ,CAC1CK,gBAAgB,CAACI,OAAO,GAAGN,eAAe,GAAGC,iBAC/C,CAAC;EAEDN,SAAS,CAAC,MAAM;IACd,IAAIO,gBAAgB,CAACI,OAAO,EAAE;MAC5BD,aAAa,CAACL,eAAe,CAAC;IAChC;EACF,CAAC,EAAE,CAACA,eAAe,CAAC,CAAC;EAErB,MAAMO,QAA8B,GAAIC,KAAK,IAAK;IAChD,IAAI,CAACN,gBAAgB,CAACI,OAAO,EAAE;MAC7BD,aAAa,CAACG,KAAK,CAAC;IACtB;EACF,CAAC;EAED,OAAO,CACLJ,UAAU,EAGVG,QAAQ,CACT;AACH,CAAC"}
|