@evoke-platform/ui-components 1.8.2-testing.0 → 1.9.0-testing.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.
- package/dist/published/components/custom/FormV2/FormRenderer.js +19 -16
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +16 -4
- package/dist/published/components/custom/FormV2/components/AccordionSections.js +30 -29
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +1 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +16 -7
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +88 -39
- package/dist/published/components/custom/FormV2/components/FormSections.js +34 -3
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +10 -29
- package/dist/published/components/custom/FormV2/components/ValidationFiles/Validation.js +2 -2
- package/dist/published/components/custom/FormV2/components/types.d.ts +9 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +18 -2
- package/dist/published/components/custom/FormV2/components/utils.js +163 -1
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +211 -2
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +9 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +134 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +1 -0
- package/dist/published/stories/FormRendererContainer.stories.js +5 -0
- package/dist/published/stories/FormRendererData.d.ts +7 -0
- package/dist/published/stories/FormRendererData.js +172 -1
- package/dist/published/stories/sharedMswHandlers.js +5 -1
- package/package.json +2 -1
|
@@ -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:
|
|
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
|
+
];
|