@okta/odyssey-react-mui 1.1.1 → 1.2.0
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 +22 -0
- package/dist/Autocomplete.js +12 -1
- package/dist/Autocomplete.js.map +1 -1
- package/dist/OdysseyCacheProvider.js +4 -1
- package/dist/OdysseyCacheProvider.js.map +1 -1
- package/dist/OdysseyProvider.js +5 -1
- package/dist/OdysseyProvider.js.map +1 -1
- package/dist/OdysseyThemeProvider.js +5 -1
- package/dist/OdysseyThemeProvider.js.map +1 -1
- package/dist/OdysseyTranslationProvider.js +1 -1
- package/dist/OdysseyTranslationProvider.js.map +1 -1
- package/dist/PasswordField.js +11 -3
- package/dist/PasswordField.js.map +1 -1
- package/dist/Select.js +34 -33
- package/dist/Select.js.map +1 -1
- package/dist/Typography.js +0 -22
- package/dist/Typography.js.map +1 -1
- package/dist/createShadowDom.js +26 -0
- package/dist/createShadowDom.js.map +1 -0
- package/dist/{OdysseyI18n.js → i18n.js} +3 -2
- package/dist/i18n.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/labs/datePickerTheme.js +4 -2
- package/dist/labs/datePickerTheme.js.map +1 -1
- package/dist/properties/ts/odyssey-react-mui.js +2 -0
- package/dist/properties/ts/odyssey-react-mui.js.map +1 -1
- package/dist/src/Autocomplete.d.ts +23 -3
- package/dist/src/Autocomplete.d.ts.map +1 -1
- package/dist/src/OdysseyCacheProvider.d.ts +6 -1
- package/dist/src/OdysseyCacheProvider.d.ts.map +1 -1
- package/dist/src/OdysseyProvider.d.ts +1 -1
- package/dist/src/OdysseyProvider.d.ts.map +1 -1
- package/dist/src/OdysseyThemeProvider.d.ts +2 -1
- package/dist/src/OdysseyThemeProvider.d.ts.map +1 -1
- package/dist/src/OdysseyTranslationProvider.d.ts +1 -1
- package/dist/src/OdysseyTranslationProvider.d.ts.map +1 -1
- package/dist/src/PasswordField.d.ts +8 -0
- package/dist/src/PasswordField.d.ts.map +1 -1
- package/dist/src/Select.d.ts +1 -54
- package/dist/src/Select.d.ts.map +1 -1
- package/dist/src/Typography.d.ts +11 -15
- package/dist/src/Typography.d.ts.map +1 -1
- package/dist/src/createShadowDom.d.ts +16 -0
- package/dist/src/createShadowDom.d.ts.map +1 -0
- package/dist/src/{OdysseyI18n.d.ts → i18n.d.ts} +5 -2
- package/dist/src/i18n.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/properties/ts/odyssey-react-mui.d.ts +2 -0
- package/dist/src/properties/ts/odyssey-react-mui.d.ts.map +1 -1
- package/dist/src/theme/components.d.ts +4 -1
- package/dist/src/theme/components.d.ts.map +1 -1
- package/dist/src/theme/createOdysseyMuiTheme.d.ts +23 -0
- package/dist/src/theme/createOdysseyMuiTheme.d.ts.map +1 -0
- package/dist/src/theme/mixins.d.ts +3 -1
- package/dist/src/theme/mixins.d.ts.map +1 -1
- package/dist/src/theme/palette.d.ts +3 -1
- package/dist/src/theme/palette.d.ts.map +1 -1
- package/dist/src/theme/shape.d.ts +3 -1
- package/dist/src/theme/shape.d.ts.map +1 -1
- package/dist/src/theme/spacing.d.ts +3 -1
- package/dist/src/theme/spacing.d.ts.map +1 -1
- package/dist/src/theme/theme.d.ts +1 -8
- package/dist/src/theme/theme.d.ts.map +1 -1
- package/dist/src/theme/typography.d.ts +3 -1
- package/dist/src/theme/typography.d.ts.map +1 -1
- package/dist/theme/components.js +80 -63
- package/dist/theme/components.js.map +1 -1
- package/dist/theme/createOdysseyMuiTheme.js +51 -0
- package/dist/theme/createOdysseyMuiTheme.js.map +1 -0
- package/dist/theme/mixins.js +4 -1
- package/dist/theme/mixins.js.map +1 -1
- package/dist/theme/palette.js +4 -1
- package/dist/theme/palette.js.map +1 -1
- package/dist/theme/shape.js +4 -1
- package/dist/theme/shape.js.map +1 -1
- package/dist/theme/spacing.js +4 -1
- package/dist/theme/spacing.js.map +1 -1
- package/dist/theme/theme.js +1 -20
- package/dist/theme/theme.js.map +1 -1
- package/dist/theme/typography.js +4 -1
- package/dist/theme/typography.js.map +1 -1
- package/dist/tsconfig.production.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/Autocomplete.tsx +44 -3
- package/src/OdysseyCacheProvider.tsx +9 -1
- package/src/OdysseyProvider.tsx +9 -2
- package/src/OdysseyThemeProvider.tsx +8 -2
- package/src/OdysseyTranslationProvider.test.tsx +2 -2
- package/src/OdysseyTranslationProvider.tsx +1 -1
- package/src/PasswordField.tsx +24 -8
- package/src/Select.tsx +147 -152
- package/src/Typography.tsx +0 -26
- package/src/createShadowDom.ts +46 -0
- package/src/{OdysseyI18n.ts → i18n.ts} +2 -2
- package/src/index.ts +1 -0
- package/src/labs/datePickerTheme.tsx +2 -2
- package/src/properties/odyssey-react-mui.properties +2 -0
- package/src/properties/ts/odyssey-react-mui.ts +1 -1
- package/src/theme/components.tsx +26 -9
- package/src/theme/createOdysseyMuiTheme.ts +47 -0
- package/src/theme/mixins.ts +5 -1
- package/src/theme/palette.ts +5 -3
- package/src/theme/shape.ts +5 -1
- package/src/theme/spacing.ts +5 -3
- package/src/theme/theme.ts +1 -26
- package/src/theme/typography.ts +5 -3
- package/dist/OdysseyI18n.js.map +0 -1
- package/dist/src/OdysseyI18n.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okta/odyssey-react-mui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "React MUI components for Odyssey, Okta's design system",
|
|
5
5
|
"author": "Okta, Inc.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -51,9 +51,9 @@
|
|
|
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.
|
|
54
|
+
"@okta/odyssey-design-tokens": "1.2.0",
|
|
55
55
|
"date-fns": "^2.30.0",
|
|
56
|
-
"i18next": "^
|
|
56
|
+
"i18next": "^23.5.1",
|
|
57
57
|
"material-react-table": "^1.14.0",
|
|
58
58
|
"react-i18next": "^12.2.2",
|
|
59
59
|
"ts-node": "^10.9.1",
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"react": ">=17 <19",
|
|
64
64
|
"react-dom": ">=17 <19"
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "6dc7afd2fce73ec25b0566b0c59aeedf0738b925"
|
|
67
67
|
}
|
package/src/Autocomplete.tsx
CHANGED
|
@@ -25,6 +25,10 @@ export type AutocompleteProps<
|
|
|
25
25
|
HasMultipleChoices extends boolean | undefined,
|
|
26
26
|
IsCustomValueAllowed extends boolean | undefined
|
|
27
27
|
> = {
|
|
28
|
+
/**
|
|
29
|
+
* The error message for the Select
|
|
30
|
+
*/
|
|
31
|
+
errorMessage?: string;
|
|
28
32
|
/**
|
|
29
33
|
* Enables multiple choice selection
|
|
30
34
|
*/
|
|
@@ -38,6 +42,10 @@ export type AutocompleteProps<
|
|
|
38
42
|
* The hint text for the Autocomplete input
|
|
39
43
|
*/
|
|
40
44
|
hint?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The id attribute of the Select
|
|
47
|
+
*/
|
|
48
|
+
id?: string;
|
|
41
49
|
/**
|
|
42
50
|
* Allows the input of custom values
|
|
43
51
|
*/
|
|
@@ -83,7 +91,20 @@ export type AutocompleteProps<
|
|
|
83
91
|
*/
|
|
84
92
|
label: string;
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
94
|
+
* The name of the `input` element. Defaults to the `id` if not set.
|
|
95
|
+
*/
|
|
96
|
+
name?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Callback fired when the autocomplete loses focus.
|
|
99
|
+
*/
|
|
100
|
+
onBlur?: MuiAutocompleteProps<
|
|
101
|
+
OptionType,
|
|
102
|
+
HasMultipleChoices,
|
|
103
|
+
undefined,
|
|
104
|
+
IsCustomValueAllowed
|
|
105
|
+
>["onBlur"];
|
|
106
|
+
/**
|
|
107
|
+
* Callback fired when a selection is made.
|
|
87
108
|
*/
|
|
88
109
|
onChange?: MuiAutocompleteProps<
|
|
89
110
|
OptionType,
|
|
@@ -92,7 +113,7 @@ export type AutocompleteProps<
|
|
|
92
113
|
IsCustomValueAllowed
|
|
93
114
|
>["onChange"];
|
|
94
115
|
/**
|
|
95
|
-
* Callback fired when the
|
|
116
|
+
* Callback fired when the textbox receives typed characters.
|
|
96
117
|
*/
|
|
97
118
|
onInputChange?: MuiAutocompleteProps<
|
|
98
119
|
OptionType,
|
|
@@ -100,6 +121,15 @@ export type AutocompleteProps<
|
|
|
100
121
|
undefined,
|
|
101
122
|
IsCustomValueAllowed
|
|
102
123
|
>["onInputChange"];
|
|
124
|
+
/**
|
|
125
|
+
* Callback fired when the autocomplete gains focus.
|
|
126
|
+
*/
|
|
127
|
+
onFocus?: MuiAutocompleteProps<
|
|
128
|
+
OptionType,
|
|
129
|
+
HasMultipleChoices,
|
|
130
|
+
undefined,
|
|
131
|
+
IsCustomValueAllowed
|
|
132
|
+
>["onFocus"];
|
|
103
133
|
/**
|
|
104
134
|
* The options for the Autocomplete input
|
|
105
135
|
*/
|
|
@@ -125,7 +155,9 @@ const Autocomplete = <
|
|
|
125
155
|
HasMultipleChoices extends boolean | undefined,
|
|
126
156
|
IsCustomValueAllowed extends boolean | undefined
|
|
127
157
|
>({
|
|
158
|
+
errorMessage,
|
|
128
159
|
hasMultipleChoices,
|
|
160
|
+
id: idOverride,
|
|
129
161
|
isCustomValueAllowed,
|
|
130
162
|
isDisabled,
|
|
131
163
|
isLoading,
|
|
@@ -133,8 +165,11 @@ const Autocomplete = <
|
|
|
133
165
|
isReadOnly,
|
|
134
166
|
hint,
|
|
135
167
|
label,
|
|
168
|
+
name: nameOverride,
|
|
169
|
+
onBlur,
|
|
136
170
|
onChange,
|
|
137
171
|
onInputChange,
|
|
172
|
+
onFocus,
|
|
138
173
|
options,
|
|
139
174
|
value,
|
|
140
175
|
testId,
|
|
@@ -142,6 +177,7 @@ const Autocomplete = <
|
|
|
142
177
|
const renderInput = useCallback(
|
|
143
178
|
({ InputLabelProps, InputProps, ...params }) => (
|
|
144
179
|
<Field
|
|
180
|
+
errorMessage={errorMessage}
|
|
145
181
|
fieldType="single"
|
|
146
182
|
hasVisibleLabel
|
|
147
183
|
id={InputLabelProps.htmlFor}
|
|
@@ -154,12 +190,13 @@ const Autocomplete = <
|
|
|
154
190
|
{...InputProps}
|
|
155
191
|
aria-describedby={ariaDescribedBy}
|
|
156
192
|
id={id}
|
|
193
|
+
name={nameOverride ?? id}
|
|
157
194
|
required={!isOptional}
|
|
158
195
|
/>
|
|
159
196
|
)}
|
|
160
197
|
/>
|
|
161
198
|
),
|
|
162
|
-
[hint, isOptional, label]
|
|
199
|
+
[errorMessage, hint, isOptional, label, nameOverride]
|
|
163
200
|
);
|
|
164
201
|
|
|
165
202
|
return (
|
|
@@ -170,10 +207,14 @@ const Autocomplete = <
|
|
|
170
207
|
disableCloseOnSelect={hasMultipleChoices}
|
|
171
208
|
disabled={isDisabled}
|
|
172
209
|
freeSolo={isCustomValueAllowed}
|
|
210
|
+
filterSelectedOptions={true}
|
|
211
|
+
id={idOverride}
|
|
173
212
|
loading={isLoading}
|
|
174
213
|
multiple={hasMultipleChoices}
|
|
214
|
+
onBlur={onBlur}
|
|
175
215
|
onChange={onChange}
|
|
176
216
|
onInputChange={onInputChange}
|
|
217
|
+
onFocus={onFocus}
|
|
177
218
|
options={options}
|
|
178
219
|
readOnly={isReadOnly}
|
|
179
220
|
renderInput={renderInput}
|
|
@@ -24,12 +24,18 @@ import { useUniqueAlphabeticalId } from "./useUniqueAlphabeticalId";
|
|
|
24
24
|
|
|
25
25
|
export type OdysseyCacheProviderProps = {
|
|
26
26
|
children: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Emotion renders into this HTML element.
|
|
29
|
+
* When enabling this prop, Emotion renders at the top of this component rather than the bottom like it does in the HTML `<head>`.
|
|
30
|
+
*/
|
|
31
|
+
emotionRootElement?: HTMLStyleElement;
|
|
27
32
|
nonce?: string;
|
|
28
33
|
stylisPlugins?: StylisPlugin[];
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
const OdysseyCacheProvider = ({
|
|
32
37
|
children,
|
|
38
|
+
emotionRootElement,
|
|
33
39
|
nonce,
|
|
34
40
|
stylisPlugins,
|
|
35
41
|
}: OdysseyCacheProviderProps) => {
|
|
@@ -38,11 +44,13 @@ const OdysseyCacheProvider = ({
|
|
|
38
44
|
const emotionCache = useMemo(
|
|
39
45
|
() =>
|
|
40
46
|
createCache({
|
|
47
|
+
container: emotionRootElement,
|
|
41
48
|
key: uniqueAlphabeticalId,
|
|
42
49
|
nonce: nonce || window.cspNonce,
|
|
50
|
+
prepend: Boolean(emotionRootElement),
|
|
43
51
|
stylisPlugins,
|
|
44
52
|
}),
|
|
45
|
-
[nonce, stylisPlugins, uniqueAlphabeticalId]
|
|
53
|
+
[emotionRootElement, nonce, stylisPlugins, uniqueAlphabeticalId]
|
|
46
54
|
);
|
|
47
55
|
|
|
48
56
|
return <CacheProvider value={emotionCache}>{children}</CacheProvider>;
|
package/src/OdysseyProvider.tsx
CHANGED
|
@@ -35,16 +35,23 @@ export type OdysseyProviderProps = OdysseyCacheProviderProps &
|
|
|
35
35
|
const OdysseyProvider = ({
|
|
36
36
|
children,
|
|
37
37
|
designTokensOverride,
|
|
38
|
+
emotionRootElement,
|
|
39
|
+
shadowRootElement,
|
|
38
40
|
languageCode,
|
|
39
41
|
nonce,
|
|
40
42
|
stylisPlugins,
|
|
41
43
|
themeOverride,
|
|
42
44
|
translationOverrides,
|
|
43
45
|
}: OdysseyProviderProps) => (
|
|
44
|
-
<OdysseyCacheProvider
|
|
46
|
+
<OdysseyCacheProvider
|
|
47
|
+
emotionRootElement={emotionRootElement}
|
|
48
|
+
nonce={nonce}
|
|
49
|
+
stylisPlugins={stylisPlugins}
|
|
50
|
+
>
|
|
45
51
|
<OdysseyThemeProvider
|
|
46
|
-
themeOverride={themeOverride}
|
|
47
52
|
designTokensOverride={designTokensOverride}
|
|
53
|
+
shadowRootElement={shadowRootElement}
|
|
54
|
+
themeOverride={themeOverride}
|
|
48
55
|
>
|
|
49
56
|
<ScopedCssBaseline>
|
|
50
57
|
<OdysseyTranslationProvider
|
|
@@ -25,12 +25,14 @@ import { OdysseyDesignTokensContext } from "./OdysseyDesignTokensContext";
|
|
|
25
25
|
export type OdysseyThemeProviderProps = {
|
|
26
26
|
children: ReactNode;
|
|
27
27
|
designTokensOverride?: DesignTokensOverride;
|
|
28
|
+
shadowRootElement?: HTMLDivElement;
|
|
28
29
|
themeOverride?: ThemeOptions;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const OdysseyThemeProvider = ({
|
|
32
33
|
children,
|
|
33
34
|
designTokensOverride,
|
|
35
|
+
shadowRootElement,
|
|
34
36
|
themeOverride,
|
|
35
37
|
}: OdysseyThemeProviderProps) => {
|
|
36
38
|
const odysseyTokens = useMemo(
|
|
@@ -38,8 +40,12 @@ const OdysseyThemeProvider = ({
|
|
|
38
40
|
[designTokensOverride]
|
|
39
41
|
);
|
|
40
42
|
const odysseyTheme = useMemo(
|
|
41
|
-
() =>
|
|
42
|
-
|
|
43
|
+
() =>
|
|
44
|
+
createOdysseyMuiTheme({
|
|
45
|
+
odysseyTokens,
|
|
46
|
+
shadowRootElement,
|
|
47
|
+
}),
|
|
48
|
+
[odysseyTokens, shadowRootElement]
|
|
43
49
|
);
|
|
44
50
|
|
|
45
51
|
const customOdysseyTheme = useMemo(
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import { render, screen } from "@testing-library/react";
|
|
14
14
|
import { OdysseyTranslationProvider } from "./OdysseyTranslationProvider";
|
|
15
|
-
import
|
|
15
|
+
import { odysseyTranslate } from "./i18n";
|
|
16
16
|
import { TextField } from "./TextField";
|
|
17
17
|
|
|
18
18
|
describe("OdysseyTranslationProvider", () => {
|
|
19
19
|
it("defaults to 'en' translation bundle", () => {
|
|
20
20
|
render(
|
|
21
21
|
<OdysseyTranslationProvider>
|
|
22
|
-
<span>{
|
|
22
|
+
<span>{odysseyTranslate("fieldlabel.optional.text")}</span>
|
|
23
23
|
</OdysseyTranslationProvider>
|
|
24
24
|
);
|
|
25
25
|
|
|
@@ -14,7 +14,7 @@ import { ReactNode, useEffect } from "react";
|
|
|
14
14
|
|
|
15
15
|
import { SupportedLanguages } from "./OdysseyTranslationProvider.types";
|
|
16
16
|
|
|
17
|
-
import i18n,
|
|
17
|
+
import { i18n, defaultNS, resources } from "./i18n";
|
|
18
18
|
import { I18nextProvider } from "react-i18next";
|
|
19
19
|
|
|
20
20
|
export type TranslationOverrides = {
|
package/src/PasswordField.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { ShowIcon, HideIcon } from "./icons.generated";
|
|
24
24
|
import { Field } from "./Field";
|
|
25
25
|
import type { SeleniumProps } from "./SeleniumProps";
|
|
26
|
+
import { useTranslation } from "react-i18next";
|
|
26
27
|
|
|
27
28
|
export type PasswordFieldProps = {
|
|
28
29
|
/**
|
|
@@ -39,6 +40,10 @@ export type PasswordFieldProps = {
|
|
|
39
40
|
* If `true`, the component will receive focus automatically.
|
|
40
41
|
*/
|
|
41
42
|
hasInitialFocus?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* If `true`, the show/hide icon is not shown to the user
|
|
45
|
+
*/
|
|
46
|
+
hasShowPassword?: boolean;
|
|
42
47
|
/**
|
|
43
48
|
* The helper text content.
|
|
44
49
|
*/
|
|
@@ -99,6 +104,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
|
|
99
104
|
id: idOverride,
|
|
100
105
|
isDisabled = false,
|
|
101
106
|
isOptional = false,
|
|
107
|
+
hasShowPassword = true,
|
|
102
108
|
isReadOnly,
|
|
103
109
|
label,
|
|
104
110
|
name: nameOverride,
|
|
@@ -111,6 +117,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
|
|
111
117
|
},
|
|
112
118
|
ref
|
|
113
119
|
) => {
|
|
120
|
+
const { t } = useTranslation();
|
|
114
121
|
const [inputType, setInputType] = useState("password");
|
|
115
122
|
|
|
116
123
|
const togglePasswordVisibility = useCallback(() => {
|
|
@@ -128,16 +135,23 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
|
|
128
135
|
autoFocus={hasInitialFocus}
|
|
129
136
|
data-se={testId}
|
|
130
137
|
endAdornment={
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
hasShowPassword && (
|
|
139
|
+
<InputAdornment position="end">
|
|
140
|
+
<IconButton
|
|
141
|
+
aria-label={
|
|
142
|
+
inputType === "password"
|
|
143
|
+
? t("passwordfield.icon.label.show")
|
|
144
|
+
: t("passwordfield.icon.label.hide")
|
|
145
|
+
}
|
|
146
|
+
onClick={togglePasswordVisibility}
|
|
147
|
+
>
|
|
148
|
+
{inputType === "password" ? <ShowIcon /> : <HideIcon />}
|
|
149
|
+
</IconButton>
|
|
150
|
+
</InputAdornment>
|
|
151
|
+
)
|
|
139
152
|
}
|
|
140
153
|
id={id}
|
|
154
|
+
inputProps={{ role: "textbox" }}
|
|
141
155
|
name={nameOverride ?? id}
|
|
142
156
|
onChange={onChange}
|
|
143
157
|
onFocus={onFocus}
|
|
@@ -153,6 +167,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
|
|
153
167
|
[
|
|
154
168
|
autoCompleteType,
|
|
155
169
|
hasInitialFocus,
|
|
170
|
+
t,
|
|
156
171
|
togglePasswordVisibility,
|
|
157
172
|
inputType,
|
|
158
173
|
nameOverride,
|
|
@@ -162,6 +177,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
|
|
162
177
|
placeholder,
|
|
163
178
|
isOptional,
|
|
164
179
|
isReadOnly,
|
|
180
|
+
hasShowPassword,
|
|
165
181
|
ref,
|
|
166
182
|
testId,
|
|
167
183
|
value,
|
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 { ReactNode,
|
|
13
|
+
import { ReactNode, memo, useCallback, useMemo, useState } from "react";
|
|
14
14
|
import {
|
|
15
15
|
Box,
|
|
16
16
|
Chip,
|
|
@@ -100,173 +100,168 @@ export type SelectProps = {
|
|
|
100
100
|
* - { text: string, type: "heading" } — Used to display a group heading with the text
|
|
101
101
|
*/
|
|
102
102
|
|
|
103
|
-
const Select =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// or an empty array (if it's a multi-select)
|
|
125
|
-
if (typeof value === "undefined") {
|
|
126
|
-
value = isMultiSelect ? [] : "";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const [selectedValue, setSelectedValue] = useState<string | string[]>(
|
|
130
|
-
value
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const onChange = useCallback(
|
|
134
|
-
(event: SelectChangeEvent<string | string[]>, child: ReactNode) => {
|
|
135
|
-
const {
|
|
136
|
-
target: { value },
|
|
137
|
-
} = event;
|
|
103
|
+
const Select = ({
|
|
104
|
+
errorMessage,
|
|
105
|
+
hint,
|
|
106
|
+
id: idOverride,
|
|
107
|
+
isDisabled = false,
|
|
108
|
+
isMultiSelect = false,
|
|
109
|
+
isOptional = false,
|
|
110
|
+
label,
|
|
111
|
+
name: nameOverride,
|
|
112
|
+
onBlur,
|
|
113
|
+
onChange: onChangeProp,
|
|
114
|
+
onFocus,
|
|
115
|
+
value,
|
|
116
|
+
testId,
|
|
117
|
+
options,
|
|
118
|
+
}: SelectProps) => {
|
|
119
|
+
// If there's no value set, we set it to a blank string (if it's a single-select)
|
|
120
|
+
// or an empty array (if it's a multi-select)
|
|
121
|
+
if (typeof value === "undefined") {
|
|
122
|
+
value = isMultiSelect ? [] : "";
|
|
123
|
+
}
|
|
138
124
|
|
|
139
|
-
|
|
140
|
-
// for multi-selects
|
|
141
|
-
if (isMultiSelect) {
|
|
142
|
-
setSelectedValue(
|
|
143
|
-
typeof value === "string" ? value.split(",") : value
|
|
144
|
-
);
|
|
145
|
-
} else {
|
|
146
|
-
setSelectedValue(value);
|
|
147
|
-
}
|
|
125
|
+
const [selectedValue, setSelectedValue] = useState<string | string[]>(value);
|
|
148
126
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
[isMultiSelect, onChangeProp, setSelectedValue]
|
|
155
|
-
);
|
|
127
|
+
const onChange = useCallback(
|
|
128
|
+
(event: SelectChangeEvent<string | string[]>, child: ReactNode) => {
|
|
129
|
+
const {
|
|
130
|
+
target: { value },
|
|
131
|
+
} = event;
|
|
156
132
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
value: option.value || option.text,
|
|
164
|
-
type: option.type === "heading" ? "heading" : "option",
|
|
165
|
-
};
|
|
133
|
+
// Set the field value, with some additional logic to handle array values
|
|
134
|
+
// for multi-selects
|
|
135
|
+
if (isMultiSelect) {
|
|
136
|
+
setSelectedValue(typeof value === "string" ? value.split(",") : value);
|
|
137
|
+
} else {
|
|
138
|
+
setSelectedValue(value);
|
|
166
139
|
}
|
|
167
140
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (typeof selected === "string") {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Convert the selected options array into <Chip>s
|
|
180
|
-
const renderedChips = selected
|
|
181
|
-
.map((item: string) => {
|
|
182
|
-
const selectedOption = normalizedOptions.find(
|
|
183
|
-
(option) => option.value === item
|
|
184
|
-
);
|
|
141
|
+
// Trigger the onChange event, if one has been passed
|
|
142
|
+
if (onChangeProp) {
|
|
143
|
+
onChangeProp(event, child);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[isMultiSelect, onChangeProp, setSelectedValue]
|
|
147
|
+
);
|
|
185
148
|
|
|
186
|
-
|
|
187
|
-
|
|
149
|
+
// Normalize the options array to accommodate the various
|
|
150
|
+
// data types that might be passed
|
|
151
|
+
const normalizedOptions = useMemo(
|
|
152
|
+
() =>
|
|
153
|
+
options.map((option) =>
|
|
154
|
+
typeof option === "object"
|
|
155
|
+
? {
|
|
156
|
+
text: option.text,
|
|
157
|
+
value: option.value || option.text,
|
|
158
|
+
type: option.type === "heading" ? "heading" : "option",
|
|
188
159
|
}
|
|
160
|
+
: { text: option, value: option, type: "option" }
|
|
161
|
+
),
|
|
162
|
+
[options]
|
|
163
|
+
);
|
|
189
164
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
165
|
+
const renderValue = useCallback(
|
|
166
|
+
(selected: string | string[]) => {
|
|
167
|
+
// If the selected value isn't an array, then we don't need to display
|
|
168
|
+
// chips and should fall back to the default render behavior
|
|
169
|
+
if (typeof selected === "string") {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
193
172
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
173
|
+
// Convert the selected options array into <Chip>s
|
|
174
|
+
const renderedChips = selected
|
|
175
|
+
.map((item: string) => {
|
|
176
|
+
const selectedOption = normalizedOptions.find(
|
|
177
|
+
(option) => option.value === item
|
|
178
|
+
);
|
|
197
179
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
[normalizedOptions]
|
|
203
|
-
);
|
|
180
|
+
if (!selectedOption) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
204
183
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
184
|
+
return <Chip key={item} label={selectedOption.text} />;
|
|
185
|
+
})
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
|
|
188
|
+
if (renderedChips.length === 0) {
|
|
189
|
+
return null;
|
|
210
190
|
}
|
|
211
191
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
</MenuItem>
|
|
219
|
-
);
|
|
220
|
-
});
|
|
192
|
+
// We need the <Box> to surround the <Chip>s for
|
|
193
|
+
// proper styling
|
|
194
|
+
return <Box>{renderedChips}</Box>;
|
|
195
|
+
},
|
|
196
|
+
[normalizedOptions]
|
|
197
|
+
);
|
|
221
198
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
onBlur={onBlur}
|
|
231
|
-
onChange={onChange}
|
|
232
|
-
onFocus={onFocus}
|
|
233
|
-
ref={ref}
|
|
234
|
-
renderValue={isMultiSelect ? renderValue : undefined}
|
|
235
|
-
value={selectedValue}
|
|
236
|
-
labelId={label}
|
|
237
|
-
/>
|
|
238
|
-
),
|
|
239
|
-
[
|
|
240
|
-
children,
|
|
241
|
-
idOverride,
|
|
242
|
-
isMultiSelect,
|
|
243
|
-
label,
|
|
244
|
-
nameOverride,
|
|
245
|
-
onBlur,
|
|
246
|
-
onChange,
|
|
247
|
-
onFocus,
|
|
248
|
-
ref,
|
|
249
|
-
renderValue,
|
|
250
|
-
selectedValue,
|
|
251
|
-
testId,
|
|
252
|
-
]
|
|
253
|
-
);
|
|
199
|
+
// Convert the options into the ReactNode children
|
|
200
|
+
// that will populate the <Select>
|
|
201
|
+
const children = useMemo(
|
|
202
|
+
() =>
|
|
203
|
+
normalizedOptions.map((option) => {
|
|
204
|
+
if (option.type === "heading") {
|
|
205
|
+
return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
|
|
206
|
+
}
|
|
254
207
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
208
|
+
return (
|
|
209
|
+
<MenuItem key={option.value} value={option.value}>
|
|
210
|
+
{isMultiSelect && (
|
|
211
|
+
<MuiCheckbox checked={selectedValue.includes(option.value)} />
|
|
212
|
+
)}
|
|
213
|
+
{option.text}
|
|
214
|
+
</MenuItem>
|
|
215
|
+
);
|
|
216
|
+
}),
|
|
217
|
+
[isMultiSelect, normalizedOptions, selectedValue]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const renderFieldComponent = useCallback(
|
|
221
|
+
({ ariaDescribedBy, id }) => (
|
|
222
|
+
<MuiSelect
|
|
223
|
+
aria-describedby={ariaDescribedBy}
|
|
224
|
+
children={children}
|
|
225
|
+
data-se={testId}
|
|
226
|
+
id={id}
|
|
227
|
+
labelId={label}
|
|
228
|
+
multiple={isMultiSelect}
|
|
229
|
+
name={nameOverride ?? id}
|
|
230
|
+
onBlur={onBlur}
|
|
231
|
+
onChange={onChange}
|
|
232
|
+
onFocus={onFocus}
|
|
233
|
+
renderValue={isMultiSelect ? renderValue : undefined}
|
|
234
|
+
value={selectedValue}
|
|
266
235
|
/>
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
|
|
236
|
+
),
|
|
237
|
+
[
|
|
238
|
+
children,
|
|
239
|
+
isMultiSelect,
|
|
240
|
+
label,
|
|
241
|
+
nameOverride,
|
|
242
|
+
onBlur,
|
|
243
|
+
onChange,
|
|
244
|
+
onFocus,
|
|
245
|
+
renderValue,
|
|
246
|
+
selectedValue,
|
|
247
|
+
testId,
|
|
248
|
+
]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<Field
|
|
253
|
+
errorMessage={errorMessage}
|
|
254
|
+
fieldType="single"
|
|
255
|
+
hasVisibleLabel
|
|
256
|
+
hint={hint}
|
|
257
|
+
id={idOverride}
|
|
258
|
+
isDisabled={isDisabled}
|
|
259
|
+
isOptional={isOptional}
|
|
260
|
+
label={label}
|
|
261
|
+
renderFieldComponent={renderFieldComponent}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
270
265
|
|
|
271
266
|
const MemoizedSelect = memo(Select);
|
|
272
267
|
MemoizedSelect.displayName = "Select";
|