@evoke-platform/ui-components 1.8.2-testing.0 → 1.9.0-dev.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 (84) hide show
  1. package/dist/published/components/custom/FormV2/FormRenderer.js +19 -16
  2. package/dist/published/components/custom/FormV2/FormRendererContainer.js +16 -4
  3. package/dist/published/components/custom/FormV2/components/AccordionSections.js +30 -29
  4. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +1 -1
  5. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +1 -2
  6. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +16 -7
  7. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +88 -39
  8. package/dist/published/components/custom/FormV2/components/FormSections.js +34 -3
  9. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +10 -29
  10. package/dist/published/components/custom/FormV2/components/ValidationFiles/Validation.js +2 -2
  11. package/dist/published/components/custom/FormV2/components/types.d.ts +9 -1
  12. package/dist/published/components/custom/FormV2/components/utils.d.ts +18 -2
  13. package/dist/published/components/custom/FormV2/components/utils.js +163 -1
  14. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +211 -2
  15. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +9 -0
  16. package/dist/published/components/custom/FormV2/tests/test-data.js +134 -0
  17. package/dist/published/stories/Accordion.stories.d.ts +36 -4
  18. package/dist/published/stories/Alert.stories.d.ts +2 -4
  19. package/dist/published/stories/AlertTitle.stories.d.ts +2 -4
  20. package/dist/published/stories/Appbar.stories.d.ts +10 -3
  21. package/dist/published/stories/Autocomplete.stories.d.ts +22 -4
  22. package/dist/published/stories/Avatar.stories.d.ts +16 -3
  23. package/dist/published/stories/Backdrop.stories.d.ts +10 -4
  24. package/dist/published/stories/Badge.stories.d.ts +10 -4
  25. package/dist/published/stories/Box.stories.d.ts +2 -3
  26. package/dist/published/stories/Breadcrumbs.stories.d.ts +10 -4
  27. package/dist/published/stories/BuilderGrid.stories.d.ts +54 -5
  28. package/dist/published/stories/Button.stories.d.ts +10 -4
  29. package/dist/published/stories/ButtonGroup.stories.d.ts +10 -4
  30. package/dist/published/stories/Card.stories.d.ts +10 -4
  31. package/dist/published/stories/Checkbox.stories.d.ts +2 -4
  32. package/dist/published/stories/Chip.stories.d.ts +10 -4
  33. package/dist/published/stories/CircularProgress.stories.d.ts +2 -4
  34. package/dist/published/stories/Collapse.stories.d.ts +2 -4
  35. package/dist/published/stories/Container.stories.d.ts +10 -4
  36. package/dist/published/stories/CriteriaBuilder.stories.d.ts +6 -8
  37. package/dist/published/stories/DataGrid.stories.d.ts +40 -4
  38. package/dist/published/stories/DatePicker.stories.d.ts +7 -4
  39. package/dist/published/stories/Dialog.stories.d.ts +2 -4
  40. package/dist/published/stories/Divider.stories.d.ts +10 -4
  41. package/dist/published/stories/Drawer.stories.d.ts +2 -4
  42. package/dist/published/stories/Form.stories.d.ts +4 -5
  43. package/dist/published/stories/FormControl.stories.d.ts +10 -4
  44. package/dist/published/stories/FormControlLabel.stories.d.ts +2 -4
  45. package/dist/published/stories/FormField.stories.d.ts +11 -13
  46. package/dist/published/stories/FormGroup.stories.d.ts +2 -4
  47. package/dist/published/stories/FormHelperText.stories.d.ts +10 -4
  48. package/dist/published/stories/FormLabel.stories.d.ts +10 -4
  49. package/dist/published/stories/FormRenderer.stories.d.ts +69 -6
  50. package/dist/published/stories/FormRendererContainer.stories.d.ts +111 -6
  51. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  52. package/dist/published/stories/FormRendererData.d.ts +7 -0
  53. package/dist/published/stories/FormRendererData.js +172 -1
  54. package/dist/published/stories/Grid.stories.d.ts +10 -4
  55. package/dist/published/stories/HistoryLog.stories.d.ts +2 -4
  56. package/dist/published/stories/IconButton.stories.d.ts +10 -4
  57. package/dist/published/stories/LinearProgress.stories.d.ts +2 -4
  58. package/dist/published/stories/Link.stories.d.ts +10 -4
  59. package/dist/published/stories/List.stories.d.ts +10 -4
  60. package/dist/published/stories/Menu.stories.d.ts +2 -4
  61. package/dist/published/stories/MenuBar.stories.d.ts +3 -4
  62. package/dist/published/stories/MultiSelect.stories.d.ts +3 -4
  63. package/dist/published/stories/OverflowTextField.stories.d.ts +2 -4
  64. package/dist/published/stories/Palette.stories.d.ts +2 -3
  65. package/dist/published/stories/Paper.stories.d.ts +10 -4
  66. package/dist/published/stories/RadioGroup.stories.d.ts +2 -4
  67. package/dist/published/stories/RepeatableField.stories.d.ts +3 -4
  68. package/dist/published/stories/ResponsiveOverflow.stories.d.ts +3 -4
  69. package/dist/published/stories/RichTextViewer.stories.d.ts +2 -4
  70. package/dist/published/stories/Skeleton.stories.d.ts +10 -4
  71. package/dist/published/stories/Snackbar.stories.d.ts +9 -3
  72. package/dist/published/stories/Stack.stories.d.ts +10 -4
  73. package/dist/published/stories/StaticDatePicker.stories.d.ts +10 -5
  74. package/dist/published/stories/Stepper.stories.d.ts +12 -6
  75. package/dist/published/stories/Switch.stories.d.ts +2 -4
  76. package/dist/published/stories/Table.stories.d.ts +10 -4
  77. package/dist/published/stories/Tabs.stories.d.ts +10 -4
  78. package/dist/published/stories/TextField.stories.d.ts +6 -8
  79. package/dist/published/stories/TimePicker.stories.d.ts +2 -7
  80. package/dist/published/stories/TimePickerSelect.stories.d.ts +4 -6
  81. package/dist/published/stories/ToggleButton.stories.d.ts +10 -4
  82. package/dist/published/stories/Typography.stories.d.ts +2 -4
  83. package/dist/published/stories/sharedMswHandlers.js +5 -1
  84. package/package.json +17 -16
@@ -64,13 +64,13 @@ export const handleValidation = (entries, register, formValues, parameters, inst
64
64
  };
65
65
  }
66
66
  // Min/max number fields
67
- if (isNumericValidation(validation) && validation.maximum) {
67
+ if (isNumericValidation(validation) && typeof validation.maximum === 'number') {
68
68
  validationRules.max = {
69
69
  value: validation.maximum,
70
70
  message: errorMsg || `${fieldName} must have a value under ${validation.maximum}`,
71
71
  };
72
72
  }
73
- if (isNumericValidation(validation) && validation.minimum) {
73
+ if (isNumericValidation(validation) && typeof validation.minimum === 'number') {
74
74
  validationRules.min = {
75
75
  value: validation.minimum,
76
76
  message: errorMsg || `${fieldName} must have a value over ${validation.minimum}`,
@@ -83,6 +83,11 @@ export type BaseProps = {
83
83
  export type ExpandedSection = Section & {
84
84
  id: string;
85
85
  expanded?: boolean;
86
+ isNested?: boolean;
87
+ };
88
+ export type ExpandedSections = Omit<Sections, 'sections'> & {
89
+ sections: ExpandedSection[];
90
+ id: string;
86
91
  };
87
92
  export type EntryRendererProps = BaseProps & {
88
93
  entry: FormEntry;
@@ -93,7 +98,7 @@ export type EntryRendererProps = BaseProps & {
93
98
  };
94
99
  };
95
100
  export type SectionsProps = {
96
- entry: Sections;
101
+ entry: ExpandedSections;
97
102
  };
98
103
  export type DocumentData = {
99
104
  id: string;
@@ -105,3 +110,6 @@ export type DocumentData = {
105
110
  type: string;
106
111
  view_permission: string;
107
112
  };
113
+ export type RichTextFormEntry = FormEntry & {
114
+ uniqueId: string;
115
+ };
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { Action, ApiServices, Column, Columns, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
2
+ import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { FieldErrors, FieldValues } from 'react-hook-form';
5
5
  import { AutocompleteOption } from '../../../core';
@@ -81,8 +81,24 @@ export declare const deleteDocuments: (submittedFields: FieldValues, requestSucc
81
81
  *
82
82
  * Returns the cleaned submission ready for submitting.
83
83
  */
84
- export declare function formatSubmission(submission: FieldValues, apiServices?: ApiServices, objectId?: string, instanceId?: string, setSnackbarError?: React.Dispatch<React.SetStateAction<{
84
+ export declare function formatSubmission(submission: FieldValues, apiServices?: ApiServices, objectId?: string, instanceId?: string, form?: EvokeForm, setSnackbarError?: React.Dispatch<React.SetStateAction<{
85
85
  showAlert: boolean;
86
86
  message?: string;
87
87
  isError: boolean;
88
88
  }>>): Promise<FieldValues>;
89
+ export declare function filterEmptySections(entry: Sections | Columns, formData: FieldValues, instance?: FieldValues): Sections | Columns | null;
90
+ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object?: Obj, parameters?: InputParameter[]): FormEntry[];
91
+ /**
92
+ * Converts a plain text string to RTF format suitable for a RichTextEditor.
93
+ *
94
+ * Steps performed:
95
+ * 1. If the input is empty, returns a minimal RTF document with default font and size.
96
+ * 2. Escapes special RTF characters: backslashes, braces, line breaks, and tabs.
97
+ * 3. Removes carriage returns (\r) as they are unnecessary in RTF.
98
+ * 4. Converts non-ASCII characters (> 127) to Unicode escapes (\uN?) to preserve encoding.
99
+ * 5. Converts control characters (0-31, except \t, \n) to Unicode escapes to avoid RTF parsing errors.
100
+ *
101
+ * This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
102
+ */
103
+ export declare function plainTextToRtf(plainText: string): string;
104
+ export declare function getFieldDefinition(entry: FormEntry, object?: Obj, parameters?: InputParameter[], isDocument?: boolean): InputParameter | Property | undefined;
@@ -2,6 +2,7 @@ import { LocalDateTime } from '@js-joda/core';
2
2
  import jsonLogic from 'json-logic-js';
3
3
  import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
4
4
  import { DateTime } from 'luxon';
5
+ import { nanoid } from 'nanoid';
5
6
  import Handlebars from 'no-eval-handlebars';
6
7
  import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB } from 'react-querybuilder';
7
8
  export const scrollIntoViewWithOffset = (el, offset, container) => {
@@ -624,15 +625,18 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
624
625
  *
625
626
  * Returns the cleaned submission ready for submitting.
626
627
  */
627
- export async function formatSubmission(submission, apiServices, objectId, instanceId, setSnackbarError) {
628
+ export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
628
629
  for (const [key, value] of Object.entries(submission)) {
629
630
  if (isArray(value)) {
630
631
  const fileInArray = value.some((item) => item instanceof File);
631
632
  if (fileInArray && instanceId && apiServices && objectId) {
632
633
  try {
634
+ const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
635
+ const entry = allEntries?.find((entry) => getEntryId(entry) === key);
633
636
  const uploadedDocuments = await uploadDocuments(value, {
634
637
  type: '',
635
638
  view_permission: '',
639
+ ...entry?.documentMetadata,
636
640
  }, apiServices, instanceId, objectId);
637
641
  submission[key] = uploadedDocuments;
638
642
  }
@@ -668,3 +672,161 @@ export async function formatSubmission(submission, apiServices, objectId, instan
668
672
  }
669
673
  return submission;
670
674
  }
675
+ export function filterEmptySections(entry, formData, instance) {
676
+ if (entry.type === 'sections' && isArray(entry.sections)) {
677
+ const visibleSections = entry.sections.filter((section) => {
678
+ if (!section.entries || section.entries.length === 0)
679
+ return false;
680
+ for (const sectionEntry of section.entries) {
681
+ if (sectionEntry.type === 'sections' || sectionEntry.type === 'columns') {
682
+ if (sectionEntry.visibility && !entryIsVisible(sectionEntry, formData, instance)) {
683
+ return false;
684
+ }
685
+ else if (filterEmptySections(sectionEntry, formData, instance)) {
686
+ return true;
687
+ }
688
+ }
689
+ else if (entryIsVisible(sectionEntry, formData, instance)) {
690
+ return true;
691
+ }
692
+ }
693
+ return false;
694
+ });
695
+ if (visibleSections.length === 0)
696
+ return null;
697
+ return {
698
+ ...entry,
699
+ sections: visibleSections,
700
+ };
701
+ }
702
+ if (entry.type === 'columns' && isArray(entry.columns)) {
703
+ const visibleColumns = entry.columns.filter((column) => {
704
+ if (!column.entries || column.entries.length === 0)
705
+ return false;
706
+ let hasVisibleEntry = false;
707
+ for (const columnEntry of column.entries) {
708
+ if (columnEntry.type === 'sections' || columnEntry.type === 'columns') {
709
+ if (filterEmptySections(columnEntry, formData, instance)) {
710
+ hasVisibleEntry = true;
711
+ break;
712
+ }
713
+ }
714
+ else {
715
+ if (entryIsVisible(columnEntry, formData, instance)) {
716
+ hasVisibleEntry = true;
717
+ break;
718
+ }
719
+ }
720
+ }
721
+ return hasVisibleEntry;
722
+ });
723
+ if (visibleColumns.length === 0)
724
+ return null;
725
+ return {
726
+ ...entry,
727
+ columns: visibleColumns,
728
+ };
729
+ }
730
+ return entry;
731
+ }
732
+ export function assignIdsToSectionsAndRichText(entries, object, parameters) {
733
+ return entries.map((entry) => {
734
+ if (entry.type === 'columns' && isArray(entry.columns)) {
735
+ return {
736
+ ...entry,
737
+ columns: entry.columns.map((column) => ({
738
+ ...column,
739
+ entries: isArray(column.entries)
740
+ ? assignIdsToSectionsAndRichText(column.entries, object, parameters)
741
+ : [],
742
+ })),
743
+ };
744
+ }
745
+ if (entry.type === 'sections' && isArray(entry.sections)) {
746
+ return {
747
+ ...entry,
748
+ sections: entry.sections.map((section) => ({
749
+ ...section,
750
+ id: nanoid(),
751
+ entries: isArray(section.entries)
752
+ ? assignIdsToSectionsAndRichText(section.entries, object, parameters)
753
+ : [],
754
+ })),
755
+ id: nanoid(),
756
+ };
757
+ }
758
+ // Assign a unique id to each RichText field to avoid conflicts when rendering more than one
759
+ const fieldDefinition = getFieldDefinition(entry, object, parameters);
760
+ if (fieldDefinition && fieldDefinition.type === 'richText') {
761
+ return {
762
+ ...entry,
763
+ uniqueId: nanoid(),
764
+ };
765
+ }
766
+ return entry;
767
+ });
768
+ }
769
+ /**
770
+ * Converts a plain text string to RTF format suitable for a RichTextEditor.
771
+ *
772
+ * Steps performed:
773
+ * 1. If the input is empty, returns a minimal RTF document with default font and size.
774
+ * 2. Escapes special RTF characters: backslashes, braces, line breaks, and tabs.
775
+ * 3. Removes carriage returns (\r) as they are unnecessary in RTF.
776
+ * 4. Converts non-ASCII characters (> 127) to Unicode escapes (\uN?) to preserve encoding.
777
+ * 5. Converts control characters (0-31, except \t, \n) to Unicode escapes to avoid RTF parsing errors.
778
+ *
779
+ * This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
780
+ */
781
+ export function plainTextToRtf(plainText) {
782
+ if (!plainText) {
783
+ return `{\\rtf1\\ansi\\deff0 {\\fonttbl {\\f0 Calibri;}} \\f0\\fs22 }`;
784
+ }
785
+ let escaped = plainText
786
+ .replace(/\\/g, '\\\\')
787
+ .replace(/{/g, '\\{')
788
+ .replace(/}/g, '\\}')
789
+ .replace(/\n/g, '\\par\n')
790
+ .replace(/\t/g, '\\tab ')
791
+ .replace(/\r/g, '');
792
+ // Handle non-ASCII characters (> 127) with Unicode escapes
793
+ escaped = escaped.replace(/[\u0080-\uffff]/g, (char) => {
794
+ const code = char.charCodeAt(0);
795
+ if (code > 32767) {
796
+ return `\\u${code - 65536}?`;
797
+ }
798
+ return `\\u${code}?`;
799
+ });
800
+ // Handle control characters (0-31 except \t, \n, \r which we already handled)
801
+ // eslint-disable-next-line no-control-regex
802
+ escaped = escaped.replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, (char) => {
803
+ return `\\u${char.charCodeAt(0)}?`;
804
+ });
805
+ return `{\\rtf1\\ansi\\deff0 {\\fonttbl {\\f0 Calibri;}} \\f0\\fs22 ${escaped}}`;
806
+ }
807
+ export function getFieldDefinition(entry, object, parameters, isDocument) {
808
+ let def;
809
+ if (entry.type === 'input') {
810
+ def = parameters?.find((param) => param.id === entry.parameterId);
811
+ }
812
+ else if (entry.type === 'readonlyField') {
813
+ def = isDocument
814
+ ? docProperties.find((prop) => prop.id === entry.propertyId)
815
+ : isAddressProperty(entry.propertyId)
816
+ ? object?.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
817
+ : object?.properties?.find((prop) => prop.id === entry.propertyId);
818
+ }
819
+ else if (entry.type === 'inputField') {
820
+ def = entry.input;
821
+ }
822
+ else {
823
+ return undefined;
824
+ }
825
+ if (def?.enum && def.type === 'string') {
826
+ const cloned = structuredClone(def);
827
+ // single select must be made to be type choices for label and error handling
828
+ cloned.type = 'choices';
829
+ return cloned;
830
+ }
831
+ return def;
832
+ }
@@ -8,7 +8,7 @@ import React from 'react';
8
8
  import { MemoryRouter } from 'react-router-dom';
9
9
  import { expect, it } from 'vitest';
10
10
  import FormRenderer from '../FormRenderer';
11
- import { createSpecialtyForm, jsonLogicDisplayTestSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, simpleConditionDisplayTestSpecialtyForm, specialtyObject, specialtyTypeObject, } from './test-data';
11
+ import { accessibility508Object, createSpecialtyForm, jsonLogicDisplayTestSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, simpleConditionDisplayTestSpecialtyForm, specialtyObject, specialtyTypeObject, UpdateAccessibilityFormOne, UpdateAccessibilityFormTwo, users, } from './test-data';
12
12
  expect.extend(matchers);
13
13
  const removePoppers = () => {
14
14
  const portalSelectors = ['.MuiAutocomplete-popper'];
@@ -35,6 +35,11 @@ describe('Form component', () => {
35
35
  if (sanitizedVersion === 'true') {
36
36
  return HttpResponse.json(specialtyObject);
37
37
  }
38
+ }), http.get('/api/data/objects/accessibility508Object/effective', (req) => {
39
+ const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
40
+ if (sanitizedVersion === 'true') {
41
+ return HttpResponse.json(accessibility508Object);
42
+ }
38
43
  }), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/api/data/objects/license/instances', () => {
39
44
  return HttpResponse.json([rnLicense, npLicense]);
40
45
  }), http.get('/api/data/objects/specialtyType/instances', (req) => {
@@ -56,7 +61,7 @@ describe('Form component', () => {
56
61
  else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'npLicenseType' }, {}] }))
57
62
  return HttpResponse.json([npSpecialtyType1, npSpecialtyType2]);
58
63
  }
59
- }));
64
+ }), http.get('/api/accessManagement/users', () => HttpResponse.json(users)));
60
65
  server.listen();
61
66
  });
62
67
  afterAll(() => {
@@ -170,4 +175,208 @@ describe('Form component', () => {
170
175
  expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).not.toBeInTheDocument();
171
176
  });
172
177
  });
178
+ // accessibility508Form2
179
+ describe('508 accessibility compliance', () => {
180
+ it('supports keyboard navigation back and forth through Related Object dropdowns', async () => {
181
+ const user = userEvent.setup();
182
+ render(React.createElement(MemoryRouter, null,
183
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { } }),
184
+ ","));
185
+ await waitFor(() => {
186
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
187
+ });
188
+ await user.tab();
189
+ // Name field should be focused
190
+ expect(screen.getByLabelText('Name')).toHaveFocus();
191
+ await user.tab();
192
+ // License should be focused
193
+ expect(screen.getByLabelText('License')).toHaveFocus();
194
+ // Check reverse tabbing
195
+ await user.tab({ shift: true });
196
+ expect(screen.getByLabelText('Name')).toHaveFocus();
197
+ });
198
+ it('supports keyboard navigation back and forth through User dropdowns', async () => {
199
+ const user = userEvent.setup();
200
+ render(React.createElement(MemoryRouter, null,
201
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormTwo, onChange: () => { } }),
202
+ ","));
203
+ await waitFor(() => {
204
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
205
+ });
206
+ await user.tab();
207
+ // Name field should be focused
208
+ await waitFor(() => {
209
+ expect(screen.getByLabelText('Name')).toHaveFocus();
210
+ });
211
+ await user.tab();
212
+ // User should be focused
213
+ expect(screen.getByLabelText('User')).toHaveFocus();
214
+ // Check reverse tabbing
215
+ await user.tab({ shift: true });
216
+ expect(screen.getByLabelText('Name')).toHaveFocus();
217
+ });
218
+ it('supports keyboard selection of dropdown values using Enter key on Related Objects', async () => {
219
+ const user = userEvent.setup();
220
+ render(React.createElement(MemoryRouter, null,
221
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { } }),
222
+ ","));
223
+ await waitFor(() => {
224
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
225
+ });
226
+ // Navigate to License field
227
+ await user.tab();
228
+ await user.tab();
229
+ // Open dropdown with Enter
230
+ await user.keyboard('{Enter}');
231
+ // Navigate to first option
232
+ await user.keyboard('{ArrowDown}');
233
+ // Select option with Enter
234
+ await user.keyboard('{Enter}');
235
+ await waitFor(() => {
236
+ expect(screen.getByText('RN License')).toBeInTheDocument();
237
+ });
238
+ });
239
+ it('supports keyboard selection of dropdown values using Enter key on Users', async () => {
240
+ const user = userEvent.setup();
241
+ render(React.createElement(MemoryRouter, null,
242
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormTwo, onChange: () => { } }),
243
+ ","));
244
+ await waitFor(() => {
245
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
246
+ });
247
+ // Navigate to License field
248
+ await user.tab();
249
+ await user.tab();
250
+ // Open dropdown with Enter
251
+ await user.keyboard('{Enter}');
252
+ // Navigate to first option
253
+ await user.keyboard('{ArrowDown}');
254
+ // Select option with Enter
255
+ await user.keyboard('{Enter}');
256
+ await waitFor(() => {
257
+ const input = screen.getByRole('combobox', { name: 'User' });
258
+ expect(input).toHaveValue('User 1');
259
+ });
260
+ });
261
+ it('supports navigating between dropdown options using arrow keys on Related Objects', async () => {
262
+ const user = userEvent.setup();
263
+ render(React.createElement(MemoryRouter, null,
264
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { } }),
265
+ ","));
266
+ await waitFor(() => {
267
+ expect(screen.getByLabelText('License')).toBeInTheDocument();
268
+ });
269
+ // Navigate to and open dropdown
270
+ await user.tab();
271
+ await user.tab();
272
+ await user.keyboard('{ArrowDown}');
273
+ await waitFor(() => {
274
+ expect(screen.getByText('RN License')).toBeInTheDocument();
275
+ });
276
+ await user.keyboard('{ArrowDown}');
277
+ await user.keyboard('{Enter}');
278
+ // Verify first selection
279
+ await waitFor(() => {
280
+ expect(screen.getByText('RN License')).toBeInTheDocument();
281
+ });
282
+ });
283
+ it('supports navigating between dropdown options using arrow keys on User dropdowns', async () => {
284
+ const user = userEvent.setup();
285
+ render(React.createElement(MemoryRouter, null,
286
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormTwo, onChange: () => { } }),
287
+ ","));
288
+ await waitFor(() => {
289
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
290
+ });
291
+ // Navigate to and open dropdown
292
+ await user.tab();
293
+ await user.tab();
294
+ await user.keyboard('{ArrowDown}');
295
+ await user.keyboard('{ArrowDown}');
296
+ await user.keyboard('{Enter}');
297
+ // Verify first selection
298
+ await waitFor(() => {
299
+ const input = screen.getByRole('combobox', { name: 'User' });
300
+ expect(input).toHaveValue('User 1');
301
+ });
302
+ });
303
+ it('supports clearing selection with the clear button on Related Objects', async () => {
304
+ const user = userEvent.setup();
305
+ render(React.createElement(MemoryRouter, null,
306
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { }, value: {
307
+ id: '123',
308
+ objectId: 'accessibility508Object',
309
+ name: 'Test Accessibility 508 Object Instance',
310
+ license: {
311
+ id: 'rnLicense',
312
+ name: 'RN License',
313
+ },
314
+ } })));
315
+ // Set up a selection first
316
+ await waitFor(() => {
317
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
318
+ });
319
+ await waitFor(() => {
320
+ expect(screen.getByText('RN License')).toBeInTheDocument();
321
+ });
322
+ // Manually focus the clear button since test environment can't reach it via tab,
323
+ // even though tabbing works correctly in the actual form
324
+ const clearButton = screen.getByRole('button', { name: 'Clear selection' });
325
+ clearButton.focus();
326
+ expect(clearButton).toHaveFocus();
327
+ await user.keyboard('{Enter}');
328
+ await waitFor(() => {
329
+ expect(screen.queryByText('RN License')).not.toBeInTheDocument();
330
+ });
331
+ });
332
+ it('supports clearing selection with the clear button on User dropdowns', async () => {
333
+ const user = userEvent.setup();
334
+ render(React.createElement(MemoryRouter, null,
335
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormTwo, onChange: () => { }, value: {
336
+ id: '123',
337
+ objectId: 'accessibility508Object',
338
+ name: 'Test Accessibility 508 Object Instance',
339
+ user: {
340
+ id: 'user1',
341
+ name: 'User 1',
342
+ },
343
+ } })));
344
+ // Set up a selection first
345
+ await waitFor(() => {
346
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
347
+ });
348
+ await waitFor(() => {
349
+ const input = screen.getByRole('combobox', { name: 'User' });
350
+ expect(input).toHaveValue('User 1');
351
+ });
352
+ // Manually focus the clear button since test environment can't reach it via tab,
353
+ // even though tabbing works correctly in the actual form
354
+ const clearButton = screen.getByRole('button', { name: 'Clear selection' });
355
+ clearButton.focus();
356
+ expect(clearButton).toHaveFocus();
357
+ await user.keyboard('{Enter}');
358
+ await waitFor(() => {
359
+ const input = screen.getByRole('combobox', { name: 'User' });
360
+ expect(input).toHaveValue('');
361
+ });
362
+ });
363
+ it('supports navigating to Add New option with arrow keys and opens modal', async () => {
364
+ const user = userEvent.setup();
365
+ render(React.createElement(MemoryRouter, null,
366
+ React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { } }),
367
+ ","));
368
+ await waitFor(() => {
369
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
370
+ });
371
+ // Navigate to and open dropdown
372
+ await user.tab();
373
+ await user.tab();
374
+ await user.keyboard('{Enter}');
375
+ await user.keyboard('{ArrowUp}'); // Navigate to "Add New" option
376
+ await user.keyboard('{Enter}');
377
+ await waitFor(() => {
378
+ expect(screen.getByRole('dialog', { name: /add license/i })).toBeInTheDocument();
379
+ });
380
+ });
381
+ });
173
382
  });
@@ -2,10 +2,13 @@ import { EvokeForm, Obj, ObjectInstance } from '@evoke-platform/context';
2
2
  export declare const createSpecialtyForm: EvokeForm;
3
3
  export declare const simpleConditionDisplayTestSpecialtyForm: EvokeForm;
4
4
  export declare const jsonLogicDisplayTestSpecialtyForm: EvokeForm;
5
+ export declare const UpdateAccessibilityFormOne: EvokeForm;
6
+ export declare const UpdateAccessibilityFormTwo: EvokeForm;
5
7
  export declare const licenseObject: Obj;
6
8
  export declare const licenseTypeObject: Obj;
7
9
  export declare const specialtyObject: Obj;
8
10
  export declare const specialtyTypeObject: Obj;
11
+ export declare const accessibility508Object: Obj;
9
12
  export declare const rnLicense: ObjectInstance;
10
13
  export declare const npLicense: ObjectInstance;
11
14
  export declare const rnLicenseType: ObjectInstance;
@@ -14,3 +17,9 @@ export declare const rnSpecialtyType1: ObjectInstance;
14
17
  export declare const rnSpecialtyType2: ObjectInstance;
15
18
  export declare const npSpecialtyType1: ObjectInstance;
16
19
  export declare const npSpecialtyType2: ObjectInstance;
20
+ export declare const users: {
21
+ id: string;
22
+ status: string;
23
+ email: string;
24
+ name: string;
25
+ }[];
@@ -97,6 +97,51 @@ export const jsonLogicDisplayTestSpecialtyForm = {
97
97
  },
98
98
  ],
99
99
  };
100
+ export const UpdateAccessibilityFormOne = {
101
+ id: 'accessibility508Form1',
102
+ name: 'Accessibility Form One',
103
+ objectId: 'accessibility508Object',
104
+ actionId: '_update1',
105
+ entries: [
106
+ {
107
+ parameterId: 'name',
108
+ type: 'input',
109
+ display: {
110
+ label: 'Name',
111
+ },
112
+ },
113
+ {
114
+ parameterId: 'license',
115
+ type: 'input',
116
+ display: {
117
+ label: 'License',
118
+ relatedObjectDisplay: 'dropdown',
119
+ },
120
+ },
121
+ ],
122
+ };
123
+ export const UpdateAccessibilityFormTwo = {
124
+ id: 'accessibility508Form2',
125
+ name: 'Accessibility Form Two',
126
+ objectId: 'accessibility508Object',
127
+ actionId: '_update2',
128
+ entries: [
129
+ {
130
+ parameterId: 'name',
131
+ type: 'input',
132
+ display: {
133
+ label: 'Name',
134
+ },
135
+ },
136
+ {
137
+ type: 'input',
138
+ parameterId: 'user',
139
+ display: {
140
+ label: 'User',
141
+ },
142
+ },
143
+ ],
144
+ };
100
145
  // Objects
101
146
  export const licenseObject = {
102
147
  id: 'license',
@@ -321,6 +366,80 @@ export const specialtyTypeObject = {
321
366
  },
322
367
  ],
323
368
  };
369
+ export const accessibility508Object = {
370
+ id: 'accessibility508',
371
+ name: 'Accessibility 508 Test Object',
372
+ properties: [
373
+ {
374
+ id: 'name',
375
+ name: 'Name',
376
+ type: 'string',
377
+ },
378
+ {
379
+ id: 'license',
380
+ name: 'License',
381
+ type: 'object',
382
+ objectId: 'license',
383
+ },
384
+ {
385
+ id: 'user',
386
+ name: 'User',
387
+ type: 'user',
388
+ },
389
+ ],
390
+ actions: [
391
+ {
392
+ id: '_update1',
393
+ name: 'Update Related Object',
394
+ type: 'update',
395
+ outputEvent: '508 Test Object Updated',
396
+ parameters: [
397
+ {
398
+ id: 'name',
399
+ name: 'Name',
400
+ type: 'string',
401
+ },
402
+ {
403
+ id: 'license',
404
+ name: 'License',
405
+ type: 'object',
406
+ objectId: 'license',
407
+ },
408
+ ],
409
+ },
410
+ {
411
+ id: '_update2',
412
+ name: 'Update User',
413
+ type: 'update',
414
+ outputEvent: '508 Test Object Updated',
415
+ parameters: [
416
+ {
417
+ id: 'name',
418
+ name: 'Name',
419
+ type: 'string',
420
+ },
421
+ {
422
+ id: 'user',
423
+ type: 'user',
424
+ name: 'User',
425
+ required: false,
426
+ },
427
+ ],
428
+ },
429
+ {
430
+ id: '_delete',
431
+ name: 'Delete',
432
+ type: 'delete',
433
+ outputEvent: 'Accessibility Type Deleted',
434
+ },
435
+ {
436
+ id: '_create',
437
+ name: 'Create',
438
+ type: 'create',
439
+ outputEvent: 'Accessibility Type Created',
440
+ },
441
+ ],
442
+ };
324
443
  // Instances
325
444
  export const rnLicense = {
326
445
  id: 'rnLicense',
@@ -392,3 +511,18 @@ export const npSpecialtyType2 = {
392
511
  name: 'NP License Type',
393
512
  },
394
513
  };
514
+ // Users
515
+ export const users = [
516
+ {
517
+ id: 'user1',
518
+ status: 'Active',
519
+ email: 'user1@systemautomation.com',
520
+ name: 'User 1',
521
+ },
522
+ {
523
+ id: 'user2',
524
+ status: 'Active',
525
+ email: 'user2@systemautomation.com',
526
+ name: 'User 2',
527
+ },
528
+ ];