@jasperoosthoek/react-toolbox 0.8.1 → 0.9.1

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 (63) hide show
  1. package/change-log.md +330 -312
  2. package/dist/components/buttons/ConfirmButton.d.ts +2 -2
  3. package/dist/components/buttons/DeleteConfirmButton.d.ts +2 -2
  4. package/dist/components/buttons/IconButtons.d.ts +40 -41
  5. package/dist/components/errors/Errors.d.ts +1 -2
  6. package/dist/components/forms/FormField.d.ts +22 -0
  7. package/dist/components/forms/FormFields.d.ts +1 -56
  8. package/dist/components/forms/FormModal.d.ts +7 -34
  9. package/dist/components/forms/FormModalProvider.d.ts +25 -15
  10. package/dist/components/forms/FormProvider.d.ts +66 -0
  11. package/dist/components/forms/fields/FormBadgesSelection.d.ts +26 -0
  12. package/dist/components/forms/fields/FormCheckbox.d.ts +7 -0
  13. package/dist/components/forms/fields/FormDropdown.d.ts +19 -0
  14. package/dist/components/forms/fields/FormInput.d.ts +17 -0
  15. package/dist/components/forms/fields/FormSelect.d.ts +12 -0
  16. package/dist/components/forms/fields/index.d.ts +5 -0
  17. package/dist/components/indicators/CheckIndicator.d.ts +1 -2
  18. package/dist/components/indicators/LoadingIndicator.d.ts +4 -4
  19. package/dist/components/login/LoginPage.d.ts +1 -1
  20. package/dist/components/tables/DataTable.d.ts +2 -2
  21. package/dist/components/tables/DragAndDropList.d.ts +2 -2
  22. package/dist/components/tables/SearchBox.d.ts +2 -2
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.LICENSE.txt +0 -4
  26. package/dist/localization/LocalizationContext.d.ts +1 -1
  27. package/dist/utils/hooks.d.ts +1 -1
  28. package/dist/utils/timeAndDate.d.ts +5 -2
  29. package/dist/utils/utils.d.ts +3 -3
  30. package/package.json +10 -11
  31. package/src/__tests__/buttons.test.tsx +545 -0
  32. package/src/__tests__/errors.test.tsx +339 -0
  33. package/src/__tests__/forms.test.tsx +3021 -0
  34. package/src/__tests__/hooks.test.tsx +413 -0
  35. package/src/__tests__/indicators.test.tsx +284 -0
  36. package/src/__tests__/localization.test.tsx +462 -0
  37. package/src/__tests__/login.test.tsx +417 -0
  38. package/src/__tests__/setupTests.ts +328 -0
  39. package/src/__tests__/tables.test.tsx +609 -0
  40. package/src/__tests__/timeAndDate.test.tsx +308 -0
  41. package/src/__tests__/utils.test.tsx +422 -0
  42. package/src/components/forms/FormField.tsx +92 -0
  43. package/src/components/forms/FormFields.tsx +3 -423
  44. package/src/components/forms/FormModal.tsx +168 -243
  45. package/src/components/forms/FormModalProvider.tsx +164 -85
  46. package/src/components/forms/FormProvider.tsx +218 -0
  47. package/src/components/forms/fields/FormBadgesSelection.tsx +108 -0
  48. package/src/components/forms/fields/FormCheckbox.tsx +76 -0
  49. package/src/components/forms/fields/FormDropdown.tsx +123 -0
  50. package/src/components/forms/fields/FormInput.tsx +114 -0
  51. package/src/components/forms/fields/FormSelect.tsx +47 -0
  52. package/src/components/forms/fields/index.ts +6 -0
  53. package/src/index.ts +32 -29
  54. package/src/localization/LocalizationContext.tsx +156 -131
  55. package/src/localization/localization.ts +131 -131
  56. package/src/utils/hooks.ts +108 -94
  57. package/src/utils/timeAndDate.ts +33 -4
  58. package/src/utils/utils.ts +74 -66
  59. package/dist/components/forms/CreateEditModal.d.ts +0 -41
  60. package/dist/components/forms/CreateEditModalProvider.d.ts +0 -41
  61. package/dist/components/forms/FormFields.test.d.ts +0 -4
  62. package/dist/login/Login.d.ts +0 -70
  63. package/src/components/forms/FormFields.test.tsx +0 -107
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+
5
+ export interface FormCheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'type'> {
6
+ name: string;
7
+ label?: React.ReactElement | string;
8
+ }
9
+
10
+ export const FormCheckbox = (props: FormCheckboxProps) => {
11
+ const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(props);
12
+
13
+ const errorId = isInvalid && error ? `${props.name}-error` : undefined;
14
+ const checkboxId = `${props.name}-checkbox`; // Use different ID to avoid conflict
15
+
16
+ return (
17
+ <Form.Group controlId={props.name}>
18
+ {isInvalid && error && (
19
+ <Form.Text id={errorId} className="text-danger">
20
+ {error}
21
+ </Form.Text>
22
+ )}
23
+ <Form.Check
24
+ id={checkboxId}
25
+ type="checkbox"
26
+ {...mergedProps}
27
+ checked={!!value}
28
+ isInvalid={isInvalid}
29
+ aria-describedby={errorId}
30
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
31
+ onChange(e.target.checked);
32
+ }}
33
+ label={label ? (
34
+ <span>
35
+ {label}
36
+ {required && ' *'}
37
+ </span>
38
+ ) : undefined}
39
+ />
40
+ </Form.Group>
41
+ );
42
+ };
43
+
44
+ export const FormSwitch = (props: FormCheckboxProps) => {
45
+ const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(props);
46
+
47
+ const errorId = isInvalid && error ? `${props.name}-error` : undefined;
48
+ const switchId = `${props.name}-switch`; // Use different ID to avoid conflict
49
+
50
+ return (
51
+ <Form.Group controlId={props.name}>
52
+ {isInvalid && error && (
53
+ <Form.Text id={errorId} className="text-danger">
54
+ {error}
55
+ </Form.Text>
56
+ )}
57
+ <Form.Check
58
+ id={switchId}
59
+ type="switch"
60
+ {...mergedProps}
61
+ checked={!!value}
62
+ isInvalid={isInvalid}
63
+ aria-describedby={errorId}
64
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
65
+ onChange(e.target.checked);
66
+ }}
67
+ label={label ? (
68
+ <span>
69
+ {label}
70
+ {required && ' *'}
71
+ </span>
72
+ ) : undefined}
73
+ />
74
+ </Form.Group>
75
+ );
76
+ };
@@ -0,0 +1,123 @@
1
+ import React, { useMemo, KeyboardEvent } from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+ import { useLocalization } from '../../../localization/LocalizationContext';
5
+
6
+ type DisabledProps = {
7
+ list: any[];
8
+ value: string | number;
9
+ state: any;
10
+ initialState: any;
11
+ initialValue: any;
12
+ }
13
+
14
+ export interface FormDropdownProps<T> extends Omit<
15
+ React.InputHTMLAttributes<HTMLInputElement>,
16
+ 'name' | 'value' | 'onChange' | 'disabled' | 'list'
17
+ > {
18
+ name: string;
19
+ label?: React.ReactElement | string;
20
+ list?: T[];
21
+ options?: T[]; // Alias for list
22
+ idKey?: keyof T;
23
+ nameKey?: keyof T;
24
+ disabled?: boolean | ((props: DisabledProps) => boolean);
25
+ }
26
+
27
+ export const FormDropdown = <T,>(props: FormDropdownProps<T>) => {
28
+ const {
29
+ list: listProp,
30
+ options,
31
+ idKey = 'value' as keyof T, // Change default to match test
32
+ nameKey = 'label' as keyof T, // Change default to match test
33
+ disabled,
34
+ ...componentProps
35
+ } = props;
36
+
37
+ const { value, onChange, isInvalid, error, label, required, mergedProps, submit } = useFormField(componentProps);
38
+ const { strings } = useLocalization();
39
+
40
+ // Use options or list, with options taking precedence
41
+ const listBase = options || listProp;
42
+
43
+ const [list, mismatch] = useMemo(() => {
44
+ if (!listBase || !Array.isArray(listBase)) {
45
+ return [null, null] as [T[] | null, any];
46
+ }
47
+
48
+ // Handle string arrays by converting them to { value, label } objects
49
+ if (listBase.length > 0 && typeof listBase[0] === 'string') {
50
+ const convertedList = listBase.map((item: any) => ({
51
+ value: item,
52
+ label: item
53
+ })) as unknown as T[];
54
+ return [convertedList, null];
55
+ }
56
+
57
+ // Only validate object arrays
58
+ if (listBase.length > 0 && typeof listBase[0] === 'object') {
59
+ const mismatch = listBase?.find((item: any) => (
60
+ !['number', 'string'].includes(typeof item[idKey])
61
+ || !['number', 'string'].includes(typeof item[nameKey])
62
+ ))
63
+ if (mismatch) return [null, mismatch];
64
+ }
65
+
66
+ return [listBase, null];
67
+ }, [listBase, idKey, nameKey]);
68
+
69
+ if (!list) {
70
+ console.error(
71
+ `FormDropdown Error:
72
+ - Each item in 'list' must include a valid 'idKey' (${String(idKey)}) and 'nameKey' (${String(nameKey)}).
73
+ - Both keys must exist on every item and be of type 'string' or 'number'.
74
+ - One or more items failed this check.
75
+
76
+ Received list:`,
77
+ listBase,
78
+ ...mismatch ? ['First mismatch:', mismatch] : []
79
+ );
80
+ return null;
81
+ }
82
+
83
+ const selectedItem = list.find(item => item[idKey] === value);
84
+
85
+ return (
86
+ <Form.Group controlId={props.name}>
87
+ {label && <Form.Label htmlFor={props.name}>{label}{required && ' *'}</Form.Label>}
88
+ {isInvalid && error && (
89
+ <Form.Text className="text-danger">
90
+ {error}
91
+ </Form.Text>
92
+ )}
93
+
94
+ <Form.Select
95
+ id={props.name}
96
+ value={value || ''}
97
+ isInvalid={isInvalid}
98
+ onChange={(e) => onChange(e.target.value)}
99
+ onKeyDown={(e: KeyboardEvent<HTMLSelectElement>) => {
100
+ if (e.key === 'Enter') {
101
+ e.preventDefault();
102
+ submit();
103
+ }
104
+ }}
105
+ {...mergedProps}
106
+ >
107
+ {list.map((item, key) =>
108
+ <option
109
+ key={key}
110
+ value={item[idKey] as string | number}
111
+ disabled={
112
+ typeof disabled === 'function'
113
+ ? disabled({ list, value: item[idKey] as number | string, state: {}, initialState: {}, initialValue: '' })
114
+ : disabled
115
+ }
116
+ >
117
+ {item[nameKey] as string}
118
+ </option>
119
+ )}
120
+ </Form.Select>
121
+ </Form.Group>
122
+ );
123
+ };
@@ -0,0 +1,114 @@
1
+ import React, { ChangeEvent, KeyboardEvent } from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+
5
+ export interface FormInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'onChange'> {
6
+ name: string;
7
+ label?: React.ReactElement | string;
8
+ as?: string; // For textarea, select, etc.
9
+ rows?: number; // For textarea
10
+ onChange: (value: string) => void;
11
+ }
12
+
13
+ export const FormInput = (props: FormInputProps) => {
14
+ const { value, onChange, isInvalid, error, label, required, mergedProps, submit } = useFormField(props);
15
+
16
+ const errorId = isInvalid && error ? `${props.name}-error` : undefined;
17
+
18
+ return (
19
+ <Form.Group controlId={props.name}>
20
+ {label && <Form.Label htmlFor={props.name}>{label}{required && ' *'}</Form.Label>}
21
+ {isInvalid && error && (
22
+ <Form.Text id={errorId} className="text-danger">
23
+ {error}
24
+ </Form.Text>
25
+ )}
26
+ <Form.Control
27
+ id={props.name}
28
+ autoComplete="off"
29
+ {...mergedProps}
30
+ value={value || ''}
31
+ isInvalid={isInvalid}
32
+ aria-describedby={errorId}
33
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
34
+ onChange(e.target.value);
35
+ }}
36
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
37
+ if (e.key === 'Enter' && mergedProps.as !== 'textarea') {
38
+ // Pressing the enter key will save data unless it is a multi line text area
39
+ e.preventDefault();
40
+ submit();
41
+ }
42
+ }}
43
+ />
44
+ </Form.Group>
45
+ );
46
+ };
47
+
48
+ export const FormTextarea = ({ rows = 3, ...props }: FormInputProps) => (
49
+ <FormInput as="textarea" rows={rows} {...props} />
50
+ );
51
+
52
+ export const FormDate = (props: FormInputProps) => (
53
+ <FormInput type="date" {...props} />
54
+ );
55
+
56
+ export interface FormDateTimeProps extends Omit<FormInputProps, 'value' | 'onChange'> {
57
+ value?: string | Date;
58
+ onChange?: (value: string) => void;
59
+ timezone?: string;
60
+ }
61
+
62
+ export const FormDateTime = ({ value, onChange, timezone, ...props }: FormDateTimeProps) => {
63
+ // Convert value to datetime-local format (YYYY-MM-DDTHH:mm)
64
+ const formatForInput = (val: string | Date | undefined) => {
65
+ if (!val) return '';
66
+
67
+ try {
68
+ const date = typeof val === 'string' ? new Date(val) : val;
69
+ if (isNaN(date.getTime())) return '';
70
+
71
+ // Format as YYYY-MM-DDTHH:mm for datetime-local input
72
+ const year = date.getFullYear();
73
+ const month = String(date.getMonth() + 1).padStart(2, '0');
74
+ const day = String(date.getDate()).padStart(2, '0');
75
+ const hours = String(date.getHours()).padStart(2, '0');
76
+ const minutes = String(date.getMinutes()).padStart(2, '0');
77
+
78
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
79
+ } catch {
80
+ return '';
81
+ }
82
+ };
83
+
84
+ const handleChange = (inputValue: string) => {
85
+ if (!onChange) return;
86
+
87
+ if (!inputValue) {
88
+ onChange('');
89
+ return;
90
+ }
91
+
92
+ try {
93
+ // Convert datetime-local value to ISO string
94
+ const date = new Date(inputValue);
95
+ if (isNaN(date.getTime())) {
96
+ onChange('');
97
+ return;
98
+ }
99
+
100
+ onChange(date.toISOString());
101
+ } catch {
102
+ onChange('');
103
+ }
104
+ };
105
+
106
+ return (
107
+ <FormInput
108
+ type="datetime-local"
109
+ value={formatForInput(value)}
110
+ onChange={handleChange}
111
+ {...props}
112
+ />
113
+ );
114
+ };
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+
5
+ export interface FormSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'name' | 'value' | 'onChange'> {
6
+ name: string;
7
+ label?: React.ReactElement | string;
8
+ options: Array<{ value: string | number; label: string; disabled?: boolean }>;
9
+ placeholder?: string;
10
+ }
11
+
12
+ export const FormSelect = (props: FormSelectProps) => {
13
+ const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(props);
14
+ const { options, placeholder = "Choose..." } = props;
15
+
16
+ return (
17
+ <Form.Group controlId={props.name}>
18
+ {label && <Form.Label>{label}{required && ' *'}</Form.Label>}
19
+ {isInvalid && error && (
20
+ <Form.Text className="text-danger">
21
+ {error}
22
+ </Form.Text>
23
+ )}
24
+ <Form.Select
25
+ {...mergedProps}
26
+ value={value || ''}
27
+ isInvalid={isInvalid}
28
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
29
+ onChange(e.target.value);
30
+ }}
31
+ >
32
+ <option value="" disabled>
33
+ {placeholder}
34
+ </option>
35
+ {options.map((option, index) => (
36
+ <option
37
+ key={index}
38
+ value={option.value}
39
+ disabled={option.disabled}
40
+ >
41
+ {option.label}
42
+ </option>
43
+ ))}
44
+ </Form.Select>
45
+ </Form.Group>
46
+ );
47
+ };
@@ -0,0 +1,6 @@
1
+ // Export all form field components
2
+ export * from './FormInput';
3
+ export * from './FormSelect';
4
+ export * from './FormCheckbox';
5
+ export * from './FormDropdown';
6
+ export * from './FormBadgesSelection';
package/src/index.ts CHANGED
@@ -1,29 +1,32 @@
1
- export * from './components/buttons/IconButtons';
2
- export * from './components/buttons/ConfirmButton';
3
- export { default as ConfirmButton } from './components/buttons/ConfirmButton';
4
- export { default as DeleteConfirmButton } from './components/buttons/DeleteConfirmButton';
5
- export * from './components/buttons/DeleteConfirmButton';
6
-
7
- export * from './components/forms/FormModal';
8
- export * from './components/forms/FormModalProvider';
9
- export * from './components/forms/FormFields';
10
-
11
- export * from './components/indicators/LoadingIndicator';
12
- export * from './components/indicators/CheckIndicator';
13
-
14
- export * from './components/tables/DataTable';
15
- export * from './components/tables/DragAndDropList';
16
- export * from './components/tables/SearchBox';
17
-
18
- export * from './components/errors/Errors';
19
- export * from './components/errors/ErrorBoundary';
20
-
21
- export * from './components/login/LoginPage';
22
-
23
- export * from './utils/hooks';
24
- export * from './utils/timeAndDate';
25
- export * from './utils/utils';
26
-
27
-
28
- export * from './localization/localization';
29
- export * from './localization/LocalizationContext';
1
+ export * from './components/buttons/IconButtons';
2
+ export * from './components/buttons/ConfirmButton';
3
+ export { default as ConfirmButton } from './components/buttons/ConfirmButton';
4
+ export { default as DeleteConfirmButton } from './components/buttons/DeleteConfirmButton';
5
+ export * from './components/buttons/DeleteConfirmButton';
6
+
7
+ export * from './components/forms/FormProvider';
8
+ export * from './components/forms/FormModal';
9
+ export * from './components/forms/FormModalProvider';
10
+ export * from './components/forms/FormFields';
11
+ export * from './components/forms/FormField';
12
+ export * from './components/forms/fields';
13
+
14
+ export * from './components/indicators/LoadingIndicator';
15
+ export * from './components/indicators/CheckIndicator';
16
+
17
+ export * from './components/tables/DataTable';
18
+ export * from './components/tables/DragAndDropList';
19
+ export * from './components/tables/SearchBox';
20
+
21
+ export * from './components/errors/Errors';
22
+ export * from './components/errors/ErrorBoundary';
23
+
24
+ export * from './components/login/LoginPage';
25
+
26
+ export * from './utils/hooks';
27
+ export * from './utils/timeAndDate';
28
+ export * from './utils/utils';
29
+
30
+
31
+ export * from './localization/localization';
32
+ export * from './localization/LocalizationContext';