@evoke-platform/ui-components 1.8.0-testing.9 → 1.8.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 (40) hide show
  1. package/dist/published/components/custom/DataGrid/DataGrid.d.ts +1 -0
  2. package/dist/published/components/custom/DataGrid/DataGrid.js +2 -1
  3. package/dist/published/components/custom/DataGrid/Toolbar.d.ts +1 -0
  4. package/dist/published/components/custom/DataGrid/Toolbar.js +3 -2
  5. package/dist/published/components/custom/DataGrid/index.d.ts +1 -0
  6. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +47 -40
  7. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/RelatedObjectInstance.js +1 -1
  8. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/ActionDialog.d.ts +2 -0
  9. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/ActionDialog.js +2 -2
  10. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.d.ts +2 -0
  11. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +2 -2
  12. package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.d.ts +1 -1
  13. package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.js +18 -6
  14. package/dist/published/components/custom/Form/tests/Form.test.js +192 -2
  15. package/dist/published/components/custom/Form/tests/test-data.d.ts +7 -0
  16. package/dist/published/components/custom/Form/tests/test-data.js +138 -0
  17. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -2
  18. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +3 -2
  19. package/dist/published/components/custom/FormV2/FormRendererContainer.js +2 -2
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +1 -1
  21. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +1 -2
  22. package/dist/published/components/custom/FormV2/components/utils.d.ts +1 -1
  23. package/dist/published/components/custom/FormV2/components/utils.js +1 -1
  24. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.d.ts +1 -0
  25. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +173 -0
  26. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.d.ts +1 -0
  27. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +96 -0
  28. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +16 -0
  29. package/dist/published/components/custom/FormV2/tests/test-data.js +394 -0
  30. package/dist/published/components/custom/index.d.ts +1 -0
  31. package/dist/published/index.d.ts +1 -1
  32. package/dist/published/stories/FormRenderer.stories.d.ts +7 -0
  33. package/dist/published/stories/FormRenderer.stories.js +65 -0
  34. package/dist/published/stories/FormRendererContainer.stories.d.ts +7 -0
  35. package/dist/published/stories/FormRendererContainer.stories.js +56 -0
  36. package/dist/published/stories/FormRendererData.d.ts +116 -0
  37. package/dist/published/stories/FormRendererData.js +925 -0
  38. package/dist/published/stories/sharedMswHandlers.d.ts +1 -0
  39. package/dist/published/stories/sharedMswHandlers.js +100 -0
  40. package/package.json +10 -4
@@ -23,6 +23,7 @@ export type DataGridProps<T extends GridValidRowModel> = MuiDataGridProps<T> & {
23
23
  hideSearchbar?: boolean;
24
24
  loadingOptions?: LoadingOptions;
25
25
  hideDownload?: boolean;
26
+ exportFileName?: string;
26
27
  };
27
28
  export default function <T extends GridValidRowModel>(props: DataGridProps<T>): React.JSX.Element;
28
29
  export {};
@@ -6,7 +6,7 @@ import LinearProgress from '../../core/LinearProgress';
6
6
  import { dateTimeBetweenOperator } from './DateTimeCustomOperator';
7
7
  import Toolbar from './Toolbar';
8
8
  export default function (props) {
9
- const { onRefresh, loading, theme, title, bulkAction, filterSettings, columns, rows, hideSearchbar, loadingOptions, hideDownload, ...rest } = props;
9
+ const { onRefresh, loading, theme, title, bulkAction, filterSettings, columns, rows, hideSearchbar, loadingOptions, hideDownload, exportFileName, ...rest } = props;
10
10
  const [anchorEl, setAnchorEl] = useState();
11
11
  const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
12
12
  const addColumnFilterOperators = (columns) => {
@@ -93,6 +93,7 @@ export default function (props) {
93
93
  bulkAction,
94
94
  hideSearchbar,
95
95
  hideDownload,
96
+ exportFileName,
96
97
  },
97
98
  panel: {
98
99
  anchorEl: anchorEl,
@@ -11,6 +11,7 @@ export type GridToolbarProps = MuiGridToolbarProps & {
11
11
  bulkAction?: BulkAction;
12
12
  hideSearchbar?: boolean;
13
13
  hideDownload?: boolean;
14
+ exportFileName?: string;
14
15
  };
15
16
  declare function Toolbar(props: GridToolbarProps): React.JSX.Element;
16
17
  export default Toolbar;
@@ -6,7 +6,8 @@ import UIThemeProvider from '../../../theme';
6
6
  import { Button, IconButton } from '../../core';
7
7
  import { Grid } from '../../layout';
8
8
  function Toolbar(props) {
9
- const { onRefresh, setAnchorEl, loading, theme, title, bulkAction, hideSearchbar, hideDownload } = props;
9
+ const { onRefresh, setAnchorEl, loading, theme, title, bulkAction, hideSearchbar, hideDownload, exportFileName } = props;
10
+ const resolvedFileName = exportFileName ?? 'data';
10
11
  const styles = {
11
12
  container: { display: 'flex', justifyContent: 'space-between', margin: '0 0 15px 0' },
12
13
  iconButton: {
@@ -57,7 +58,7 @@ function Toolbar(props) {
57
58
  '&:hover': { backgroundColor: '#f2f3f5' },
58
59
  paddingLeft: '10px',
59
60
  paddingRight: '10px',
60
- }, printOptions: { disableToolbarButton: true }, startIcon: React.createElement(FileDownloadRounded, null) }))),
61
+ }, printOptions: { disableToolbarButton: true }, csvOptions: { fileName: resolvedFileName }, startIcon: React.createElement(FileDownloadRounded, null) }))),
61
62
  React.createElement(Grid, { item: true },
62
63
  React.createElement(GridToolbarFilterButton, { componentsProps: {
63
64
  button: {
@@ -1,3 +1,4 @@
1
1
  import DataGrid from './DataGrid';
2
+ export type { GridSortModel } from '@mui/x-data-grid';
2
3
  export { DataGrid };
3
4
  export default DataGrid;
@@ -3,7 +3,7 @@ import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
3
3
  import Handlebars from 'no-eval-handlebars';
4
4
  import React, { useCallback, useEffect, useState } from 'react';
5
5
  import { Close } from '../../../../../icons';
6
- import { Autocomplete, Button, Dialog, IconButton, Link, Paper, TextField, Tooltip, Typography, } from '../../../../core';
6
+ import { Autocomplete, Button, Dialog, IconButton, Link, ListItem, Paper, TextField, Tooltip, Typography, } from '../../../../core';
7
7
  import { Box } from '../../../../layout';
8
8
  import { getPrefixedUrl, normalizeDates, transformToWhere } from '../../utils';
9
9
  import { RelatedObjectInstance } from './RelatedObjectInstance';
@@ -146,9 +146,27 @@ export const ObjectPropertyInput = (props) => {
146
146
  return instance ? template(instance) : undefined;
147
147
  };
148
148
  const navigationSlug = defaultPages && property.objectId && defaultPages[property.objectId];
149
+ const dropdownOptions = [
150
+ ...options.map((o) => ({ label: o.name, value: o.id })),
151
+ ...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === DEFAULT_CREATE_ACTION)
152
+ ? [
153
+ {
154
+ value: '__new__',
155
+ label: '+ Add New',
156
+ },
157
+ ]
158
+ : []),
159
+ ];
149
160
  return (React.createElement(React.Fragment, null,
150
161
  displayOption === 'dropdown' ? (React.createElement(React.Fragment, null,
151
- React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, componentsProps: {
162
+ React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, clearIcon: React.createElement(IconButton, { size: "small", disableRipple: true, onKeyDown: (e) => {
163
+ if (e.key === 'Enter') {
164
+ e.stopPropagation();
165
+ }
166
+ }, onClick: (e) => {
167
+ setOpenOptions(false);
168
+ }, "aria-label": "Clear selection", sx: { padding: 0 } },
169
+ React.createElement(Close, { sx: { fontSize: '20px' } })), componentsProps: {
152
170
  popper: {
153
171
  modifiers: [
154
172
  {
@@ -163,6 +181,7 @@ export const ObjectPropertyInput = (props) => {
163
181
  boxShadow: '0px 24px 48px 0px rgba(145, 158, 171, 0.2)',
164
182
  '& .MuiAutocomplete-listbox': {
165
183
  maxHeight: '25vh',
184
+ paddingBottom: '0px',
166
185
  },
167
186
  '& .MuiAutocomplete-noOptions': {
168
187
  fontFamily: 'sans-serif',
@@ -176,20 +195,7 @@ export const ObjectPropertyInput = (props) => {
176
195
  paddingLeft: '24px',
177
196
  color: 'rgba(145, 158, 171, 1)',
178
197
  },
179
- } },
180
- children,
181
- mode !== 'existingOnly' &&
182
- relatedObject?.actions?.some((a) => a.id === DEFAULT_CREATE_ACTION) && (React.createElement(Button, { fullWidth: true, sx: {
183
- justifyContent: 'flex-start',
184
- pl: 2,
185
- minHeight: '48px',
186
- borderTop: '1px solid rgba(145, 158, 171, 0.24)',
187
- borderRadius: '0p 0pc 6px 6px',
188
- paddingLeft: '22px',
189
- fontWeight: 400,
190
- }, onMouseDown: (e) => {
191
- setOpenCreateDialog(true);
192
- }, color: 'inherit' }, "+ Add New"))));
198
+ } }, children));
193
199
  }, sx: {
194
200
  '& button.MuiButtonBase-root': {
195
201
  ...(!loadingOptions &&
@@ -200,18 +206,14 @@ export const ObjectPropertyInput = (props) => {
200
206
  : {}),
201
207
  },
202
208
  }, noOptionsText: 'No options available', renderOption: (props, option) => {
203
- return (React.createElement("li", { ...props, key: option.id },
209
+ const isAddNew = option.value === '__new__';
210
+ return (React.createElement(ListItem, { ...props, key: props.id, sx: {
211
+ ...(isAddNew ? { borderTop: '1px solid rgba(145, 158, 171, 0.24)' } : {}),
212
+ } },
204
213
  React.createElement(Box, null,
205
214
  React.createElement(Typography, { sx: { marginLeft: '8px', fontSize: '14px' } }, option.label),
206
215
  layout?.secondaryTextExpression ? (React.createElement(Typography, { sx: { marginLeft: '8px', fontSize: '14px', color: '#637381' } }, compileExpression(layout?.secondaryTextExpression, options.find((o) => o.id === option.value)))) : null)));
207
- }, onOpen: () => {
208
- if (instance?.[property.id]?.id || selectedInstance?.id) {
209
- setOpenOptions(false);
210
- }
211
- else {
212
- setOpenOptions(true);
213
- }
214
- }, onClose: () => setOpenOptions(false), value: instance?.[property.id]?.id || selectedInstance?.id
216
+ }, onOpen: () => setOpenOptions(true), onClose: () => setOpenOptions(false), value: instance?.[property.id]?.id || selectedInstance?.id
215
217
  ? {
216
218
  value: instance?.[property.id]?.id ??
217
219
  selectedInstance?.id ??
@@ -225,15 +227,20 @@ export const ObjectPropertyInput = (props) => {
225
227
  return option.value === value;
226
228
  }
227
229
  return option.value === value?.value;
228
- }, options: options.map((o) => ({ label: o.name, value: o.id })), getOptionLabel: (option) => {
230
+ }, options: dropdownOptions, filterOptions: (options) => options, getOptionLabel: (option) => {
229
231
  return typeof option === 'string'
230
232
  ? (options.find((o) => o.id === option)?.name ?? '')
231
233
  : option.label;
232
- }, onKeyDownCapture: (e) => {
234
+ }, onKeyDown: (e) => {
233
235
  // prevents keyboard trap
234
236
  if (e.key === 'Tab') {
235
237
  return;
236
238
  }
239
+ if (e.key === 'Enter') {
240
+ setOpenOptions(true);
241
+ setDropdownInput(undefined);
242
+ return;
243
+ }
237
244
  if (instance?.[property.id]?.id || selectedInstance?.id) {
238
245
  e.preventDefault();
239
246
  }
@@ -244,31 +251,31 @@ export const ObjectPropertyInput = (props) => {
244
251
  handleChangeObjectProperty(property.id, null);
245
252
  instance[property.id] = null;
246
253
  }
254
+ else if (value?.value === '__new__') {
255
+ setOpenCreateDialog(true);
256
+ }
247
257
  else {
248
258
  const selectedInstance = options.find((o) => o.id === value?.value);
249
259
  setSelectedInstance(selectedInstance);
250
260
  handleChangeObjectProperty(property.id, selectedInstance);
251
261
  }
252
- }, selectOnFocus: false, onBlur: () => setDropdownInput(undefined), renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: 'Select', readOnly: !loadingOptions &&
262
+ }, selectOnFocus: false, onBlur: () => {
263
+ if (dropdownInput) {
264
+ getDropdownOptions();
265
+ }
266
+ }, renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: 'Select', readOnly: !loadingOptions &&
253
267
  !!(instance?.[property.id]?.id ||
254
268
  selectedInstance?.id), onChange: (event) => setDropdownInput(event.target.value), onClick: (e) => {
255
269
  if (navigationSlug &&
256
270
  ['DIV', 'INPUT'].includes(e.target?.nodeName) &&
257
271
  (instance?.[property.id]?.id ||
258
272
  selectedInstance?.id)) {
259
- navigateTo &&
273
+ if (navigateTo) {
274
+ setOpenOptions(false);
260
275
  navigateTo(navigationSlug.replace(':instanceId', instance?.[property.id]?.id ??
261
276
  selectedInstance?.id ??
262
277
  ''));
263
- }
264
- if (openOptions &&
265
- e.target?.nodeName === 'svg') {
266
- setOpenOptions(false);
267
- }
268
- else if (!['DIV', 'INPUT'].includes(e.target?.nodeName) &&
269
- !selectedInstance) {
270
- setOptions(options);
271
- setOpenOptions(true);
278
+ }
272
279
  }
273
280
  }, sx: {
274
281
  ...(!loadingOptions &&
@@ -349,8 +356,8 @@ export const ObjectPropertyInput = (props) => {
349
356
  event.stopPropagation();
350
357
  setOpenCreateDialog(true);
351
358
  }, "aria-label": `Add`, disabled: !canUpdateProperty }, "Add")))),
352
- openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { apiServices: apiServices, handleClose: handleClose, handleChangeObjectProperty: handleChangeObjectProperty, instance: instance, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, property: property, nestedFieldsView: nestedFieldsView, mode: mode, setSnackbarError: setSnackbarError, displayOption: displayOption, setOptions: setOptions, options: options, filter: filter, user: user, layout: layout, richTextEditor: richTextEditor })) : (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose },
353
- React.createElement(Typography, { sx: {
359
+ openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { apiServices: apiServices, handleClose: handleClose, handleChangeObjectProperty: handleChangeObjectProperty, instance: instance, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, property: property, nestedFieldsView: nestedFieldsView, mode: mode, setSnackbarError: setSnackbarError, displayOption: displayOption, setOptions: setOptions, options: options, filter: filter, user: user, layout: layout, richTextEditor: richTextEditor })) : (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose, "aria-labelledby": "add-dialog-title" },
360
+ React.createElement(Typography, { id: "add-dialog-title", sx: {
354
361
  marginTop: '28px',
355
362
  fontSize: '22px',
356
363
  fontWeight: 700,
@@ -5,7 +5,7 @@ import React, { useState } from 'react';
5
5
  import { InfoRounded } from '../../../../../icons';
6
6
  import { Alert, Button, FormControlLabel, Radio, RadioGroup } from '../../../../core';
7
7
  import { Box, Grid } from '../../../../layout';
8
- import { Form } from '../../../Form';
8
+ import { Form } from '../../Common/Form';
9
9
  import { getPrefixedUrl, normalizeDateTime, normalizeDates } from '../../utils';
10
10
  import { InstanceLookup } from './InstanceLookup';
11
11
  const styles = {
@@ -1,4 +1,5 @@
1
1
  import { Action, ApiServices, Obj, UserAccount } from '@evoke-platform/context';
2
+ import { ReactComponent } from '@formio/react';
2
3
  import React from 'react';
3
4
  import { Address, ObjectPropertyInputProps } from '../../types';
4
5
  export type ActionDialogProps = {
@@ -20,5 +21,6 @@ export type ActionDialogProps = {
20
21
  instanceId: string;
21
22
  propertyId: string;
22
23
  };
24
+ richTextEditor?: typeof ReactComponent;
23
25
  };
24
26
  export declare const ActionDialog: (props: ActionDialogProps) => React.JSX.Element;
@@ -36,7 +36,7 @@ const styles = {
36
36
  },
37
37
  };
38
38
  export const ActionDialog = (props) => {
39
- const { open, onClose, action, instanceInput, handleSubmit, apiServices, object, instanceId, objectInputCommonProps, queryAddresses, associatedObject, user, } = props;
39
+ const { open, onClose, action, instanceInput, handleSubmit, apiServices, object, instanceId, objectInputCommonProps, queryAddresses, associatedObject, user, richTextEditor, } = props;
40
40
  const [updatedObject, setUpdatedObject] = useState();
41
41
  const [hasAccess, setHasAccess] = useState(false);
42
42
  const [loading, setLoading] = useState(false);
@@ -76,7 +76,7 @@ export const ActionDialog = (props) => {
76
76
  React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose },
77
77
  React.createElement(Close, { fontSize: "small" })),
78
78
  action && hasAccess && !loading ? action?.name : ''),
79
- React.createElement(DialogContent, null, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } }, (updatedObject || isDeleteAction) && (React.createElement(Form, { actionId: action.id, actionType: action.type, apiServices: objectInputCommonProps.apiServices, object: !isDeleteAction ? updatedObject : object, instance: instanceInput, onSave: async (data, setSubmitting) => handleSubmit(action.type, data, instanceId, setSubmitting), objectInputCommonProps: objectInputCommonProps, closeModal: onClose, queryAddresses: queryAddresses, user: user, submitButtonLabel: isDeleteAction ? 'Delete' : undefined, associatedObject: associatedObject })))) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
79
+ React.createElement(DialogContent, null, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } }, (updatedObject || isDeleteAction) && (React.createElement(Form, { actionId: action.id, actionType: action.type, apiServices: objectInputCommonProps.apiServices, object: !isDeleteAction ? updatedObject : object, instance: instanceInput, onSave: async (data, setSubmitting) => handleSubmit(action.type, data, instanceId, setSubmitting), objectInputCommonProps: objectInputCommonProps, closeModal: onClose, queryAddresses: queryAddresses, user: user, submitButtonLabel: isDeleteAction ? 'Delete' : undefined, associatedObject: associatedObject, richTextEditor: richTextEditor })))) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
80
80
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
81
81
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
82
82
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }))) : (React.createElement(ErrorComponent, { code: 'AccessDenied', message: 'You do not have permission to perform this action.', styles: { boxShadow: 'none' } })))))));
@@ -1,4 +1,5 @@
1
1
  import { ApiServices, ObjectInstance, Property, UserAccount, ViewLayoutEntityReference } from '@evoke-platform/context';
2
+ import { ReactComponent } from '@formio/react';
2
3
  import React from 'react';
3
4
  import { Address } from '../../types';
4
5
  export type ObjectPropertyInputProps = {
@@ -9,6 +10,7 @@ export type ObjectPropertyInputProps = {
9
10
  queryAddresses?: (query: string) => Promise<Address[]>;
10
11
  user?: UserAccount;
11
12
  viewLayout?: ViewLayoutEntityReference;
13
+ richTextEditor?: typeof ReactComponent;
12
14
  };
13
15
  declare const RepeatableField: (props: ObjectPropertyInputProps) => React.JSX.Element;
14
16
  export default RepeatableField;
@@ -33,7 +33,7 @@ const styles = {
33
33
  },
34
34
  };
35
35
  const RepeatableField = (props) => {
36
- const { property, instance, canUpdateProperty, apiServices, queryAddresses, user, viewLayout } = props;
36
+ const { property, instance, canUpdateProperty, apiServices, queryAddresses, user, viewLayout, richTextEditor } = props;
37
37
  const [relatedInstances, setRelatedInstances] = useState([]);
38
38
  const [relatedObject, setRelatedObject] = useState();
39
39
  const [hasCreateAction, setHasCreateAction] = useState(false);
@@ -433,7 +433,7 @@ const RepeatableField = (props) => {
433
433
  objectInputCommonProps: { apiServices }, action: relatedObject?.actions?.find((a) => a.id ===
434
434
  (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, queryAddresses: queryAddresses, user: user, associatedObject: instance.id && property.relatedPropertyId
435
435
  ? { instanceId: instance.id, propertyId: property.relatedPropertyId }
436
- : undefined })),
436
+ : undefined, richTextEditor: richTextEditor })),
437
437
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
438
438
  };
439
439
  export default RepeatableField;
@@ -6,7 +6,7 @@ export type UserPropertyProps = {
6
6
  property: Property;
7
7
  apiServices: ApiServices;
8
8
  user?: UserAccount;
9
- handleChangeUserProperty: (user: AutocompleteOption) => void;
9
+ handleChangeUserProperty: (user: AutocompleteOption | null) => void;
10
10
  error?: boolean;
11
11
  setSnackbarError?: (snackbarError: {
12
12
  showAlert: boolean;
@@ -1,6 +1,7 @@
1
1
  import { ExpandMore } from '@mui/icons-material';
2
2
  import React, { useEffect, useState } from 'react';
3
- import { Autocomplete, Paper, TextField, Typography } from '../../../../core';
3
+ import { Close } from '../../../../../icons';
4
+ import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../../../core';
4
5
  import { getPrefixedUrl } from '../../utils';
5
6
  export const UserProperty = (props) => {
6
7
  const { id, property, apiServices, handleChangeUserProperty, error, filter, value, user, fieldHeight } = props;
@@ -37,7 +38,16 @@ export const UserProperty = (props) => {
37
38
  }
38
39
  }, [property, filter]);
39
40
  return (options && (React.createElement(React.Fragment, null,
40
- React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue ? '' : React.createElement(ExpandMore, null), PaperComponent: ({ children }) => {
41
+ React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue ? '' : React.createElement(ExpandMore, null), clearIcon: !loadingOptions && userValue ? (React.createElement(IconButton, { size: "small", disableRipple: true, onKeyDown: (e) => {
42
+ if (e.key === 'Enter') {
43
+ e.stopPropagation();
44
+ }
45
+ }, onClick: (e) => {
46
+ setOpenOptions(false);
47
+ }, "aria-label": "Clear selection", sx: {
48
+ padding: 0,
49
+ } },
50
+ React.createElement(Close, { sx: { fontSize: '20px' } }))) : null, PaperComponent: ({ children }) => {
41
51
  return (React.createElement(Paper, { sx: {
42
52
  borderRadius: '12px',
43
53
  boxShadow: '0px 24px 48px 0px rgba(145, 158, 171, 0.2)',
@@ -72,9 +82,7 @@ export const UserProperty = (props) => {
72
82
  " ",
73
83
  '',
74
84
  users?.find((user) => option.value === user.id)?.status === 'Inactive' ? (React.createElement("span", null, "(Inactive)")) : (''))));
75
- }, onOpen: () => {
76
- setOpenOptions(true);
77
- }, onClose: () => setOpenOptions(false), value: userValue ?? '', options: options, getOptionLabel: (option) => {
85
+ }, onOpen: () => setOpenOptions(true), onClose: () => setOpenOptions(false), value: userValue ?? '', options: options, getOptionLabel: (option) => {
78
86
  if (typeof option === 'string') {
79
87
  return options.find((o) => o.value === option)?.label ?? '';
80
88
  }
@@ -88,11 +96,15 @@ export const UserProperty = (props) => {
88
96
  }
89
97
  }
90
98
  }
91
- }, onKeyDownCapture: (e) => {
99
+ }, onKeyDown: (e) => {
92
100
  // prevents keyboard trap
93
101
  if (e.key === 'Tab') {
94
102
  return;
95
103
  }
104
+ if (e.key === 'Enter') {
105
+ setOpenOptions(true);
106
+ return;
107
+ }
96
108
  if (value) {
97
109
  e.preventDefault();
98
110
  }
@@ -1,4 +1,5 @@
1
1
  import { ApiServices } from '@evoke-platform/context';
2
+ import * as matchers from '@testing-library/jest-dom/matchers';
2
3
  import { render, screen, waitFor, within } from '@testing-library/react';
3
4
  import userEvent from '@testing-library/user-event';
4
5
  import axios from 'axios';
@@ -8,7 +9,8 @@ import { setupServer } from 'msw/node';
8
9
  import React from 'react';
9
10
  import { expect, it } from 'vitest';
10
11
  import Form from '../Common/Form';
11
- import { licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
12
+ import { accessibility508Object, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, users, } from './test-data';
13
+ expect.extend(matchers);
12
14
  const removePoppers = () => {
13
15
  const portalSelectors = ['.MuiAutocomplete-popper'];
14
16
  portalSelectors.forEach((selector) => {
@@ -20,7 +22,7 @@ describe('Form component', () => {
20
22
  let server;
21
23
  let apiServices;
22
24
  beforeAll(() => {
23
- server = setupServer(http.get('/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/data/objects/license/instances', () => {
25
+ server = setupServer(http.get('/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('accessManagement/users', () => HttpResponse.json(users)), http.get('/data/objects/license/instances', () => {
24
26
  return HttpResponse.json([rnLicense, npLicense]);
25
27
  }), http.get('/data/objects/specialtyType/instances', (req) => {
26
28
  const filter = new URL(req.request.url).searchParams.get('filter');
@@ -155,4 +157,192 @@ describe('Form component', () => {
155
157
  expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).to.be.null;
156
158
  });
157
159
  });
160
+ describe('508 accessibility compliance', () => {
161
+ it('supports keyboard navigation back and forth through Related Object dropdowns', async () => {
162
+ const user = userEvent.setup();
163
+ render(React.createElement(Form, { actionId: '_update1', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
164
+ await waitFor(() => {
165
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
166
+ });
167
+ await user.tab();
168
+ // Name field should be focused
169
+ expect(screen.getByLabelText('Name')).toHaveFocus();
170
+ await user.tab();
171
+ // License should be focused
172
+ expect(screen.getByLabelText('License')).toHaveFocus();
173
+ // Check reverse tabbing
174
+ await user.tab({ shift: true });
175
+ expect(screen.getByLabelText('Name')).toHaveFocus();
176
+ });
177
+ it('supports keyboard navigation back and forth through User dropdowns', async () => {
178
+ const user = userEvent.setup();
179
+ render(React.createElement(Form, { actionId: '_update2', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
180
+ await waitFor(() => {
181
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
182
+ });
183
+ await user.tab();
184
+ // Name field should be focused
185
+ await waitFor(() => {
186
+ expect(screen.getByLabelText('Name')).toHaveFocus();
187
+ });
188
+ await user.tab();
189
+ // User should be focused
190
+ expect(screen.getByLabelText('User')).toHaveFocus();
191
+ // Check reverse tabbing
192
+ await user.tab({ shift: true });
193
+ expect(screen.getByLabelText('Name')).toHaveFocus();
194
+ });
195
+ it('supports keyboard selection of dropdown values using Enter key on Related Objects', async () => {
196
+ const user = userEvent.setup();
197
+ render(React.createElement(Form, { actionId: '_update1', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
198
+ await waitFor(() => {
199
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
200
+ });
201
+ // Navigate to License field
202
+ await user.tab();
203
+ await user.tab();
204
+ // Open dropdown with Enter
205
+ await user.keyboard('{Enter}');
206
+ // Navigate to first option
207
+ await user.keyboard('{ArrowDown}');
208
+ // Select option with Enter
209
+ await user.keyboard('{Enter}');
210
+ await waitFor(() => {
211
+ const input = screen.getByRole('combobox', { name: 'License' });
212
+ expect(input).toHaveValue('RN License');
213
+ });
214
+ });
215
+ it('supports keyboard selection of dropdown values using Enter key on Users', async () => {
216
+ const user = userEvent.setup();
217
+ render(React.createElement(Form, { actionId: '_update2', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
218
+ await waitFor(() => {
219
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
220
+ });
221
+ // Navigate to License field
222
+ await user.tab();
223
+ await user.tab();
224
+ // Open dropdown with Enter
225
+ await user.keyboard('{Enter}');
226
+ // Navigate to first option
227
+ await user.keyboard('{ArrowDown}');
228
+ // Select option with Enter
229
+ await user.keyboard('{Enter}');
230
+ await waitFor(() => {
231
+ const input = screen.getByRole('combobox', { name: 'User' });
232
+ expect(input).toHaveValue('User 1');
233
+ });
234
+ });
235
+ it('supports navigating between dropdown options using arrow keys on Related Objects', async () => {
236
+ const user = userEvent.setup();
237
+ render(React.createElement(Form, { actionId: '_update1', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
238
+ await waitFor(() => {
239
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
240
+ });
241
+ // Navigate to and open dropdown
242
+ await user.tab();
243
+ await user.tab();
244
+ await user.keyboard('{ArrowDown}'); // Open dropdown
245
+ await user.keyboard('{ArrowDown}'); // First option
246
+ await user.keyboard('{Enter}');
247
+ // Verify first selection
248
+ await waitFor(() => {
249
+ const input = screen.getByRole('combobox', { name: 'License' });
250
+ expect(input).toHaveValue('RN License');
251
+ });
252
+ });
253
+ it('supports navigating between dropdown options using arrow keys on User dropdowns', async () => {
254
+ const user = userEvent.setup();
255
+ render(React.createElement(Form, { actionId: '_update2', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
256
+ await waitFor(() => {
257
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
258
+ });
259
+ // Navigate to and open dropdown
260
+ await user.tab();
261
+ await user.tab();
262
+ await user.keyboard('{ArrowDown}'); // Open dropdown
263
+ await user.keyboard('{ArrowDown}'); // First option
264
+ await user.keyboard('{Enter}');
265
+ // Verify first selection
266
+ await waitFor(() => {
267
+ const input = screen.getByRole('combobox', { name: 'User' });
268
+ expect(input).toHaveValue('User 1');
269
+ });
270
+ });
271
+ it('supports clearing selection with the clear button on Related Objects', async () => {
272
+ const user = userEvent.setup();
273
+ render(React.createElement(Form, { actionId: '_update1', actionType: 'update', object: accessibility508Object, apiServices: apiServices, instance: {
274
+ id: '123',
275
+ objectId: 'accessibility508',
276
+ name: 'Test Accessibility 508 Object Instance',
277
+ license: {
278
+ id: 'rnLicense',
279
+ name: 'RN License',
280
+ },
281
+ } }));
282
+ // Set up a selection first
283
+ await waitFor(() => {
284
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
285
+ });
286
+ await waitFor(() => {
287
+ const input = screen.getByRole('combobox', { name: 'License' });
288
+ expect(input).toHaveValue('RN License');
289
+ });
290
+ // Manually focus the clear button since test environment can't reach it via tab,
291
+ // even though tabbing works correctly in the actual form
292
+ const clearButton = screen.getByRole('button', { name: 'Clear selection' });
293
+ clearButton.focus();
294
+ expect(clearButton).toHaveFocus();
295
+ await user.keyboard('{Enter}');
296
+ await waitFor(() => {
297
+ const input = screen.getByRole('combobox', { name: 'License' });
298
+ expect(input).toHaveValue('');
299
+ });
300
+ });
301
+ it('supports clearing selection with the clear button on User dropdowns', async () => {
302
+ const user = userEvent.setup();
303
+ render(React.createElement(Form, { actionId: '_update2', actionType: 'update', object: accessibility508Object, apiServices: apiServices, instance: {
304
+ id: '123',
305
+ objectId: 'accessibility508',
306
+ name: 'Test Accessibility 508 Object Instance',
307
+ user: {
308
+ id: 'user1',
309
+ name: 'User 1',
310
+ },
311
+ } }));
312
+ // Set up a selection first
313
+ await waitFor(() => {
314
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
315
+ });
316
+ await waitFor(() => {
317
+ const input = screen.getByRole('combobox', { name: 'User' });
318
+ expect(input).toHaveValue('User 1');
319
+ });
320
+ // Manually focus the clear button since test environment can't reach it via tab,
321
+ // even though tabbing works correctly in the actual form
322
+ const clearButton = screen.getByRole('button', { name: 'Clear selection' });
323
+ clearButton.focus();
324
+ expect(clearButton).toHaveFocus();
325
+ await user.keyboard('{Enter}');
326
+ await waitFor(() => {
327
+ const input = screen.getByRole('combobox', { name: 'User' });
328
+ expect(input).toHaveValue('');
329
+ });
330
+ });
331
+ it('supports navigating to Add New option with arrow keys and opens modal', async () => {
332
+ const user = userEvent.setup();
333
+ render(React.createElement(Form, { actionId: '_update1', actionType: 'update', object: accessibility508Object, apiServices: apiServices }));
334
+ await waitFor(() => {
335
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
336
+ });
337
+ // Navigate to and open dropdown
338
+ await user.tab();
339
+ await user.tab();
340
+ await user.keyboard('{Enter}');
341
+ await user.keyboard('{ArrowUp}'); // Navigate to "Add New" option
342
+ await user.keyboard('{Enter}');
343
+ await waitFor(() => {
344
+ expect(screen.getByRole('dialog', { name: /add license/i })).toBeInTheDocument();
345
+ });
346
+ });
347
+ });
158
348
  });
@@ -1,6 +1,7 @@
1
1
  import { Obj, ObjectInstance } from '@evoke-platform/context';
2
2
  export declare const licenseObject: Obj;
3
3
  export declare const licenseTypeObject: Obj;
4
+ export declare const accessibility508Object: Obj;
4
5
  export declare const specialtyObject: Obj;
5
6
  export declare const specialtyTypeObject: Obj;
6
7
  export declare const rnLicense: ObjectInstance;
@@ -11,3 +12,9 @@ export declare const rnSpecialtyType1: ObjectInstance;
11
12
  export declare const rnSpecialtyType2: ObjectInstance;
12
13
  export declare const npSpecialtyType1: ObjectInstance;
13
14
  export declare const npSpecialtyType2: ObjectInstance;
15
+ export declare const users: {
16
+ id: string;
17
+ status: string;
18
+ email: string;
19
+ name: string;
20
+ }[];