@evoke-platform/ui-components 1.8.0-dev.1 → 1.8.0-dev.11

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 (52) hide show
  1. package/dist/published/components/core/TextField/TextField.js +3 -2
  2. package/dist/published/components/custom/DataGrid/DataGrid.d.ts +1 -0
  3. package/dist/published/components/custom/DataGrid/DataGrid.js +2 -1
  4. package/dist/published/components/custom/DataGrid/Toolbar.d.ts +1 -0
  5. package/dist/published/components/custom/DataGrid/Toolbar.js +3 -2
  6. package/dist/published/components/custom/DataGrid/index.d.ts +1 -0
  7. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +48 -39
  8. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/RelatedObjectInstance.js +1 -1
  9. package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.d.ts +1 -1
  10. package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.js +22 -6
  11. package/dist/published/components/custom/Form/tests/Form.test.js +192 -2
  12. package/dist/published/components/custom/Form/tests/test-data.d.ts +7 -0
  13. package/dist/published/components/custom/Form/tests/test-data.js +138 -0
  14. package/dist/published/components/custom/Form/utils.js +76 -44
  15. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +6 -2
  16. package/dist/published/components/custom/FormV2/FormRenderer.js +13 -14
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +7 -2
  18. package/dist/published/components/custom/FormV2/FormRendererContainer.js +61 -109
  19. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +4 -0
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +9 -5
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +12 -24
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +5 -1
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +80 -30
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +1 -1
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +51 -27
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +5 -5
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +45 -7
  29. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -6
  30. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +3 -0
  31. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +1 -3
  32. package/dist/published/components/custom/FormV2/components/types.d.ts +7 -1
  33. package/dist/published/components/custom/FormV2/components/utils.d.ts +27 -2
  34. package/dist/published/components/custom/FormV2/components/utils.js +108 -2
  35. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.d.ts +1 -0
  36. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +173 -0
  37. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.d.ts +1 -0
  38. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +96 -0
  39. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +16 -0
  40. package/dist/published/components/custom/FormV2/tests/test-data.js +394 -0
  41. package/dist/published/components/custom/index.d.ts +1 -0
  42. package/dist/published/index.d.ts +1 -1
  43. package/dist/published/stories/FormRenderer.stories.d.ts +7 -0
  44. package/dist/published/stories/FormRenderer.stories.js +65 -0
  45. package/dist/published/stories/FormRendererContainer.stories.d.ts +7 -0
  46. package/dist/published/stories/FormRendererContainer.stories.js +56 -0
  47. package/dist/published/stories/FormRendererData.d.ts +116 -0
  48. package/dist/published/stories/FormRendererData.js +925 -0
  49. package/dist/published/stories/sharedMswHandlers.d.ts +1 -0
  50. package/dist/published/stories/sharedMswHandlers.js +100 -0
  51. package/dist/published/theme/hooks.d.ts +4 -0
  52. package/package.json +12 -4
@@ -4,7 +4,8 @@ import UIThemeProvider from '../../../theme';
4
4
  import FieldError from '../FieldError';
5
5
  import Typography from '../Typography';
6
6
  const TextField = (props) => {
7
- const { id, variant, label, labelPlacement, readOnly, required, error, instructionText, errorMessage } = props;
7
+ const { labelPlacement, readOnly, instructionText, errorMessage, ...muiProps } = props;
8
+ const { id, variant, label, required, error } = muiProps;
8
9
  const readOnlyStyles = {
9
10
  '.MuiOutlinedInput-root': {
10
11
  paddingRight: '5px',
@@ -28,7 +29,7 @@ const TextField = (props) => {
28
29
  readOnly: readOnly,
29
30
  'aria-readonly': !!readOnly,
30
31
  'data-testid': 'label-outside',
31
- }, ...props, label: null, sx: readOnly
32
+ }, ...muiProps, label: null, sx: readOnly
32
33
  ? { ...readOnlyStyles, ...props.sx }
33
34
  : {
34
35
  '& fieldset': { borderRadius: '8px' },
@@ -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,11 +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) => {
235
+ // prevents keyboard trap
236
+ if (e.key === 'Tab') {
237
+ return;
238
+ }
239
+ if (e.key === 'Enter') {
240
+ setOpenOptions(true);
241
+ setDropdownInput(undefined);
242
+ return;
243
+ }
233
244
  if (instance?.[property.id]?.id || selectedInstance?.id) {
234
245
  e.preventDefault();
235
246
  }
@@ -240,12 +251,19 @@ export const ObjectPropertyInput = (props) => {
240
251
  handleChangeObjectProperty(property.id, null);
241
252
  instance[property.id] = null;
242
253
  }
254
+ else if (value?.value === '__new__') {
255
+ setOpenCreateDialog(true);
256
+ }
243
257
  else {
244
258
  const selectedInstance = options.find((o) => o.id === value?.value);
245
259
  setSelectedInstance(selectedInstance);
246
260
  handleChangeObjectProperty(property.id, selectedInstance);
247
261
  }
248
- }, 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 &&
249
267
  !!(instance?.[property.id]?.id ||
250
268
  selectedInstance?.id), onChange: (event) => setDropdownInput(event.target.value), onClick: (e) => {
251
269
  if (navigationSlug &&
@@ -257,15 +275,6 @@ export const ObjectPropertyInput = (props) => {
257
275
  selectedInstance?.id ??
258
276
  ''));
259
277
  }
260
- if (openOptions &&
261
- e.target?.nodeName === 'svg') {
262
- setOpenOptions(false);
263
- }
264
- else if (!['DIV', 'INPUT'].includes(e.target?.nodeName) &&
265
- !selectedInstance) {
266
- setOptions(options);
267
- setOpenOptions(true);
268
- }
269
278
  }, sx: {
270
279
  ...(!loadingOptions &&
271
280
  (instance?.[property.id]?.id ||
@@ -345,8 +354,8 @@ export const ObjectPropertyInput = (props) => {
345
354
  event.stopPropagation();
346
355
  setOpenCreateDialog(true);
347
356
  }, "aria-label": `Add`, disabled: !canUpdateProperty }, "Add")))),
348
- 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 },
349
- React.createElement(Typography, { sx: {
357
+ 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" },
358
+ React.createElement(Typography, { id: "add-dialog-title", sx: {
350
359
  marginTop: '28px',
351
360
  fontSize: '22px',
352
361
  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 = {
@@ -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,7 +96,15 @@ export const UserProperty = (props) => {
88
96
  }
89
97
  }
90
98
  }
91
- }, onKeyDownCapture: (e) => {
99
+ }, onKeyDown: (e) => {
100
+ // prevents keyboard trap
101
+ if (e.key === 'Tab') {
102
+ return;
103
+ }
104
+ if (e.key === 'Enter') {
105
+ setOpenOptions(true);
106
+ return;
107
+ }
92
108
  if (value) {
93
109
  e.preventDefault();
94
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
+ }[];