@pagamio/frontend-commons-lib 0.8.324 → 0.8.326
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/lib/components/ui/MultiSelect.js +27 -5
- package/lib/components/ui/UploadField.d.ts +2 -0
- package/lib/components/ui/UploadField.js +3 -3
- package/lib/form-engine/components/inputs/upload-field/UploadFieldForm.js +1 -1
- package/lib/form-engine/types/index.d.ts +2 -0
- package/lib/styles.css +0 -4
- package/package.json +1 -1
|
@@ -3,6 +3,12 @@ import { ChevronDownIcon, Cross2Icon } from '@radix-ui/react-icons';
|
|
|
3
3
|
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { cn } from '../../helpers';
|
|
5
5
|
import Button from './Button';
|
|
6
|
+
const DROPDOWN_MARGIN = 4;
|
|
7
|
+
const VIEWPORT_PADDING = 8;
|
|
8
|
+
// Target height when there's plenty of room. The actual height is capped to
|
|
9
|
+
// the available space, so in tight viewports the dropdown shrinks rather
|
|
10
|
+
// than overshooting.
|
|
11
|
+
const PREFERRED_DROPDOWN_HEIGHT = 224;
|
|
6
12
|
const MultiSelect = forwardRef(({ options, value, defaultValue = [], disabled, onChange, placeholder, className, field, tagPosition = 'inside' }, ref) => {
|
|
7
13
|
const [selectedValues, setSelectedValues] = useState(defaultValue);
|
|
8
14
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -15,15 +21,29 @@ const MultiSelect = forwardRef(({ options, value, defaultValue = [], disabled, o
|
|
|
15
21
|
setSelectedValues(value);
|
|
16
22
|
}
|
|
17
23
|
}, [value]);
|
|
18
|
-
// Measure trigger position
|
|
24
|
+
// Measure trigger position and decide whether to render the dropdown
|
|
25
|
+
// below or above the trigger based on available viewport space. Cap the
|
|
26
|
+
// height to whatever room is left so the list never extends past the
|
|
27
|
+
// visible area — that's what produced the "I can't see more shifts" bug
|
|
28
|
+
// when the trigger sat near the bottom of a drawer.
|
|
19
29
|
const updateDropdownPosition = useCallback(() => {
|
|
20
30
|
if (!triggerRef.current)
|
|
21
31
|
return;
|
|
22
32
|
const rect = triggerRef.current.getBoundingClientRect();
|
|
33
|
+
const viewportH = window.innerHeight;
|
|
34
|
+
const spaceBelow = Math.max(0, viewportH - rect.bottom - DROPDOWN_MARGIN - VIEWPORT_PADDING);
|
|
35
|
+
const spaceAbove = Math.max(0, rect.top - DROPDOWN_MARGIN - VIEWPORT_PADDING);
|
|
36
|
+
const placeBelow = spaceBelow >= PREFERRED_DROPDOWN_HEIGHT || spaceBelow >= spaceAbove;
|
|
37
|
+
const available = placeBelow ? spaceBelow : spaceAbove;
|
|
38
|
+
// Never exceed the actual room — overshooting causes the list to
|
|
39
|
+
// disappear under the trigger or off the bottom of the viewport.
|
|
40
|
+
const maxHeight = Math.min(PREFERRED_DROPDOWN_HEIGHT, available);
|
|
23
41
|
setDropdownPos({
|
|
24
|
-
top: rect.bottom +
|
|
25
|
-
left: rect.left
|
|
42
|
+
top: placeBelow ? rect.bottom + DROPDOWN_MARGIN : rect.top - DROPDOWN_MARGIN,
|
|
43
|
+
left: rect.left,
|
|
26
44
|
width: rect.width,
|
|
45
|
+
maxHeight,
|
|
46
|
+
placement: placeBelow ? 'bottom' : 'top',
|
|
27
47
|
});
|
|
28
48
|
}, []);
|
|
29
49
|
// Reposition on scroll / resize while open
|
|
@@ -90,10 +110,12 @@ const MultiSelect = forwardRef(({ options, value, defaultValue = [], disabled, o
|
|
|
90
110
|
? displayPlaceholder
|
|
91
111
|
: `${selectedOptions.length} selected` })) }), _jsx(ChevronDownIcon, { className: cn('ml-2 flex-shrink-0 text-muted-foreground transition-transform duration-150', isOpen && 'rotate-180') })] }), tagPosition === 'bottom' && selectedOptions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1 mt-2", children: tags })), isOpen && dropdownPos && (_jsx("div", { ref: ref, id: "ms-dropdown-portal", style: {
|
|
92
112
|
position: 'fixed',
|
|
93
|
-
top: dropdownPos.top,
|
|
113
|
+
top: dropdownPos.placement === 'bottom' ? dropdownPos.top : undefined,
|
|
114
|
+
bottom: dropdownPos.placement === 'top' ? window.innerHeight - dropdownPos.top : undefined,
|
|
94
115
|
left: dropdownPos.left,
|
|
95
116
|
width: dropdownPos.width,
|
|
96
|
-
|
|
117
|
+
maxHeight: dropdownPos.maxHeight,
|
|
118
|
+
}, className: cn('z-[9999] flex flex-col', 'bg-popover border border-border rounded-md shadow-lg', 'animate-in fade-in-0 zoom-in-95 duration-100'), children: _jsx("ul", { className: "overflow-y-auto p-1.5 space-y-0.5", style: { maxHeight: dropdownPos.maxHeight }, children: options.length > 0 ? (options.map((option) => {
|
|
97
119
|
const checked = internalValues.includes(option.value);
|
|
98
120
|
const id = `ms-opt-${field?.name ?? 'ms'}-${option.value}`;
|
|
99
121
|
return (_jsx("li", { children: _jsxs("label", { htmlFor: id, className: cn('flex items-center gap-2.5 px-2 py-2 rounded-md cursor-pointer text-sm', 'hover:bg-accent hover:text-accent-foreground transition-colors', checked && 'bg-primary/5'), children: [_jsx("input", { id: id, type: "checkbox", checked: checked, onChange: () => handleToggle(option.value), className: cn('w-4 h-4 rounded border border-input bg-background flex-shrink-0', 'accent-[hsl(var(--primary))] cursor-pointer', 'focus:ring-2 focus:ring-ring focus:ring-offset-0') }), _jsx("span", { className: "flex-1 font-medium text-foreground", children: option.label })] }) }, option.value));
|
|
@@ -9,6 +9,8 @@ type UploadProps = {
|
|
|
9
9
|
allowedFileTypes?: string[];
|
|
10
10
|
maxFileSize?: number;
|
|
11
11
|
helperText?: string;
|
|
12
|
+
/** Optional label for the inline trigger (defaults to "Choose file"). */
|
|
13
|
+
chooseFileLabel?: string;
|
|
12
14
|
value?: never;
|
|
13
15
|
};
|
|
14
16
|
declare const UploadField: React.ForwardRefExoticComponent<UploadProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useState } from 'react';
|
|
3
3
|
import { Button, Icon } from '../../components';
|
|
4
|
-
const UploadField = forwardRef(({ onChange, id, type = 'file', hideUploadButton = false, className = '', disabled = false, allowedFileTypes, maxFileSize, helperText, value, // Explicitly exclude value prop becuase it's not allowed for security reasons.
|
|
4
|
+
const UploadField = forwardRef(({ onChange, id, type = 'file', hideUploadButton = false, className = '', disabled = false, allowedFileTypes, maxFileSize, helperText, chooseFileLabel = 'Choose file', value, // Explicitly exclude value prop becuase it's not allowed for security reasons.
|
|
5
5
|
...props }, ref) => {
|
|
6
6
|
const [fileName, setFileName] = useState(null);
|
|
7
7
|
const [key, setKey] = useState(0);
|
|
@@ -42,11 +42,11 @@ const UploadField = forwardRef(({ onChange, id, type = 'file', hideUploadButton
|
|
|
42
42
|
setKey((prev) => prev + 1);
|
|
43
43
|
};
|
|
44
44
|
const acceptValue = allowedFileTypes?.map((ext) => `.${ext}`).join(',');
|
|
45
|
-
return (_jsxs("div", { className: `flex flex-col gap-3 w-full ${className}`, children: [_jsxs("div", { className: "flex gap-3 w-full items-center", children: [_jsxs("label", { htmlFor: id, className: "flex items-center gap-2 cursor-pointer flex-1", children: [_jsx("div", { className: "bg-primary rounded-full h-10 w-10 flex items-center justify-center", children: _jsx(Icon, { name: "FiUpload", size: 20, className: "text-white" }) }), _jsx("span", { className: "text-sm font-medium text-foreground", children:
|
|
45
|
+
return (_jsxs("div", { className: `flex flex-col gap-3 w-full ${className}`, children: [_jsxs("div", { className: "flex gap-3 w-full items-center", children: [_jsxs("label", { htmlFor: id, className: "flex items-center gap-2 cursor-pointer flex-1", children: [_jsx("div", { className: "bg-primary rounded-full h-10 w-10 flex items-center justify-center", children: _jsx(Icon, { name: "FiUpload", size: 20, className: "text-white" }) }), _jsx("span", { className: "text-sm font-medium text-foreground", children: chooseFileLabel }), _jsx("input", { type: type, onChange: handleFileChange, className: "hidden", id: id, ref: ref, disabled: disabled, accept: acceptValue, ...props }, key)] }), !hideUploadButton && (_jsx(Button, { onClick: () => {
|
|
46
46
|
if (ref && typeof ref === 'object' && ref.current) {
|
|
47
47
|
ref.current.click();
|
|
48
48
|
}
|
|
49
|
-
}, disabled: disabled, children: "Upload" }))] }), fileName && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm text-muted-foreground", children: ["Selected: ", fileName] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: handleClearFile, disabled: disabled, children: "Clear" })] })), helperText &&
|
|
49
|
+
}, disabled: disabled, children: "Upload" }))] }), fileName && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm text-muted-foreground", children: ["Selected: ", fileName] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: handleClearFile, disabled: disabled, children: "Clear" })] })), helperText && _jsx("p", { className: "text-sm text-muted-foreground", children: helperText }), error && _jsx("div", { className: "text-red-500 text-sm", children: error })] }));
|
|
50
50
|
});
|
|
51
51
|
UploadField.displayName = 'UploadField';
|
|
52
52
|
export default UploadField;
|
|
@@ -53,7 +53,7 @@ const UploadFieldForm = forwardRef(({ field, error, ...props }, ref) => {
|
|
|
53
53
|
return _jsx("span", { className: "text-muted-foreground", children: "File uploaded." });
|
|
54
54
|
}, [preview, field.name]);
|
|
55
55
|
const previewLabel = preview.type === 'pdf' ? 'Uploaded PDF:' : 'Uploaded Image:';
|
|
56
|
-
return (_jsxs("div", { className: "flex flex-col space-y-4", children: [_jsxs("div", { className: "flex flex-col space-y-2", children: [_jsx("label", { htmlFor: field.name, className: "text-sm font-medium text-foreground", children: field.label }), _jsx(UploadField, { ...props, id: field.name, ref: ref, value: props.value || null, onChange: handleFileChange, className: "w-full p-2 border border-input rounded-md", hideUploadButton: field.hideUploadButton, allowedFileTypes: field.allowedFileTypes, maxFileSize: field.maxFileSize, helperText: field.fileUploadHelperText }), error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: error.message })] }), field.showFileUploadPreview && (_jsxs("div", { className: "mt-4", children: [_jsx("label", { htmlFor: `${field.name}-preview`, className: "text-sm font-medium text-foreground", children: previewLabel }), _jsx("div", { className: "mt-2 flex flex-col items-center p-4 border border-input rounded-md", children: previewElement })] }))] }));
|
|
56
|
+
return (_jsxs("div", { className: "flex flex-col space-y-4", children: [_jsxs("div", { className: "flex flex-col space-y-2", children: [_jsx("label", { htmlFor: field.name, className: "text-sm font-medium text-foreground", children: field.label }), _jsx(UploadField, { ...props, id: field.name, ref: ref, value: props.value || null, onChange: handleFileChange, className: "w-full p-2 border border-input rounded-md", hideUploadButton: field.hideUploadButton, allowedFileTypes: field.allowedFileTypes, maxFileSize: field.maxFileSize, helperText: field.fileUploadHelperText, chooseFileLabel: field.chooseFileLabel }), error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: error.message })] }), field.showFileUploadPreview && (_jsxs("div", { className: "mt-4", children: [_jsx("label", { htmlFor: `${field.name}-preview`, className: "text-sm font-medium text-foreground", children: previewLabel }), _jsx("div", { className: "mt-2 flex flex-col items-center p-4 border border-input rounded-md", children: previewElement })] }))] }));
|
|
57
57
|
});
|
|
58
58
|
UploadFieldForm.displayName = 'UploadFieldForm';
|
|
59
59
|
export default UploadFieldForm;
|
|
@@ -157,6 +157,8 @@ export interface Field {
|
|
|
157
157
|
showFileUploadPreview?: boolean;
|
|
158
158
|
/** To display file upload helper text */
|
|
159
159
|
fileUploadHelperText?: string;
|
|
160
|
+
/** Custom label for the inline upload trigger (defaults to "Choose file") */
|
|
161
|
+
chooseFileLabel?: string;
|
|
160
162
|
/** Default country for phone input */
|
|
161
163
|
defaultCountry?: string;
|
|
162
164
|
/** Disables the field */
|
package/lib/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagamio/frontend-commons-lib",
|
|
3
3
|
"description": "Pagamio library for Frontend reusable components like the form engine and table container",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.326",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|