@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/Autocomplete.js +12 -1
  3. package/dist/Autocomplete.js.map +1 -1
  4. package/dist/OdysseyCacheProvider.js +4 -1
  5. package/dist/OdysseyCacheProvider.js.map +1 -1
  6. package/dist/OdysseyProvider.js +5 -1
  7. package/dist/OdysseyProvider.js.map +1 -1
  8. package/dist/OdysseyThemeProvider.js +5 -1
  9. package/dist/OdysseyThemeProvider.js.map +1 -1
  10. package/dist/OdysseyTranslationProvider.js +1 -1
  11. package/dist/OdysseyTranslationProvider.js.map +1 -1
  12. package/dist/PasswordField.js +11 -3
  13. package/dist/PasswordField.js.map +1 -1
  14. package/dist/Select.js +34 -33
  15. package/dist/Select.js.map +1 -1
  16. package/dist/Typography.js +0 -22
  17. package/dist/Typography.js.map +1 -1
  18. package/dist/createShadowDom.js +26 -0
  19. package/dist/createShadowDom.js.map +1 -0
  20. package/dist/{OdysseyI18n.js → i18n.js} +3 -2
  21. package/dist/i18n.js.map +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/labs/datePickerTheme.js +4 -2
  25. package/dist/labs/datePickerTheme.js.map +1 -1
  26. package/dist/properties/ts/odyssey-react-mui.js +2 -0
  27. package/dist/properties/ts/odyssey-react-mui.js.map +1 -1
  28. package/dist/src/Autocomplete.d.ts +23 -3
  29. package/dist/src/Autocomplete.d.ts.map +1 -1
  30. package/dist/src/OdysseyCacheProvider.d.ts +6 -1
  31. package/dist/src/OdysseyCacheProvider.d.ts.map +1 -1
  32. package/dist/src/OdysseyProvider.d.ts +1 -1
  33. package/dist/src/OdysseyProvider.d.ts.map +1 -1
  34. package/dist/src/OdysseyThemeProvider.d.ts +2 -1
  35. package/dist/src/OdysseyThemeProvider.d.ts.map +1 -1
  36. package/dist/src/OdysseyTranslationProvider.d.ts +1 -1
  37. package/dist/src/OdysseyTranslationProvider.d.ts.map +1 -1
  38. package/dist/src/PasswordField.d.ts +8 -0
  39. package/dist/src/PasswordField.d.ts.map +1 -1
  40. package/dist/src/Select.d.ts +1 -54
  41. package/dist/src/Select.d.ts.map +1 -1
  42. package/dist/src/Typography.d.ts +11 -15
  43. package/dist/src/Typography.d.ts.map +1 -1
  44. package/dist/src/createShadowDom.d.ts +16 -0
  45. package/dist/src/createShadowDom.d.ts.map +1 -0
  46. package/dist/src/{OdysseyI18n.d.ts → i18n.d.ts} +5 -2
  47. package/dist/src/i18n.d.ts.map +1 -0
  48. package/dist/src/index.d.ts +1 -0
  49. package/dist/src/index.d.ts.map +1 -1
  50. package/dist/src/properties/ts/odyssey-react-mui.d.ts +2 -0
  51. package/dist/src/properties/ts/odyssey-react-mui.d.ts.map +1 -1
  52. package/dist/src/theme/components.d.ts +4 -1
  53. package/dist/src/theme/components.d.ts.map +1 -1
  54. package/dist/src/theme/createOdysseyMuiTheme.d.ts +23 -0
  55. package/dist/src/theme/createOdysseyMuiTheme.d.ts.map +1 -0
  56. package/dist/src/theme/mixins.d.ts +3 -1
  57. package/dist/src/theme/mixins.d.ts.map +1 -1
  58. package/dist/src/theme/palette.d.ts +3 -1
  59. package/dist/src/theme/palette.d.ts.map +1 -1
  60. package/dist/src/theme/shape.d.ts +3 -1
  61. package/dist/src/theme/shape.d.ts.map +1 -1
  62. package/dist/src/theme/spacing.d.ts +3 -1
  63. package/dist/src/theme/spacing.d.ts.map +1 -1
  64. package/dist/src/theme/theme.d.ts +1 -8
  65. package/dist/src/theme/theme.d.ts.map +1 -1
  66. package/dist/src/theme/typography.d.ts +3 -1
  67. package/dist/src/theme/typography.d.ts.map +1 -1
  68. package/dist/theme/components.js +80 -63
  69. package/dist/theme/components.js.map +1 -1
  70. package/dist/theme/createOdysseyMuiTheme.js +51 -0
  71. package/dist/theme/createOdysseyMuiTheme.js.map +1 -0
  72. package/dist/theme/mixins.js +4 -1
  73. package/dist/theme/mixins.js.map +1 -1
  74. package/dist/theme/palette.js +4 -1
  75. package/dist/theme/palette.js.map +1 -1
  76. package/dist/theme/shape.js +4 -1
  77. package/dist/theme/shape.js.map +1 -1
  78. package/dist/theme/spacing.js +4 -1
  79. package/dist/theme/spacing.js.map +1 -1
  80. package/dist/theme/theme.js +1 -20
  81. package/dist/theme/theme.js.map +1 -1
  82. package/dist/theme/typography.js +4 -1
  83. package/dist/theme/typography.js.map +1 -1
  84. package/dist/tsconfig.production.tsbuildinfo +1 -1
  85. package/package.json +4 -4
  86. package/src/Autocomplete.tsx +44 -3
  87. package/src/OdysseyCacheProvider.tsx +9 -1
  88. package/src/OdysseyProvider.tsx +9 -2
  89. package/src/OdysseyThemeProvider.tsx +8 -2
  90. package/src/OdysseyTranslationProvider.test.tsx +2 -2
  91. package/src/OdysseyTranslationProvider.tsx +1 -1
  92. package/src/PasswordField.tsx +24 -8
  93. package/src/Select.tsx +147 -152
  94. package/src/Typography.tsx +0 -26
  95. package/src/createShadowDom.ts +46 -0
  96. package/src/{OdysseyI18n.ts → i18n.ts} +2 -2
  97. package/src/index.ts +1 -0
  98. package/src/labs/datePickerTheme.tsx +2 -2
  99. package/src/properties/odyssey-react-mui.properties +2 -0
  100. package/src/properties/ts/odyssey-react-mui.ts +1 -1
  101. package/src/theme/components.tsx +26 -9
  102. package/src/theme/createOdysseyMuiTheme.ts +47 -0
  103. package/src/theme/mixins.ts +5 -1
  104. package/src/theme/palette.ts +5 -3
  105. package/src/theme/shape.ts +5 -1
  106. package/src/theme/spacing.ts +5 -3
  107. package/src/theme/theme.ts +1 -26
  108. package/src/theme/typography.ts +5 -3
  109. package/dist/OdysseyI18n.js.map +0 -1
  110. 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.1.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.1.1",
54
+ "@okta/odyssey-design-tokens": "1.2.0",
55
55
  "date-fns": "^2.30.0",
56
- "i18next": "^22.4.15",
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": "3cf42aaed1fa433ee7a0e4b10112f98544f28f56"
66
+ "gitHead": "6dc7afd2fce73ec25b0566b0c59aeedf0738b925"
67
67
  }
@@ -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
- * Callback fired when the value of the autocomplete input changes
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 input value of the autocomplete input changes
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>;
@@ -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 nonce={nonce} stylisPlugins={stylisPlugins}>
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
- () => createOdysseyMuiTheme(odysseyTokens),
42
- [odysseyTokens]
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 i18n from "./OdysseyI18n";
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>{i18n.t("fieldlabel.optional.text")}</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, { defaultNS, resources } from "./OdysseyI18n";
17
+ import { i18n, defaultNS, resources } from "./i18n";
18
18
  import { I18nextProvider } from "react-i18next";
19
19
 
20
20
  export type TranslationOverrides = {
@@ -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
- <InputAdornment position="end">
132
- <IconButton
133
- aria-label="toggle password visibility"
134
- onClick={togglePasswordVisibility}
135
- >
136
- {inputType === "password" ? <ShowIcon /> : <HideIcon />}
137
- </IconButton>
138
- </InputAdornment>
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, forwardRef, memo, useCallback, useState } from "react";
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 = forwardRef<HTMLSelectElement, SelectProps>(
104
- (
105
- {
106
- errorMessage,
107
- hint,
108
- id: idOverride,
109
- isDisabled = false,
110
- isMultiSelect = false,
111
- isOptional = false,
112
- label,
113
- name: nameOverride,
114
- onBlur,
115
- onChange: onChangeProp,
116
- onFocus,
117
- value,
118
- testId,
119
- options,
120
- },
121
- ref
122
- ) => {
123
- // If there's no value set, we set it to a blank string (if it's a single-select)
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
- // Set the field value, with some additional logic to handle array values
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
- // Trigger the onChange event, if one has been passed
150
- if (onChangeProp) {
151
- onChangeProp(event, child);
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
- // Normalize the options array to accommodate the various
158
- // data types that might be passed
159
- const normalizedOptions = options.map((option) => {
160
- if (typeof option === "object") {
161
- return {
162
- text: option.text,
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
- return { text: option, value: option, type: "option" };
169
- });
170
-
171
- const renderValue = useCallback(
172
- (selected: string | string[]) => {
173
- // If the selected value isn't an array, then we don't need to display
174
- // chips and should fall back to the default render behavior
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
- if (!selectedOption) {
187
- return null;
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
- return <Chip key={item} label={selectedOption.text} />;
191
- })
192
- .filter(Boolean);
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
- if (renderedChips.length === 0) {
195
- return null;
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
- // We need the <Box> to surround the <Chip>s for
199
- // proper styling
200
- return <Box>{renderedChips}</Box>;
201
- },
202
- [normalizedOptions]
203
- );
180
+ if (!selectedOption) {
181
+ return null;
182
+ }
204
183
 
205
- // Convert the options into the ReactNode children
206
- // that will populate the <Select>
207
- const children = normalizedOptions.map((option) => {
208
- if (option.type === "heading") {
209
- return <ListSubheader key={option.text}>{option.text}</ListSubheader>;
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
- return (
213
- <MenuItem key={option.value} value={option.value}>
214
- {isMultiSelect && (
215
- <MuiCheckbox checked={selectedValue.includes(option.value)} />
216
- )}
217
- {option.text}
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
- const renderFieldComponent = useCallback(
223
- () => (
224
- <MuiSelect
225
- children={children}
226
- data-se={testId}
227
- id={idOverride}
228
- multiple={isMultiSelect}
229
- name={nameOverride ?? idOverride}
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
- return (
256
- <Field
257
- errorMessage={errorMessage}
258
- fieldType="single"
259
- hasVisibleLabel
260
- hint={hint}
261
- id={idOverride}
262
- isDisabled={isDisabled}
263
- isOptional={isOptional}
264
- label={label}
265
- renderFieldComponent={renderFieldComponent}
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";