@evoke-platform/ui-components 1.5.1-testing.1 → 1.6.0-dev.2
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/Form/Common/FormComponentWrapper.js +2 -13
- package/dist/published/components/custom/Form/FormComponents/FormFieldComponent.d.ts +1 -0
- package/dist/published/components/custom/Form/FormComponents/FormFieldComponent.js +1 -1
- package/dist/published/components/custom/Form/utils.js +1 -0
- package/dist/published/components/custom/FormField/FormField.d.ts +1 -0
- package/dist/published/components/custom/FormField/FormField.js +2 -1
- package/dist/published/components/custom/FormField/Select/Select.js +122 -22
- package/dist/published/components/custom/FormField/Select/Select.test.js +72 -3
- package/dist/published/index.d.ts +1 -1
- package/dist/published/index.js +1 -1
- package/dist/published/theme/hooks.d.ts +78 -0
- package/dist/published/theme/hooks.js +65 -0
- package/dist/published/theme/hooks.test.d.ts +1 -0
- package/dist/published/theme/hooks.test.js +187 -0
- package/dist/published/theme/index.d.ts +2 -2
- package/dist/published/theme/index.js +2 -2
- package/package.json +2 -1
@@ -1,5 +1,5 @@
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
2
|
-
import { ErrorRounded, Help
|
2
|
+
import { ErrorRounded, Help } from '../../../../icons';
|
3
3
|
import { IconButton, Tooltip, Typography } from '../../../core';
|
4
4
|
import { Box } from '../../../layout';
|
5
5
|
const underFieldStyles = {
|
@@ -13,11 +13,6 @@ const descriptionStyles = {
|
|
13
13
|
whiteSpace: 'normal',
|
14
14
|
paddingBottom: '4px',
|
15
15
|
};
|
16
|
-
const clearBtnStyles = {
|
17
|
-
color: '#637381',
|
18
|
-
fontSize: '1.2rem',
|
19
|
-
marginBottom: '2px',
|
20
|
-
};
|
21
16
|
const PrefixSuffix = (props) => {
|
22
17
|
const { prefix, suffix, height } = props;
|
23
18
|
const text = prefix || suffix;
|
@@ -51,7 +46,7 @@ const PrefixSuffix = (props) => {
|
|
51
46
|
* description, tooltip, prefix, suffix and word/char counts
|
52
47
|
*/
|
53
48
|
export const FormComponentWrapper = (props) => {
|
54
|
-
const { inputId, label, description, tooltip, prefix, suffix, value, validate, errorMessage, showCharCount, type, viewOnly, children, displayOption,
|
49
|
+
const { inputId, label, description, tooltip, prefix, suffix, value, validate, errorMessage, showCharCount, type, viewOnly, children, displayOption, property, } = props;
|
55
50
|
const [fieldHeight, setFieldHeight] = useState(40);
|
56
51
|
const { maxLength } = validate;
|
57
52
|
const fieldRef = useRef(null);
|
@@ -76,12 +71,6 @@ export const FormComponentWrapper = (props) => {
|
|
76
71
|
tooltip && (React.createElement(Tooltip, { placement: "right", title: tooltip },
|
77
72
|
React.createElement(IconButton, null,
|
78
73
|
React.createElement(Help, { sx: { fontSize: '14px' } }))))),
|
79
|
-
displayOption === 'radioButton' && onChange && !viewOnly && !readOnly && value && (React.createElement(Tooltip, { title: `Clear` },
|
80
|
-
React.createElement("span", null,
|
81
|
-
React.createElement(IconButton, { "aria-label": `Clear`, sx: { padding: '0px' }, onClick: () => {
|
82
|
-
property && onChange(property.id, '');
|
83
|
-
} },
|
84
|
-
React.createElement(HighlightOffOutlined, { sx: clearBtnStyles }))))),
|
85
74
|
React.createElement(Box, { sx: { ...(displayOption === 'radioButton' && { display: 'flex' }) } },
|
86
75
|
React.createElement(Typography, { variant: "caption", sx: descriptionStyles }, description)))),
|
87
76
|
React.createElement(Box, { sx: { display: 'flex', flexDirection: 'row' } },
|
@@ -12,6 +12,7 @@ type FormFieldComponentProps = Omit<BaseFormComponentProps, 'property'> & {
|
|
12
12
|
initialValue?: string;
|
13
13
|
displayOption?: 'radioButton' | 'dropdown';
|
14
14
|
strictlyTrue?: boolean;
|
15
|
+
nonStrictEnum?: boolean;
|
15
16
|
};
|
16
17
|
export declare class FormFieldComponent extends ReactComponent {
|
17
18
|
[x: string]: any;
|
@@ -485,6 +485,6 @@ export class FormFieldComponent extends ReactComponent {
|
|
485
485
|
falsePositiveMaskError &&
|
486
486
|
isEmpty(this.errorDetails) &&
|
487
487
|
this.emit('changed-' + this.component.key, e.target.value);
|
488
|
-
}, ...this.component, id: inputId, defaultValue: this.dataValue, mask: this.component.inputMask, error: this.hasErrors(), size: this.component.fieldHeight ?? 'medium', required: this.component.property.type === 'boolean' ? this.component.strictlyTrue : undefined }))), root);
|
488
|
+
}, ...this.component, id: inputId, defaultValue: this.dataValue, mask: this.component.inputMask, error: this.hasErrors(), size: this.component.fieldHeight ?? 'medium', required: this.component.property.type === 'boolean' ? this.component.strictlyTrue : undefined, isCombobox: this.component.nonStrictEnum }))), root);
|
489
489
|
}
|
490
490
|
}
|
@@ -165,6 +165,7 @@ export function convertFormToComponents(entries, parameters, object) {
|
|
165
165
|
data: {
|
166
166
|
values: entry.type === 'input' ? entry.enumWithLabels : undefined,
|
167
167
|
},
|
168
|
+
nonStrictEnum: parameter.nonStrictEnum,
|
168
169
|
property: property,
|
169
170
|
defaultToCurrentTime: displayOptions?.defaultValue === 'currentTime' && parameter.type === 'time' ? true : false,
|
170
171
|
defaultToCurrentDate: displayOptions?.defaultValue === 'currentDate' && parameter.type === 'date' ? true : false,
|
@@ -8,7 +8,7 @@ import InputFieldComponent from './InputFieldComponent/InputFieldComponent';
|
|
8
8
|
import Select from './Select/Select';
|
9
9
|
import TimePickerSelect from './TimePickerSelect/TimePickerSelect';
|
10
10
|
const FormField = (props) => {
|
11
|
-
const { id, defaultValue, error, onChange, property, readOnly, selectOptions, required, strictlyTrue, size, placeholder, errorMessage, onBlur, mask, max, min, isMultiLineText, rows, inputMaskPlaceholderChar, queryAddresses, isOptionEqualToValue, renderOption, disableCloseOnSelect, getOptionLabel, additionalProps, displayOption, sortBy, label, description, tooltip, } = props;
|
11
|
+
const { id, defaultValue, error, onChange, property, readOnly, selectOptions, required, strictlyTrue, size, placeholder, errorMessage, onBlur, mask, max, min, isMultiLineText, rows, inputMaskPlaceholderChar, queryAddresses, isOptionEqualToValue, renderOption, disableCloseOnSelect, getOptionLabel, additionalProps, displayOption, sortBy, label, description, tooltip, isCombobox, } = props;
|
12
12
|
let control;
|
13
13
|
const commonProps = {
|
14
14
|
id: id ?? property.id,
|
@@ -36,6 +36,7 @@ const FormField = (props) => {
|
|
36
36
|
label,
|
37
37
|
description,
|
38
38
|
tooltip,
|
39
|
+
isCombobox,
|
39
40
|
};
|
40
41
|
if (queryAddresses) {
|
41
42
|
control = (React.createElement(AddressFieldComponent, { ...commonProps, mask: mask, inputMaskPlaceholderChar: inputMaskPlaceholderChar, isMultiLineText: isMultiLineText, rows: rows, queryAddresses: queryAddresses }));
|
@@ -1,23 +1,56 @@
|
|
1
|
-
import
|
2
|
-
import
|
1
|
+
import { createFilterOptions, List, ListSubheader } from '@mui/material';
|
2
|
+
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
3
|
+
import { Clear } from '../../../../icons';
|
4
|
+
import { Autocomplete, FormControl, FormControlLabel, IconButton, Radio, RadioGroup, TextField, Typography, } from '../../../core';
|
5
|
+
import { Box } from '../../../layout';
|
3
6
|
import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
|
7
|
+
const filter = createFilterOptions();
|
4
8
|
const Select = (props) => {
|
5
|
-
const { id, property, defaultValue, error, errorMessage, onBlur, readOnly, selectOptions, required, size, isOptionEqualToValue, renderOption, getOptionLabel, disableCloseOnSelect, additionalProps, displayOption, sortBy, } = props;
|
9
|
+
const { id, property, defaultValue, error, errorMessage, onBlur, onChange, readOnly, isCombobox, selectOptions, required, size, isOptionEqualToValue, renderOption, getOptionLabel, disableCloseOnSelect, additionalProps, displayOption, sortBy, } = props;
|
10
|
+
const otherInputRef = useRef(null);
|
11
|
+
const [isOther, setIsOther] = useState(!!isCombobox &&
|
12
|
+
!!defaultValue &&
|
13
|
+
!selectOptions?.some((option) => (typeof option === 'string' && option === defaultValue) ||
|
14
|
+
option.value === defaultValue));
|
15
|
+
const [isOtherFocused, setIsOtherFocused] = useState(false);
|
6
16
|
const [value, setValue] = useState(defaultValue);
|
7
|
-
const [inputValue, setInputValue] = useState('')
|
17
|
+
const [inputValue, setInputValue] = useState(!selectOptions?.some((option) => (typeof option === 'string' && option === defaultValue) ||
|
18
|
+
option.value === defaultValue)
|
19
|
+
? defaultValue
|
20
|
+
: '');
|
21
|
+
const [errorState, setErrorState] = useState();
|
8
22
|
useEffect(() => {
|
9
|
-
|
10
|
-
}, [
|
23
|
+
setErrorState(isCombobox ? error && !isOtherFocused : error);
|
24
|
+
}, [error, isOtherFocused]);
|
25
|
+
useEffect(() => {
|
26
|
+
if (isOther && otherInputRef.current && value === '') {
|
27
|
+
otherInputRef.current.focus();
|
28
|
+
}
|
29
|
+
}, [isOther, value]);
|
11
30
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
12
31
|
const handleChange = (event, selected) => {
|
13
32
|
if (Array.isArray(selected)) {
|
14
|
-
|
15
|
-
|
16
|
-
|
33
|
+
const newValues = selected.map((option) => option.value ?? option);
|
34
|
+
setValue(newValues);
|
35
|
+
onChange && onChange(property.id, newValues, property);
|
17
36
|
}
|
18
37
|
else {
|
19
|
-
|
20
|
-
|
38
|
+
if (isCombobox && displayOption === 'radioButton') {
|
39
|
+
if (selected === '__other__') {
|
40
|
+
setIsOther(true);
|
41
|
+
setValue('');
|
42
|
+
}
|
43
|
+
else {
|
44
|
+
setIsOther(false);
|
45
|
+
setValue(selected);
|
46
|
+
setInputValue('');
|
47
|
+
onChange && onChange(property.id, selected, property);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
else {
|
51
|
+
setValue(selected);
|
52
|
+
onChange && onChange(property.id, selected, property);
|
53
|
+
}
|
21
54
|
}
|
22
55
|
};
|
23
56
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
@@ -29,6 +62,10 @@ const Select = (props) => {
|
|
29
62
|
setInputValue(selectValue);
|
30
63
|
}
|
31
64
|
};
|
65
|
+
const ListboxComponent = forwardRef(function ListboxComponent(props, ref) {
|
66
|
+
const { children, ...other } = props;
|
67
|
+
return (React.createElement(List, { component: "ul", ...other, ref: ref, subheader: isCombobox ? (React.createElement(ListSubheader, { component: 'div', id: `${id}-instructions`, role: "note", sx: { lineHeight: '36px', borderBottom: '1px solid #919EAB3D' } }, "Select below or type and click Enter to add a new item.")) : undefined }, children));
|
68
|
+
});
|
32
69
|
const sortedOptions = (() => {
|
33
70
|
if (!selectOptions)
|
34
71
|
return [];
|
@@ -42,21 +79,84 @@ const Select = (props) => {
|
|
42
79
|
return options.sort((a, b) => typeof a === 'string' ? a.localeCompare(b) : a.label.localeCompare(b.label));
|
43
80
|
}
|
44
81
|
})();
|
45
|
-
return readOnly ? (React.createElement(InputFieldComponent, { ...props })) : displayOption === 'radioButton' ? (React.createElement(
|
46
|
-
React.createElement(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
82
|
+
return readOnly ? (React.createElement(InputFieldComponent, { ...props })) : displayOption === 'radioButton' ? (React.createElement(React.Fragment, null,
|
83
|
+
React.createElement(FormControl, { error: error, required: required },
|
84
|
+
React.createElement(RadioGroup, { name: `radioGroup-${id}`, value: isOther ? '__other__' : value, onChange: handleChange },
|
85
|
+
sortedOptions.map((option, index) => (React.createElement(FormControlLabel, { key: index, sx: { margin: '3px 0' }, value: typeof option === 'string' ? option : option.value, control: React.createElement(Radio, { size: "small", sx: {
|
86
|
+
padding: '3px',
|
87
|
+
...(index === 0 && {
|
88
|
+
paddingTop: 0,
|
89
|
+
}),
|
90
|
+
...(!!errorState && {
|
91
|
+
color: '#FF4842',
|
92
|
+
'&.Mui-checked': { color: '#FF4842' },
|
93
|
+
}),
|
94
|
+
} }), label: typeof option === 'string' ? option : option.label }))),
|
95
|
+
isCombobox && (React.createElement(React.Fragment, null,
|
96
|
+
React.createElement(FormControlLabel, { value: '__other__', sx: { margin: '3px 0' }, control: React.createElement(Radio, { size: "small", sx: {
|
97
|
+
padding: '3px',
|
98
|
+
...(!!errorState && {
|
99
|
+
color: '#FF4842',
|
100
|
+
'&.Mui-checked': { color: '#FF4842' },
|
101
|
+
}),
|
102
|
+
} }), label: React.createElement("span", { id: `other-radio-button-${id}` }, "Other") }),
|
103
|
+
isOther && (React.createElement(TextField, { id: `other-${id}`, inputProps: {
|
104
|
+
'aria-labelledby': `other-radio-button-${id}`,
|
105
|
+
}, value: inputValue, inputRef: otherInputRef, error: !!errorState, sx: { marginLeft: '4px' }, onChange: (e) => {
|
106
|
+
setValue(e.target.value);
|
107
|
+
setInputValue(e.target.value);
|
108
|
+
onChange && onChange(property.id, e.target.value, property);
|
109
|
+
}, size: 'small', onFocus: () => setIsOtherFocused(true), onBlur: (e) => {
|
110
|
+
// If another radio button is focused, change the value to that radio button's value.
|
111
|
+
const targetElement = e.nativeEvent.relatedTarget;
|
112
|
+
if (targetElement?.defaultValue && targetElement.name === `radioGroup-${id}`) {
|
113
|
+
onChange && onChange(property.id, targetElement, property);
|
114
|
+
}
|
115
|
+
else {
|
116
|
+
onChange && onChange(property.id, e.target.value, property);
|
117
|
+
}
|
118
|
+
setIsOtherFocused(false);
|
119
|
+
} })))))),
|
120
|
+
displayOption === 'radioButton' && onChange && !readOnly && value && (React.createElement(Box, { sx: {
|
121
|
+
':hover': { cursor: 'pointer' },
|
122
|
+
marginTop: '4px',
|
123
|
+
display: 'flex',
|
124
|
+
alignItems: 'center',
|
125
|
+
} },
|
126
|
+
React.createElement(IconButton, { "aria-label": `Clear`, onClick: () => {
|
127
|
+
setValue('');
|
128
|
+
property && onChange(property.id, '');
|
129
|
+
setIsOther(false);
|
130
|
+
}, sx: { padding: '3px', marginRight: '6px' } },
|
131
|
+
React.createElement(Clear, { sx: {
|
132
|
+
color: '#637381',
|
133
|
+
fontSize: '1.2rem',
|
134
|
+
} })),
|
135
|
+
React.createElement(Typography, { variant: "caption" }, "Clear Selection"))))) : (React.createElement(Autocomplete, { multiple: property?.type === 'array' ? true : false, id: id, sortBy: sortBy, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, fullWidth: true, onBlur: onBlur, inputProps: {
|
136
|
+
...params.inputProps,
|
137
|
+
'aria-describedby': isCombobox ? `${id}-instructions` : undefined,
|
138
|
+
} })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
|
139
|
+
const filtered = filter(options, params);
|
140
|
+
const { inputValue } = params;
|
141
|
+
// Suggest to the user to add a new value.
|
142
|
+
const found = options.some((option) => inputValue === option.label);
|
143
|
+
if (inputValue !== '' && !found && isCombobox) {
|
144
|
+
filtered.push({
|
145
|
+
value: inputValue,
|
146
|
+
label: `Add "${inputValue}"`,
|
147
|
+
});
|
148
|
+
}
|
149
|
+
return filtered;
|
150
|
+
}, isOptionEqualToValue: isOptionEqualToValue
|
53
151
|
? (option, value) => isOptionEqualToValue(option, value)
|
54
|
-
: undefined, getOptionLabel: getOptionLabel
|
152
|
+
: undefined, getOptionLabel: getOptionLabel && !isCombobox
|
153
|
+
? (option) => getOptionLabel(option)
|
154
|
+
: (option) => (typeof option === 'string' ? option : option.label), renderOption: renderOption
|
55
155
|
? (props, option, state) => renderOption(props, option, state)
|
56
|
-
: undefined, disableCloseOnSelect: disableCloseOnSelect, sx: {
|
156
|
+
: undefined, ListboxComponent: ListboxComponent, disableCloseOnSelect: disableCloseOnSelect, sx: {
|
57
157
|
'& button.MuiButtonBase-root': {
|
58
158
|
visibility: 'visible',
|
59
159
|
},
|
60
|
-
}, ...(additionalProps ?? {}) }));
|
160
|
+
}, ...(isCombobox ? { selectOnFocus: true, handleHomeEndKeys: true, freeSolo: true } : {}), ...(additionalProps ?? {}) }));
|
61
161
|
};
|
62
162
|
export default Select;
|
@@ -14,8 +14,8 @@ describe('Single select', () => {
|
|
14
14
|
type: 'choices',
|
15
15
|
};
|
16
16
|
it('returns selected option', async () => {
|
17
|
-
const user = userEvent.setup();
|
18
17
|
const onChangeMock = vi.fn((name, value, property) => { });
|
18
|
+
const user = userEvent.setup();
|
19
19
|
const options = ['option 1', 'option 2', 'option 3'];
|
20
20
|
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock }));
|
21
21
|
const input = screen.getByRole('combobox');
|
@@ -48,6 +48,21 @@ describe('Single select', () => {
|
|
48
48
|
const optionLabels = allOptions.map((option) => option.textContent);
|
49
49
|
expect(optionLabels).toEqual(expectedValues);
|
50
50
|
});
|
51
|
+
it('allows the user to enter a custom value if it is combobox component', async () => {
|
52
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
53
|
+
const user = userEvent.setup();
|
54
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
55
|
+
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
|
56
|
+
const input = screen.getByRole('combobox');
|
57
|
+
await user.click(input);
|
58
|
+
// Verify the instruction text for a combobox displays as the sub-header of the combobox.
|
59
|
+
screen.getByText('Select below or type and click Enter to add a new item.');
|
60
|
+
await user.type(input, 'custom option');
|
61
|
+
// Verify the option to add the custom value is displayed as an available option in the dropdown.
|
62
|
+
const customOption = await screen.findByRole('option', { name: 'Add "custom option"' });
|
63
|
+
await user.click(customOption);
|
64
|
+
expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'Add "custom option"', value: 'custom option' }), choiceProperty);
|
65
|
+
});
|
51
66
|
});
|
52
67
|
describe('Multi select', () => {
|
53
68
|
// Right now an object property is required for this to function, but eventually this should go
|
@@ -66,12 +81,49 @@ describe('Multi select', () => {
|
|
66
81
|
await user.click(input);
|
67
82
|
const option2 = await screen.findByRole('option', { name: 'option 2' });
|
68
83
|
await user.click(option2);
|
69
|
-
|
84
|
+
// selecting option closes the dropdown, re-open the options
|
85
|
+
await user.click(input);
|
70
86
|
const option3 = await screen.findByRole('option', { name: 'option 3' });
|
71
87
|
await user.click(option3);
|
72
88
|
expect(onChangeMock).toBeCalledTimes(2);
|
73
89
|
expect(onChangeMock).lastCalledWith('multiSelect', ['option 2', 'option 3'], multiChoiceProperty);
|
74
90
|
});
|
91
|
+
it('allows the user to enter custom values if it is combobox component', async () => {
|
92
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
93
|
+
const user = userEvent.setup();
|
94
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
95
|
+
render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
|
96
|
+
const input = screen.getByRole('combobox');
|
97
|
+
await user.click(input);
|
98
|
+
// Verify the instruction text for a combobox displays as the sub-header of the combobox.
|
99
|
+
screen.getByText('Select below or type and click Enter to add a new item.');
|
100
|
+
await user.type(input, 'custom option 1');
|
101
|
+
const customOption1 = await screen.findByRole('option', { name: 'Add "custom option 1"' });
|
102
|
+
await user.click(customOption1);
|
103
|
+
await user.type(input, 'custom option 2');
|
104
|
+
const customOption2 = await screen.findByRole('option', { name: 'Add "custom option 2"' });
|
105
|
+
await user.click(customOption2);
|
106
|
+
expect(onChangeMock).toBeCalledTimes(2);
|
107
|
+
expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'custom option 2'], multiChoiceProperty);
|
108
|
+
});
|
109
|
+
it('allows the user to enter custom values in conjunction with the predefined options if it is combobox component', async () => {
|
110
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
111
|
+
const user = userEvent.setup();
|
112
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
113
|
+
render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
|
114
|
+
const input = screen.getByRole('combobox');
|
115
|
+
await user.click(input);
|
116
|
+
// Verify the instruction text for a combobox displays as the sub-header of the combobox.
|
117
|
+
screen.getByText('Select below or type and click Enter to add a new item.');
|
118
|
+
await user.type(input, 'custom option 1');
|
119
|
+
const customOption1 = await screen.findByRole('option', { name: 'Add "custom option 1"' });
|
120
|
+
await user.click(customOption1);
|
121
|
+
await user.click(input);
|
122
|
+
const option1 = await screen.findByRole('option', { name: 'option 1' });
|
123
|
+
await user.click(option1);
|
124
|
+
expect(onChangeMock).toBeCalledTimes(2);
|
125
|
+
expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'option 1'], multiChoiceProperty);
|
126
|
+
});
|
75
127
|
});
|
76
128
|
describe('Radio Single select', () => {
|
77
129
|
const choiceProperty = {
|
@@ -79,7 +131,7 @@ describe('Radio Single select', () => {
|
|
79
131
|
name: 'Select Options',
|
80
132
|
type: 'choices',
|
81
133
|
};
|
82
|
-
|
134
|
+
it('returns selected radio option', async () => {
|
83
135
|
const user = userEvent.setup();
|
84
136
|
const onChangeMock = vi.fn((name, value, property) => { });
|
85
137
|
const options = ['option 1', 'option 2', 'option 3'];
|
@@ -99,4 +151,21 @@ describe('Radio Single select', () => {
|
|
99
151
|
const radioValues = radioButtons.map((radioButton) => radioButton.value);
|
100
152
|
expect(radioValues).toEqual(expectedValues);
|
101
153
|
});
|
154
|
+
it('renders an "Other" option in the radio group if the component is configured to support a custom value', async () => {
|
155
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
156
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
157
|
+
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
|
158
|
+
await screen.findByRole('radio', { name: 'Other' });
|
159
|
+
});
|
160
|
+
it('renders a text field for a custom option if the "Other" option is selected', async () => {
|
161
|
+
const user = userEvent.setup();
|
162
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
163
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
164
|
+
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
|
165
|
+
await screen.findByRole('radio', { name: 'Other' });
|
166
|
+
await user.click(screen.getByRole('radio', { name: 'Other' }));
|
167
|
+
const otherTextField = await screen.findByRole('textbox', { name: `Other` });
|
168
|
+
await user.type(otherTextField, 'custom option');
|
169
|
+
expect(onChangeMock).toBeCalledWith('selectOptions', expect.stringContaining('custom option'), choiceProperty);
|
170
|
+
});
|
102
171
|
});
|
@@ -1,4 +1,4 @@
|
|
1
|
-
export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar } from '@mui/material';
|
1
|
+
export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar, useMediaQuery, useTheme, } from '@mui/material';
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
3
3
|
export * from './colors';
|
4
4
|
export * from './components/core';
|
package/dist/published/index.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar } from '@mui/material';
|
1
|
+
export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar, useMediaQuery, useTheme, } from '@mui/material';
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
3
3
|
export * from './colors';
|
4
4
|
export * from './components/core';
|
@@ -29,3 +29,81 @@ export declare function useResponsive(): {
|
|
29
29
|
/** Returns true if viewport width is between the specified breakpoints */
|
30
30
|
between: (start: Breakpoint, end: Breakpoint) => boolean;
|
31
31
|
};
|
32
|
+
export interface WidgetSizeInfo {
|
33
|
+
/** The ref to attach to the widget container */
|
34
|
+
ref: (element: HTMLElement | SVGElement | null) => void;
|
35
|
+
/** Width in pixels */
|
36
|
+
width: number;
|
37
|
+
/** Height in pixels */
|
38
|
+
height: number;
|
39
|
+
/** Widget size breakpoints */
|
40
|
+
breakpoints: {
|
41
|
+
/** Extra small: width < 800px */
|
42
|
+
isXs: boolean;
|
43
|
+
/** Small: 800px <= width < 1000px */
|
44
|
+
isSm: boolean;
|
45
|
+
/** Medium: 1000px <= width < 1200px */
|
46
|
+
isMd: boolean;
|
47
|
+
/** Large: 1200px <= width < 1400px */
|
48
|
+
isLg: boolean;
|
49
|
+
/** Extra large: width >= 1400px */
|
50
|
+
isXl: boolean;
|
51
|
+
/** Compact height: height < 300px */
|
52
|
+
isCompact: boolean;
|
53
|
+
/** Tall height: height >= 600px */
|
54
|
+
isTall: boolean;
|
55
|
+
};
|
56
|
+
/** Bounds information from react-use-measure */
|
57
|
+
bounds: {
|
58
|
+
/**
|
59
|
+
* left is usually the same as x, but can differ if width is negative.
|
60
|
+
* See: https://developer.mozilla.org/en-US/docs/Web/API/DOMRect
|
61
|
+
*/
|
62
|
+
left: number;
|
63
|
+
/**
|
64
|
+
* top is usually the same as y, but can differ if height is negative.
|
65
|
+
* See: https://developer.mozilla.org/en-US/docs/Web/API/DOMRect
|
66
|
+
*/
|
67
|
+
top: number;
|
68
|
+
width: number;
|
69
|
+
height: number;
|
70
|
+
bottom: number;
|
71
|
+
right: number;
|
72
|
+
x: number;
|
73
|
+
y: number;
|
74
|
+
};
|
75
|
+
/** Check if widget is above a breakpoint */
|
76
|
+
isAbove: (breakpoint: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => boolean;
|
77
|
+
/** Check if widget is below a breakpoint */
|
78
|
+
isBelow: (breakpoint: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => boolean;
|
79
|
+
}
|
80
|
+
/**
|
81
|
+
* Custom hook that measures widget dimensions and provides responsive breakpoints
|
82
|
+
* based on the actual widget size rather than viewport size.
|
83
|
+
*
|
84
|
+
* @param options Configuration options for the measurement
|
85
|
+
* @returns WidgetSizeInfo object with ref, dimensions, and breakpoints
|
86
|
+
*
|
87
|
+
* @example
|
88
|
+
* ```tsx
|
89
|
+
* const { ref, width, breakpoints } = useWidgetSize();
|
90
|
+
*
|
91
|
+
* return (
|
92
|
+
* <div ref={ref}>
|
93
|
+
* {breakpoints.isXs ? 'Compact view' : 'Full view'}
|
94
|
+
* <p>Widget width: {width}px</p>
|
95
|
+
* </div>
|
96
|
+
* );
|
97
|
+
* ```
|
98
|
+
*/
|
99
|
+
export declare const useWidgetSize: (options?: {
|
100
|
+
/** Debounce measurement updates (ms) */
|
101
|
+
debounce?: number;
|
102
|
+
/** Throttle measurement updates (ms) */
|
103
|
+
throttle?: number;
|
104
|
+
/** Scroll containers to observe */
|
105
|
+
scroll?: boolean;
|
106
|
+
/** Resize containers to observe */
|
107
|
+
resize?: boolean;
|
108
|
+
}) => WidgetSizeInfo;
|
109
|
+
export default useWidgetSize;
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { useTheme } from '@mui/material';
|
2
2
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
3
|
+
import useMeasure from 'react-use-measure';
|
3
4
|
/**
|
4
5
|
* Custom hook for responsive design breakpoints using size terminology.
|
5
6
|
* Breakpoints based on MUI default theme:
|
@@ -33,3 +34,67 @@ export function useResponsive() {
|
|
33
34
|
between: (start, end) => useMediaQuery(theme.breakpoints.between(start, end)),
|
34
35
|
};
|
35
36
|
}
|
37
|
+
/**
|
38
|
+
* Custom hook that measures widget dimensions and provides responsive breakpoints
|
39
|
+
* based on the actual widget size rather than viewport size.
|
40
|
+
*
|
41
|
+
* @param options Configuration options for the measurement
|
42
|
+
* @returns WidgetSizeInfo object with ref, dimensions, and breakpoints
|
43
|
+
*
|
44
|
+
* @example
|
45
|
+
* ```tsx
|
46
|
+
* const { ref, width, breakpoints } = useWidgetSize();
|
47
|
+
*
|
48
|
+
* return (
|
49
|
+
* <div ref={ref}>
|
50
|
+
* {breakpoints.isXs ? 'Compact view' : 'Full view'}
|
51
|
+
* <p>Widget width: {width}px</p>
|
52
|
+
* </div>
|
53
|
+
* );
|
54
|
+
* ```
|
55
|
+
*/
|
56
|
+
export const useWidgetSize = (options) => {
|
57
|
+
const [ref, bounds] = useMeasure({
|
58
|
+
debounce: options?.debounce ?? 16,
|
59
|
+
scroll: options?.scroll ?? true,
|
60
|
+
offsetSize: true,
|
61
|
+
...options,
|
62
|
+
});
|
63
|
+
const width = bounds.width || 0;
|
64
|
+
const height = bounds.height || 0;
|
65
|
+
// Widget-specific breakpoints based on container width
|
66
|
+
const breakpoints = {
|
67
|
+
isXs: width < 800,
|
68
|
+
isSm: width >= 800 && width < 1000,
|
69
|
+
isMd: width >= 1000 && width < 1200,
|
70
|
+
isLg: width >= 1200 && width < 1400,
|
71
|
+
isXl: width >= 1400,
|
72
|
+
isCompact: height < 300,
|
73
|
+
isTall: height >= 600,
|
74
|
+
};
|
75
|
+
// Breakpoint thresholds
|
76
|
+
const breakpointThresholds = {
|
77
|
+
xs: 800,
|
78
|
+
sm: 1000,
|
79
|
+
md: 1200,
|
80
|
+
lg: 1400,
|
81
|
+
xl: Infinity,
|
82
|
+
};
|
83
|
+
// Utility functions for responsive checks
|
84
|
+
const isAbove = (breakpoint) => {
|
85
|
+
return width >= breakpointThresholds[breakpoint];
|
86
|
+
};
|
87
|
+
const isBelow = (breakpoint) => {
|
88
|
+
return width < breakpointThresholds[breakpoint];
|
89
|
+
};
|
90
|
+
return {
|
91
|
+
ref,
|
92
|
+
width,
|
93
|
+
height,
|
94
|
+
breakpoints,
|
95
|
+
bounds,
|
96
|
+
isAbove,
|
97
|
+
isBelow,
|
98
|
+
};
|
99
|
+
};
|
100
|
+
export default useWidgetSize;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
import { useWidgetSize } from '.';
|
4
|
+
// Shared mock bounds for useMeasure
|
5
|
+
let mockBounds = {
|
6
|
+
left: 0,
|
7
|
+
top: 0,
|
8
|
+
width: 0,
|
9
|
+
height: 0,
|
10
|
+
bottom: 0,
|
11
|
+
right: 0,
|
12
|
+
x: 0,
|
13
|
+
y: 0,
|
14
|
+
};
|
15
|
+
function setMockBounds(newBounds) {
|
16
|
+
mockBounds = { ...mockBounds, ...newBounds };
|
17
|
+
}
|
18
|
+
// Mock useMeasure from react-use-measure
|
19
|
+
vi.mock('react-use-measure', () => {
|
20
|
+
const useMeasure = () => [
|
21
|
+
vi.fn(),
|
22
|
+
mockBounds,
|
23
|
+
];
|
24
|
+
return { __esModule: true, default: useMeasure, useMeasure };
|
25
|
+
});
|
26
|
+
describe('useWidgetSize', () => {
|
27
|
+
beforeEach(() => {
|
28
|
+
// Reset bounds before each test
|
29
|
+
setMockBounds({
|
30
|
+
left: 0,
|
31
|
+
top: 0,
|
32
|
+
width: 0,
|
33
|
+
height: 0,
|
34
|
+
bottom: 0,
|
35
|
+
right: 0,
|
36
|
+
x: 0,
|
37
|
+
y: 0,
|
38
|
+
});
|
39
|
+
});
|
40
|
+
it('should provide ref and basic structure', () => {
|
41
|
+
setMockBounds({ width: 123, height: 456 });
|
42
|
+
const { result } = renderHook(() => useWidgetSize());
|
43
|
+
expect(result.current.ref).toBeDefined();
|
44
|
+
expect(typeof result.current.ref).toBe('function');
|
45
|
+
expect(result.current.width).toBe(123);
|
46
|
+
expect(result.current.height).toBe(456);
|
47
|
+
});
|
48
|
+
describe('breakpoints', () => {
|
49
|
+
it('should set isXs for width < 800', () => {
|
50
|
+
setMockBounds({ width: 799, height: 100 });
|
51
|
+
const { result } = renderHook(() => useWidgetSize());
|
52
|
+
expect(result.current.breakpoints.isXs).toBe(true);
|
53
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
54
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
55
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
56
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
57
|
+
});
|
58
|
+
it('should set isSm for 800 <= width < 1000', () => {
|
59
|
+
setMockBounds({ width: 800, height: 100 });
|
60
|
+
let { result } = renderHook(() => useWidgetSize());
|
61
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
62
|
+
expect(result.current.breakpoints.isSm).toBe(true);
|
63
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
64
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
65
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
66
|
+
setMockBounds({ width: 999, height: 100 });
|
67
|
+
result = renderHook(() => useWidgetSize()).result;
|
68
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
69
|
+
expect(result.current.breakpoints.isSm).toBe(true);
|
70
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
71
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
72
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
73
|
+
});
|
74
|
+
it('should set isMd for 1000 <= width < 1200', () => {
|
75
|
+
setMockBounds({ width: 1000, height: 100 });
|
76
|
+
let { result } = renderHook(() => useWidgetSize());
|
77
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
78
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
79
|
+
expect(result.current.breakpoints.isMd).toBe(true);
|
80
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
81
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
82
|
+
setMockBounds({ width: 1199, height: 100 });
|
83
|
+
result = renderHook(() => useWidgetSize()).result;
|
84
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
85
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
86
|
+
expect(result.current.breakpoints.isMd).toBe(true);
|
87
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
88
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
89
|
+
});
|
90
|
+
it('should set isLg for 1200 <= width < 1400', () => {
|
91
|
+
setMockBounds({ width: 1200, height: 100 });
|
92
|
+
let { result } = renderHook(() => useWidgetSize());
|
93
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
94
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
95
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
96
|
+
expect(result.current.breakpoints.isLg).toBe(true);
|
97
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
98
|
+
setMockBounds({ width: 1399, height: 100 });
|
99
|
+
result = renderHook(() => useWidgetSize()).result;
|
100
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
101
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
102
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
103
|
+
expect(result.current.breakpoints.isLg).toBe(true);
|
104
|
+
expect(result.current.breakpoints.isXl).toBe(false);
|
105
|
+
});
|
106
|
+
it('should set isXl for width >= 1400', () => {
|
107
|
+
setMockBounds({ width: 1400, height: 100 });
|
108
|
+
let { result } = renderHook(() => useWidgetSize());
|
109
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
110
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
111
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
112
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
113
|
+
expect(result.current.breakpoints.isXl).toBe(true);
|
114
|
+
setMockBounds({ width: 2000, height: 100 });
|
115
|
+
result = renderHook(() => useWidgetSize()).result;
|
116
|
+
expect(result.current.breakpoints.isXs).toBe(false);
|
117
|
+
expect(result.current.breakpoints.isSm).toBe(false);
|
118
|
+
expect(result.current.breakpoints.isMd).toBe(false);
|
119
|
+
expect(result.current.breakpoints.isLg).toBe(false);
|
120
|
+
expect(result.current.breakpoints.isXl).toBe(true);
|
121
|
+
});
|
122
|
+
it('should set isCompact for height < 300', () => {
|
123
|
+
setMockBounds({ width: 1000, height: 299 });
|
124
|
+
const { result } = renderHook(() => useWidgetSize());
|
125
|
+
expect(result.current.breakpoints.isCompact).toBe(true);
|
126
|
+
expect(result.current.breakpoints.isTall).toBe(false);
|
127
|
+
});
|
128
|
+
it('should set isTall for height >= 600', () => {
|
129
|
+
setMockBounds({ width: 1000, height: 600 });
|
130
|
+
const { result } = renderHook(() => useWidgetSize());
|
131
|
+
expect(result.current.breakpoints.isTall).toBe(true);
|
132
|
+
expect(result.current.breakpoints.isCompact).toBe(false);
|
133
|
+
setMockBounds({ width: 1000, height: 800 });
|
134
|
+
const { result: result2 } = renderHook(() => useWidgetSize());
|
135
|
+
expect(result2.current.breakpoints.isTall).toBe(true);
|
136
|
+
expect(result2.current.breakpoints.isCompact).toBe(false);
|
137
|
+
});
|
138
|
+
it('should not set isCompact or isTall for 300 <= height < 600', () => {
|
139
|
+
setMockBounds({ width: 1000, height: 300 });
|
140
|
+
let { result } = renderHook(() => useWidgetSize());
|
141
|
+
expect(result.current.breakpoints.isCompact).toBe(false);
|
142
|
+
expect(result.current.breakpoints.isTall).toBe(false);
|
143
|
+
setMockBounds({ width: 1000, height: 599 });
|
144
|
+
result = renderHook(() => useWidgetSize()).result;
|
145
|
+
expect(result.current.breakpoints.isCompact).toBe(false);
|
146
|
+
expect(result.current.breakpoints.isTall).toBe(false);
|
147
|
+
});
|
148
|
+
});
|
149
|
+
it('should provide bounds information', () => {
|
150
|
+
setMockBounds({
|
151
|
+
left: 1,
|
152
|
+
top: 2,
|
153
|
+
width: 300,
|
154
|
+
height: 400,
|
155
|
+
bottom: 402,
|
156
|
+
right: 301,
|
157
|
+
x: 1,
|
158
|
+
y: 2,
|
159
|
+
});
|
160
|
+
const { result } = renderHook(() => useWidgetSize());
|
161
|
+
expect(result.current.bounds).toBeDefined();
|
162
|
+
expect(result.current.bounds.left).toBe(1);
|
163
|
+
expect(result.current.bounds.top).toBe(2);
|
164
|
+
expect(result.current.bounds.width).toBe(300);
|
165
|
+
expect(result.current.bounds.height).toBe(400);
|
166
|
+
expect(result.current.bounds.bottom).toBe(402);
|
167
|
+
expect(result.current.bounds.right).toBe(301);
|
168
|
+
expect(result.current.bounds.x).toBe(1);
|
169
|
+
expect(result.current.bounds.y).toBe(2);
|
170
|
+
});
|
171
|
+
it('should provide responsive utility functions', () => {
|
172
|
+
setMockBounds({ width: 1200 });
|
173
|
+
const { result } = renderHook(() => useWidgetSize());
|
174
|
+
expect(result.current.isAbove).toBeDefined();
|
175
|
+
expect(typeof result.current.isAbove).toBe('function');
|
176
|
+
expect(result.current.isBelow).toBeDefined();
|
177
|
+
expect(typeof result.current.isBelow).toBe('function');
|
178
|
+
});
|
179
|
+
it('should calculate responsive utility functions correctly', () => {
|
180
|
+
setMockBounds({ width: 1100 });
|
181
|
+
const { result } = renderHook(() => useWidgetSize());
|
182
|
+
expect(result.current.isAbove('sm')).toBe(true); // 1100 >= 1000
|
183
|
+
expect(result.current.isBelow('lg')).toBe(true); // 1100 < 1400
|
184
|
+
expect(result.current.isAbove('xl')).toBe(false); // 1100 < Infinity
|
185
|
+
expect(result.current.isBelow('xs')).toBe(false); // 1100 >= 800
|
186
|
+
});
|
187
|
+
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { useMediaQuery, useTheme } from '@mui/material';
|
2
2
|
import UIThemeProvider from './UIThemeProvider';
|
3
|
-
import { useResponsive } from './hooks';
|
4
|
-
export { useMediaQuery, useResponsive, useTheme };
|
3
|
+
import { useResponsive, useWidgetSize, type WidgetSizeInfo } from './hooks';
|
4
|
+
export { useMediaQuery, useResponsive, useTheme, useWidgetSize, type WidgetSizeInfo };
|
5
5
|
export default UIThemeProvider;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { useMediaQuery, useTheme } from '@mui/material';
|
2
2
|
import UIThemeProvider from './UIThemeProvider';
|
3
|
-
import { useResponsive } from './hooks';
|
4
|
-
export { useMediaQuery, useResponsive, useTheme };
|
3
|
+
import { useResponsive, useWidgetSize } from './hooks';
|
4
|
+
export { useMediaQuery, useResponsive, useTheme, useWidgetSize };
|
5
5
|
export default UIThemeProvider;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@evoke-platform/ui-components",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.6.0-dev.2",
|
4
4
|
"description": "",
|
5
5
|
"main": "dist/published/index.js",
|
6
6
|
"module": "dist/published/index.js",
|
@@ -128,6 +128,7 @@
|
|
128
128
|
"react-input-mask": "^2.0.4",
|
129
129
|
"react-number-format": "^4.9.3",
|
130
130
|
"react-querybuilder": "^6.0.2",
|
131
|
+
"react-use-measure": "^2.1.7",
|
131
132
|
"rtf.js": "^3.0.9",
|
132
133
|
"sift": "^17.1.3",
|
133
134
|
"small-date": "^2.0.0",
|