@okta/odyssey-react-mui 1.14.4 → 1.14.6

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 (108) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/Badge.js +1 -1
  3. package/dist/Badge.js.map +1 -1
  4. package/dist/DataTable/DataTable.js +178 -58
  5. package/dist/DataTable/DataTable.js.map +1 -1
  6. package/dist/DataTable/DataTableEmptyState.js +55 -0
  7. package/dist/DataTable/DataTableEmptyState.js.map +1 -0
  8. package/dist/DataTable/DataTablePagination.js +221 -0
  9. package/dist/DataTable/DataTablePagination.js.map +1 -0
  10. package/dist/DataTable/DataTableRowActions.js +34 -24
  11. package/dist/DataTable/DataTableRowActions.js.map +1 -1
  12. package/dist/DataTable/DataTableSettings.js +22 -10
  13. package/dist/DataTable/DataTableSettings.js.map +1 -1
  14. package/dist/DataTable/constants.js +1 -0
  15. package/dist/DataTable/constants.js.map +1 -1
  16. package/dist/DataTable/index.js +1 -0
  17. package/dist/DataTable/index.js.map +1 -1
  18. package/dist/DataTable/useRowReordering.js +3 -3
  19. package/dist/DataTable/useRowReordering.js.map +1 -1
  20. package/dist/DataTable/useScrollIndication.js +70 -0
  21. package/dist/DataTable/useScrollIndication.js.map +1 -0
  22. package/dist/Field.js.map +1 -1
  23. package/dist/Fieldset.js +17 -14
  24. package/dist/Fieldset.js.map +1 -1
  25. package/dist/Form.js +33 -23
  26. package/dist/Form.js.map +1 -1
  27. package/dist/MenuButton.js +1 -1
  28. package/dist/MenuButton.js.map +1 -1
  29. package/dist/SearchField.js +2 -2
  30. package/dist/SearchField.js.map +1 -1
  31. package/dist/labs/DataFilters.js +6 -2
  32. package/dist/labs/DataFilters.js.map +1 -1
  33. package/dist/labs/DataTable.js +3 -3
  34. package/dist/labs/DataTable.js.map +1 -1
  35. package/dist/labs/FileUpload.js +195 -0
  36. package/dist/labs/FileUpload.js.map +1 -0
  37. package/dist/labs/FileUploadIllustration.js +54 -0
  38. package/dist/labs/FileUploadIllustration.js.map +1 -0
  39. package/dist/labs/FileUploadPreview.js +109 -0
  40. package/dist/labs/FileUploadPreview.js.map +1 -0
  41. package/dist/labs/index.js +1 -0
  42. package/dist/labs/index.js.map +1 -1
  43. package/dist/properties/ts/odyssey-react-mui.js +12 -0
  44. package/dist/properties/ts/odyssey-react-mui.js.map +1 -1
  45. package/dist/src/DataTable/DataTable.d.ts +36 -18
  46. package/dist/src/DataTable/DataTable.d.ts.map +1 -1
  47. package/dist/src/DataTable/DataTableEmptyState.d.ts +21 -0
  48. package/dist/src/DataTable/DataTableEmptyState.d.ts.map +1 -0
  49. package/dist/src/DataTable/DataTablePagination.d.ts +33 -0
  50. package/dist/src/DataTable/DataTablePagination.d.ts.map +1 -0
  51. package/dist/src/DataTable/DataTableRowActions.d.ts.map +1 -1
  52. package/dist/src/DataTable/DataTableSettings.d.ts.map +1 -1
  53. package/dist/src/DataTable/constants.d.ts +1 -0
  54. package/dist/src/DataTable/constants.d.ts.map +1 -1
  55. package/dist/src/DataTable/index.d.ts +2 -1
  56. package/dist/src/DataTable/index.d.ts.map +1 -1
  57. package/dist/src/DataTable/useRowReordering.d.ts.map +1 -1
  58. package/dist/src/DataTable/useScrollIndication.d.ts +22 -0
  59. package/dist/src/DataTable/useScrollIndication.d.ts.map +1 -0
  60. package/dist/src/Field.d.ts +8 -7
  61. package/dist/src/Field.d.ts.map +1 -1
  62. package/dist/src/Fieldset.d.ts.map +1 -1
  63. package/dist/src/Form.d.ts.map +1 -1
  64. package/dist/src/OdysseyTranslationProvider.d.ts +1 -1
  65. package/dist/src/OdysseyTranslationProvider.d.ts.map +1 -1
  66. package/dist/src/SearchField.d.ts.map +1 -1
  67. package/dist/src/labs/DataFilters.d.ts +5 -1
  68. package/dist/src/labs/DataFilters.d.ts.map +1 -1
  69. package/dist/src/labs/DataTable.d.ts.map +1 -1
  70. package/dist/src/labs/FileUpload.d.ts +40 -0
  71. package/dist/src/labs/FileUpload.d.ts.map +1 -0
  72. package/dist/src/labs/FileUploadIllustration.d.ts +15 -0
  73. package/dist/src/labs/FileUploadIllustration.d.ts.map +1 -0
  74. package/dist/src/labs/FileUploadPreview.d.ts +21 -0
  75. package/dist/src/labs/FileUploadPreview.d.ts.map +1 -0
  76. package/dist/src/labs/index.d.ts +4 -0
  77. package/dist/src/labs/index.d.ts.map +1 -1
  78. package/dist/src/properties/ts/odyssey-react-mui.d.ts +12 -0
  79. package/dist/src/properties/ts/odyssey-react-mui.d.ts.map +1 -1
  80. package/dist/src/theme/components.d.ts.map +1 -1
  81. package/dist/theme/components.js +10 -1
  82. package/dist/theme/components.js.map +1 -1
  83. package/dist/tsconfig.production.tsbuildinfo +1 -1
  84. package/package.json +3 -3
  85. package/src/Badge.tsx +1 -1
  86. package/src/DataTable/DataTable.tsx +293 -85
  87. package/src/DataTable/DataTableEmptyState.tsx +62 -0
  88. package/src/DataTable/DataTablePagination.tsx +289 -0
  89. package/src/DataTable/DataTableRowActions.tsx +35 -37
  90. package/src/DataTable/DataTableSettings.tsx +43 -17
  91. package/src/DataTable/constants.ts +1 -0
  92. package/src/DataTable/index.tsx +7 -1
  93. package/src/DataTable/useRowReordering.tsx +5 -3
  94. package/src/DataTable/useScrollIndication.tsx +118 -0
  95. package/src/Field.tsx +9 -7
  96. package/src/Fieldset.tsx +24 -18
  97. package/src/Form.tsx +43 -27
  98. package/src/MenuButton.tsx +1 -1
  99. package/src/SearchField.tsx +1 -2
  100. package/src/labs/DataFilters.tsx +9 -0
  101. package/src/labs/DataTable.tsx +5 -9
  102. package/src/labs/FileUpload.tsx +301 -0
  103. package/src/labs/FileUploadIllustration.tsx +66 -0
  104. package/src/labs/FileUploadPreview.tsx +150 -0
  105. package/src/labs/index.ts +4 -2
  106. package/src/properties/odyssey-react-mui.properties +18 -0
  107. package/src/properties/ts/odyssey-react-mui.ts +1 -1
  108. package/src/theme/components.tsx +9 -0
package/src/Fieldset.tsx CHANGED
@@ -10,15 +10,32 @@
10
10
  * See the License for the specific language governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { Box } from "@mui/material";
14
13
  import { memo, ReactElement, useMemo } from "react";
14
+ import styled from "@emotion/styled";
15
15
 
16
16
  import { Callout } from "./Callout";
17
17
  import { FieldsetContext } from "./FieldsetContext";
18
+ import type { HtmlProps } from "./HtmlProps";
18
19
  import { Legend, Support } from "./Typography";
19
- import { useOdysseyDesignTokens } from "./OdysseyDesignTokensContext";
20
+ import {
21
+ useOdysseyDesignTokens,
22
+ DesignTokens,
23
+ } from "./OdysseyDesignTokensContext";
20
24
  import { useUniqueId } from "./useUniqueId";
21
- import type { HtmlProps } from "./HtmlProps";
25
+
26
+ const StyledFieldset = styled.fieldset<{
27
+ odysseyDesignTokens: DesignTokens;
28
+ }>(({ odysseyDesignTokens }) => ({
29
+ border: "0",
30
+ margin: odysseyDesignTokens.Spacing0,
31
+ marginBlockEnd: odysseyDesignTokens.Spacing6,
32
+ maxWidth: odysseyDesignTokens.TypographyLineLengthMax,
33
+ padding: odysseyDesignTokens.Spacing0,
34
+
35
+ "&:last-child": {
36
+ marginBlockEnd: odysseyDesignTokens.Spacing0,
37
+ },
38
+ }));
22
39
 
23
40
  export type FieldsetProps = {
24
41
  /**
@@ -73,23 +90,12 @@ const Fieldset = ({
73
90
  );
74
91
 
75
92
  return (
76
- <Box
77
- component="fieldset"
93
+ <StyledFieldset
78
94
  data-se={testId}
79
95
  disabled={isDisabled}
80
- name={name}
81
96
  id={id}
82
- sx={{
83
- border: "0",
84
- margin: odysseyDesignTokens.Spacing0,
85
- marginBlockEnd: odysseyDesignTokens.Spacing6,
86
- maxWidth: odysseyDesignTokens.TypographyLineLengthMax,
87
- padding: odysseyDesignTokens.Spacing0,
88
-
89
- "&:last-child": {
90
- marginBlockEnd: odysseyDesignTokens.Spacing0,
91
- },
92
- }}
97
+ name={name}
98
+ odysseyDesignTokens={odysseyDesignTokens}
93
99
  >
94
100
  <Legend translate={translate}>{legend}</Legend>
95
101
 
@@ -100,7 +106,7 @@ const Fieldset = ({
100
106
  <FieldsetContext.Provider value={fieldsetContextValue}>
101
107
  {children}
102
108
  </FieldsetContext.Provider>
103
- </Box>
109
+ </StyledFieldset>
104
110
  );
105
111
  };
106
112
 
package/src/Form.tsx CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { FormEventHandler, memo, ReactElement } from "react";
14
- import { Box } from "@mui/material";
14
+ import styled from "@emotion/styled";
15
15
 
16
16
  import { Button } from "./Button";
17
17
  import { Callout } from "./Callout";
@@ -19,6 +19,10 @@ import { FieldComponentProps } from "./FieldComponentProps";
19
19
  import type { HtmlProps } from "./HtmlProps";
20
20
  import { Heading4, Support } from "./Typography";
21
21
  import { useUniqueId } from "./useUniqueId";
22
+ import {
23
+ useOdysseyDesignTokens,
24
+ DesignTokens,
25
+ } from "./OdysseyDesignTokensContext";
22
26
 
23
27
  export const formEncodingTypeValues = [
24
28
  "application/x-www-form-urlencoded",
@@ -29,6 +33,34 @@ export const formEncodingTypeValues = [
29
33
  export const formAutoCompleteTypeValues = ["on", "off"] as const;
30
34
  export const formMethodValues = ["post", "get", "dialog"] as const;
31
35
 
36
+ const StyledForm = styled.form<{
37
+ isFullWidth?: boolean;
38
+ odysseyDesignTokens: DesignTokens;
39
+ }>(({ isFullWidth, odysseyDesignTokens }) => ({
40
+ maxWidth: isFullWidth ? "100%" : odysseyDesignTokens.TypographyLineLengthMax,
41
+ margin: 0,
42
+ padding: 0,
43
+ }));
44
+
45
+ const TitleContainer = styled.div<{
46
+ odysseyDesignTokens: DesignTokens;
47
+ }>(({ odysseyDesignTokens }) => ({
48
+ marginBlockEnd: odysseyDesignTokens.Spacing4,
49
+ }));
50
+
51
+ const FormActionContainer = styled.div<{
52
+ odysseyDesignTokens: DesignTokens;
53
+ }>(
54
+ {
55
+ display: "flex",
56
+ justifyContent: "flex-end",
57
+ },
58
+ ({ odysseyDesignTokens }) => ({
59
+ gap: odysseyDesignTokens.Spacing1,
60
+ marginBlockStart: odysseyDesignTokens.Spacing7,
61
+ }),
62
+ );
63
+
32
64
  export type FormProps = {
33
65
  /**
34
66
  * A Callout indicating a Form-wide error or status update.
@@ -112,31 +144,23 @@ const Form = ({
112
144
  translate,
113
145
  }: FormProps) => {
114
146
  const id = useUniqueId(idOverride);
147
+ const odysseyDesignTokens = useOdysseyDesignTokens();
115
148
 
116
149
  return (
117
- <Box
150
+ <StyledForm
118
151
  autoComplete={autoCompleteType}
119
- component="form"
120
152
  data-se={testId}
121
153
  encType={encodingType}
122
154
  id={id}
155
+ isFullWidth={isFullWidth}
123
156
  method={method}
124
157
  name={name}
125
158
  noValidate={noValidate}
159
+ odysseyDesignTokens={odysseyDesignTokens}
126
160
  onSubmit={onSubmit}
127
161
  target={target}
128
- sx={{
129
- maxWidth: (theme) => (isFullWidth ? "100%" : theme.mixins.maxWidth),
130
- margin: (theme) => theme.spacing(0),
131
- padding: (theme) => theme.spacing(0),
132
- }}
133
162
  >
134
- <Box
135
- component="div"
136
- sx={{
137
- marginBlockEnd: (theme) => theme.spacing(4),
138
- }}
139
- >
163
+ <TitleContainer odysseyDesignTokens={odysseyDesignTokens}>
140
164
  {title && (
141
165
  <Heading4 component="h1" translate={translate}>
142
166
  {title}
@@ -144,22 +168,14 @@ const Form = ({
144
168
  )}
145
169
  {description && <Support translate={translate}>{description}</Support>}
146
170
  {alert}
147
- </Box>
148
- <Box component="div">{children}</Box>
171
+ </TitleContainer>
172
+ <div>{children}</div>
149
173
  {formActions && (
150
- <Box
151
- component="div"
152
- sx={{
153
- display: "flex",
154
- justifyContent: "flex-end",
155
- gap: (theme) => theme.spacing(1),
156
- marginBlockStart: (theme) => theme.spacing(7),
157
- }}
158
- >
174
+ <FormActionContainer odysseyDesignTokens={odysseyDesignTokens}>
159
175
  {formActions}
160
- </Box>
176
+ </FormActionContainer>
161
177
  )}
162
- </Box>
178
+ </StyledForm>
163
179
  );
164
180
  };
165
181
 
@@ -179,7 +179,7 @@ const MenuButton = ({
179
179
  ariaDescribedBy={ariaDescribedBy}
180
180
  ariaLabel={ariaLabel}
181
181
  ariaLabelledBy={ariaLabelledBy}
182
- data-se={testId}
182
+ testId={testId}
183
183
  endIcon={endIcon}
184
184
  id={`${uniqueId}-button`}
185
185
  isDisabled={isDisabled}
@@ -158,7 +158,7 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
158
158
  autoFocus={hasInitialFocus}
159
159
  data-se={testId}
160
160
  endAdornment={
161
- defaultValue && (
161
+ (inputValues?.defaultValue || inputValues?.value) && (
162
162
  <InputAdornment position="end">
163
163
  <IconButton
164
164
  aria-label="Clear"
@@ -191,7 +191,6 @@ const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
191
191
  ),
192
192
  [
193
193
  autoCompleteType,
194
- defaultValue,
195
194
  hasInitialFocus,
196
195
  inputValues,
197
196
  isDisabled,
@@ -118,6 +118,10 @@ export type DataFiltersProps = {
118
118
  * the filter menu won't be shown.
119
119
  */
120
120
  filters?: Array<DataFilter>;
121
+ /**
122
+ * If true, the filter and search will be disabled
123
+ */
124
+ isDisabled?: boolean;
121
125
  };
122
126
 
123
127
  const DataFilters = ({
@@ -128,6 +132,7 @@ const DataFilters = ({
128
132
  defaultSearchTerm = "",
129
133
  additionalActions,
130
134
  filters: filtersProp = [],
135
+ isDisabled,
131
136
  }: DataFiltersProps) => {
132
137
  const [filters, setFilters] = useState<DataFilter[]>(filtersProp);
133
138
  const { t } = useTranslation();
@@ -311,6 +316,7 @@ const DataFilters = ({
311
316
  aria-expanded={isFiltersMenuOpen ? "true" : undefined}
312
317
  aria-haspopup="true"
313
318
  ariaLabel={t("filters.filters.arialabel")}
319
+ isDisabled={isDisabled}
314
320
  endIcon={<FilterIcon />}
315
321
  onClick={(event) => {
316
322
  setFiltersMenuAnchorElement(event.currentTarget);
@@ -406,6 +412,7 @@ const DataFilters = ({
406
412
  ),
407
413
  [
408
414
  isFiltersMenuOpen,
415
+ isDisabled,
409
416
  filterPopoverCurrentFilter,
410
417
  isFilterPopoverOpen,
411
418
  filtersMenuAnchorElement,
@@ -659,6 +666,8 @@ const DataFilters = ({
659
666
  <SearchField
660
667
  value={searchValue}
661
668
  label={t("filters.search.label")}
669
+ placeholder={t("filters.search.label")}
670
+ isDisabled={isDisabled}
662
671
  onClear={() => {
663
672
  setSearchValue("");
664
673
  onChangeSearch("");
@@ -12,7 +12,7 @@
12
12
 
13
13
  import {
14
14
  MRT_Updater,
15
- MRT_Virtualizer,
15
+ MRT_RowVirtualizer,
16
16
  MRT_RowData,
17
17
  MRT_TableOptions,
18
18
  useMaterialReactTable,
@@ -426,12 +426,9 @@ const DataTable = ({
426
426
  );
427
427
 
428
428
  const rowVirtualizerInstanceRef =
429
- useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null);
429
+ useRef<MRT_RowVirtualizer<HTMLDivElement, HTMLTableRowElement>>(null);
430
430
 
431
- const getRowFromTableAndSetHovered = (
432
- table: TableType,
433
- id: MRT_RowData["id"],
434
- ) => {
431
+ const setHoveredRow = (table: TableType, id: MRT_RowData["id"]) => {
435
432
  if (id) {
436
433
  const nextRow: MRT_RowData = table.getRow(id);
437
434
 
@@ -501,12 +498,12 @@ const DataTable = ({
501
498
 
502
499
  if (isArrowDown || isArrowUp) {
503
500
  const nextIndex = isArrowDown ? index + 1 : index - 1;
504
- getRowFromTableAndSetHovered(table, data[nextIndex]?.id);
501
+ setHoveredRow(table, data[nextIndex]?.id);
505
502
  }
506
503
  } else {
507
504
  if (isArrowDown || isArrowUp) {
508
505
  const nextIndex = isArrowDown ? row.index + 1 : row.index - 1;
509
- getRowFromTableAndSetHovered(table, data[nextIndex]?.id);
506
+ setHoveredRow(table, data[nextIndex]?.id);
510
507
  }
511
508
  }
512
509
  } else {
@@ -605,7 +602,6 @@ const DataTable = ({
605
602
  align: "right",
606
603
  sx: {
607
604
  width: "unset",
608
- // TODO: Make the right padding here 16px (and possibly adapt it to the density padding)
609
605
  },
610
606
  },
611
607
  },
@@ -0,0 +1,301 @@
1
+ /*!
2
+ * Copyright (c) 2022-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 {
14
+ memo,
15
+ ChangeEvent,
16
+ useCallback,
17
+ useEffect,
18
+ useRef,
19
+ useState,
20
+ } from "react";
21
+ import styled from "@emotion/styled";
22
+ import { useTranslation } from "react-i18next";
23
+
24
+ import { Button } from "../Button";
25
+ import { UploadIcon } from "../icons.generated";
26
+ import { Field, RenderFieldComponentProps } from "../Field";
27
+ import { FieldComponentProps } from "../FieldComponentProps";
28
+ import { FileUploadPreview } from "./FileUploadPreview";
29
+ import { FileUploadIllustration } from "./FileUploadIllustration";
30
+ import {
31
+ useOdysseyDesignTokens,
32
+ DesignTokens,
33
+ } from "../OdysseyDesignTokensContext";
34
+ import { Support } from "../Typography";
35
+
36
+ export const fileUploadTypes = ["single", "multiple"] as const;
37
+ export const fileUploadVariants = [
38
+ "button",
39
+ "dragAndDrop",
40
+ "dragAndDropWithIcon",
41
+ ] as const;
42
+
43
+ const BaseInputWrapper = styled.div({
44
+ position: "relative",
45
+ alignSelf: "flex-start",
46
+
47
+ input: {
48
+ position: "absolute",
49
+ width: "100%",
50
+ height: "100%",
51
+ opacity: 0,
52
+ },
53
+ });
54
+
55
+ const InputContainer = styled(BaseInputWrapper)<{
56
+ odysseyDesignTokens: DesignTokens;
57
+ }>(
58
+ {
59
+ display: "flex",
60
+ alignSelf: "unset",
61
+ alignItems: "center",
62
+ justifyContent: "center",
63
+
64
+ "&:has(input:focus)": {
65
+ borderStyle: "solid",
66
+ },
67
+ },
68
+ ({ odysseyDesignTokens }) => ({
69
+ padding: `${odysseyDesignTokens.Spacing6} ${odysseyDesignTokens.Spacing3}`,
70
+ border: `1px dashed ${odysseyDesignTokens.HueNeutral300}`,
71
+ borderRadius: odysseyDesignTokens.BorderRadiusMain,
72
+ transition: `border-color ${odysseyDesignTokens.TransitionTimingMain}, box-shadow ${odysseyDesignTokens.TransitionTimingMain}`,
73
+
74
+ "&:hover": {
75
+ borderColor: odysseyDesignTokens.HueNeutral700,
76
+ },
77
+
78
+ "&:has(input:focus)": {
79
+ borderColor: odysseyDesignTokens.FocusOutlineColorPrimary,
80
+ boxShadow: `0 0 0 1px ${odysseyDesignTokens.FocusOutlineColorPrimary}`,
81
+ outline: `${odysseyDesignTokens.FocusOutlineWidthMain} ${odysseyDesignTokens.FocusOutlineStyle} transparent`,
82
+ outlineOffset: odysseyDesignTokens.FocusOutlineOffsetTight,
83
+ },
84
+
85
+ "&:has(input:disabled)": {
86
+ backgroundColor: odysseyDesignTokens.HueNeutral50,
87
+ border: `1px solid ${odysseyDesignTokens.BorderColorDisabled}`,
88
+ color: odysseyDesignTokens.TypographyColorDisabled,
89
+
90
+ "&:hover": {
91
+ borderColor: odysseyDesignTokens.BorderColorDisabled,
92
+ },
93
+ },
94
+ }),
95
+ );
96
+
97
+ const ButtonAndInfoContainer = styled.div({
98
+ display: "flex",
99
+ flexDirection: "column",
100
+ alignItems: "center",
101
+ justifyContent: "center",
102
+ });
103
+
104
+ export type FileUploadProps = {
105
+ /**
106
+ * an array of file types the user is able to upload. @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers for examples
107
+ */
108
+ acceptedFileTypes?: string[];
109
+ /**
110
+ * The label for the `input` element.
111
+ */
112
+ label: string;
113
+ /**
114
+ * Function that is called when the list of ifles to upload is changed
115
+ */
116
+ onChange: (files: File[]) => void;
117
+ /**
118
+ * Either `single` or `multiple`. If `multiple`, multiple files can be uploaded
119
+ */
120
+ type?: (typeof fileUploadTypes)[number];
121
+ /**
122
+ * Either `button`, `dragAndDrop` or `dragAndDropWithIcon`. Will determine how component appears visually
123
+ */
124
+ variant: (typeof fileUploadVariants)[number];
125
+ } & Pick<
126
+ FieldComponentProps,
127
+ | "errorMessage"
128
+ | "hint"
129
+ | "HintLinkComponent"
130
+ | "id"
131
+ | "isDisabled"
132
+ | "isOptional"
133
+ >;
134
+
135
+ const FileUpload = ({
136
+ acceptedFileTypes,
137
+ errorMessage,
138
+ id,
139
+ isDisabled = false,
140
+ isOptional,
141
+ hint,
142
+ HintLinkComponent,
143
+ label,
144
+ onChange,
145
+ type,
146
+ variant,
147
+ }: FileUploadProps) => {
148
+ const odysseyDesignTokens = useOdysseyDesignTokens();
149
+ const { t } = useTranslation();
150
+ const inputRef = useRef<HTMLInputElement>(null);
151
+ const [filesToUpload, setFilesToUpload] = useState<File[]>([]);
152
+
153
+ useEffect(() => {
154
+ onChange(filesToUpload);
155
+ }, [filesToUpload, onChange]);
156
+
157
+ const updateFilesToUpload = useCallback(
158
+ (event: ChangeEvent<HTMLInputElement>) => {
159
+ const { files } = event.target;
160
+
161
+ if (files && files.length > 0) {
162
+ const mergedFiles =
163
+ type === "multiple"
164
+ ? [...filesToUpload, ...files]
165
+ : ([...files] satisfies File[] as File[]);
166
+
167
+ setFilesToUpload(mergedFiles);
168
+ }
169
+
170
+ // reset input value to allow re-upload of a file with the same name
171
+ event.target.value = "";
172
+ },
173
+ [type, filesToUpload],
174
+ );
175
+
176
+ const triggerFileInputClick = useCallback(() => {
177
+ inputRef.current?.focus();
178
+ }, [inputRef]);
179
+
180
+ const removeFileFromFilesToUploadList = useCallback<(name: string) => void>(
181
+ (name) => {
182
+ const deletedFileFilteredOut = filesToUpload.filter(
183
+ (file) => file.name !== name,
184
+ );
185
+ setFilesToUpload(deletedFileFilteredOut);
186
+ },
187
+ [filesToUpload],
188
+ );
189
+
190
+ const renderFileInput = useCallback(
191
+ ({
192
+ ariaDescribedBy,
193
+ errorMessageElementId,
194
+ id,
195
+ labelElementId,
196
+ }: RenderFieldComponentProps) => {
197
+ const fileNames = filesToUpload.map((file) => file.name);
198
+ const acceptedFileTypesAsString = acceptedFileTypes?.join(",");
199
+
200
+ const Input = () => (
201
+ <input
202
+ accept={acceptedFileTypesAsString}
203
+ aria-describedby={ariaDescribedBy}
204
+ aria-errormessage={errorMessageElementId}
205
+ aria-labelledby={labelElementId}
206
+ disabled={isDisabled}
207
+ id={id}
208
+ multiple={type === "multiple"}
209
+ onChange={updateFilesToUpload}
210
+ ref={inputRef}
211
+ title=""
212
+ type="file"
213
+ />
214
+ );
215
+
216
+ if (variant === "button") {
217
+ return (
218
+ <>
219
+ <BaseInputWrapper>
220
+ <Input />
221
+ <Button
222
+ isDisabled={isDisabled}
223
+ label={t("fileupload.button.text")}
224
+ onClick={triggerFileInputClick}
225
+ startIcon={<UploadIcon />}
226
+ variant="secondary"
227
+ />
228
+ </BaseInputWrapper>
229
+ <FileUploadPreview
230
+ fileNames={fileNames}
231
+ onFileRemove={removeFileFromFilesToUploadList}
232
+ isDisabled={isDisabled}
233
+ />
234
+ </>
235
+ );
236
+ }
237
+
238
+ return (
239
+ <>
240
+ <InputContainer odysseyDesignTokens={odysseyDesignTokens}>
241
+ <Input />
242
+ <ButtonAndInfoContainer>
243
+ {variant === "dragAndDropWithIcon" && <FileUploadIllustration />}
244
+ <Support color="textSecondary">
245
+ {t("fileupload.prompt.text")}
246
+ </Support>
247
+ <Button
248
+ isDisabled={isDisabled}
249
+ label={t("fileupload.button.text")}
250
+ onClick={triggerFileInputClick}
251
+ startIcon={<UploadIcon />}
252
+ variant="secondary"
253
+ />
254
+ </ButtonAndInfoContainer>
255
+ </InputContainer>
256
+ <FileUploadPreview
257
+ fileNames={fileNames}
258
+ onFileRemove={removeFileFromFilesToUploadList}
259
+ isDisabled={isDisabled}
260
+ />
261
+ </>
262
+ );
263
+ },
264
+ [
265
+ acceptedFileTypes,
266
+ filesToUpload,
267
+ isDisabled,
268
+ inputRef,
269
+ odysseyDesignTokens,
270
+ removeFileFromFilesToUploadList,
271
+ triggerFileInputClick,
272
+ t,
273
+ type,
274
+ updateFilesToUpload,
275
+ variant,
276
+ ],
277
+ );
278
+
279
+ return (
280
+ <>
281
+ <Field
282
+ errorMessage={errorMessage}
283
+ fieldType="single"
284
+ hasVisibleLabel
285
+ hint={hint}
286
+ HintLinkComponent={HintLinkComponent}
287
+ id={id}
288
+ isDisabled={isDisabled}
289
+ isFullWidth
290
+ isOptional={isOptional}
291
+ label={label}
292
+ renderFieldComponent={renderFileInput}
293
+ />
294
+ </>
295
+ );
296
+ };
297
+
298
+ const MemoizedFileUpload = memo(FileUpload);
299
+ MemoizedFileUpload.displayName = "FileUpload";
300
+
301
+ export { MemoizedFileUpload as FileUpload };
@@ -0,0 +1,66 @@
1
+ /*!
2
+ * Copyright (c) 2022-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 { memo } from "react";
14
+ import styled from "@emotion/styled";
15
+
16
+ import {
17
+ useOdysseyDesignTokens,
18
+ DesignTokens,
19
+ } from "../OdysseyDesignTokensContext";
20
+
21
+ const UploadIllustrationContainer = styled.div<{
22
+ odysseyDesignTokens: DesignTokens;
23
+ }>(({ odysseyDesignTokens }) => ({
24
+ marginBlockEnd: odysseyDesignTokens.Spacing2,
25
+ padding: odysseyDesignTokens.Spacing3,
26
+ backgroundColor: odysseyDesignTokens.HueNeutral50,
27
+ borderRadius: "50%",
28
+
29
+ svg: {
30
+ display: "flex",
31
+ width: odysseyDesignTokens.Spacing8,
32
+ height: odysseyDesignTokens.Spacing8,
33
+ },
34
+ }));
35
+
36
+ const FileUploadIllustration = () => {
37
+ const odysseyDesignTokens = useOdysseyDesignTokens();
38
+
39
+ return (
40
+ <UploadIllustrationContainer
41
+ aria-hidden="true"
42
+ odysseyDesignTokens={odysseyDesignTokens}
43
+ >
44
+ <svg
45
+ aria-hidden="true"
46
+ viewBox="0 0 44 45"
47
+ fill="none"
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ >
50
+ <path
51
+ d="M32.0763 11.001C29.3564 3.7855 21.6595 -0.565827 13.7765 0.726748C5.35441 2.10773 -0.676662 9.50714 0.0603005 17.8612C0.441865 22.1865 2.56458 25.9787 5.71703 28.614L8.28246 25.545C5.90122 23.5544 4.32811 20.7209 4.04483 17.5097C3.50262 11.3633 7.94433 5.73648 14.4238 4.67404C20.9164 3.60944 27.0806 7.52016 28.6895 13.5191C28.9239 14.3932 29.7162 15.001 30.6212 15.001H32.9114C36.8985 15.001 39.9997 18.0938 39.9997 21.7505C39.9997 24.3423 38.4576 26.6352 36.1259 27.7678L37.8736 31.3658C41.4737 29.6171 43.9997 25.9917 43.9997 21.7505C43.9997 15.7428 38.963 11.001 32.9114 11.001H32.0763Z"
52
+ fill={odysseyDesignTokens.HueNeutral200}
53
+ />
54
+ <path
55
+ d="M23.9994 29.3277V44.5H19.9994V29.3289L14.4142 34.9141L11.5858 32.0857L19.7373 23.9342C20.9869 22.6845 23.0131 22.6845 24.2627 23.9342L32.4142 32.0857L29.5858 34.9141L23.9994 29.3277Z"
56
+ fill={odysseyDesignTokens.HueNeutral200}
57
+ />
58
+ </svg>
59
+ </UploadIllustrationContainer>
60
+ );
61
+ };
62
+
63
+ const MemoizedFileUploadIllustration = memo(FileUploadIllustration);
64
+ MemoizedFileUploadIllustration.displayName = "FileUploadIllustration";
65
+
66
+ export { MemoizedFileUploadIllustration as FileUploadIllustration };