@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.
@@ -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 so the fixed dropdown aligns correctly
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 + window.scrollY + 4,
25
- left: rect.left + window.scrollX,
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
- }, className: cn('z-[9999]', 'bg-popover border border-border rounded-md shadow-lg', 'animate-in fade-in-0 zoom-in-95 duration-100'), children: _jsx("ul", { className: "max-h-56 overflow-y-auto p-1.5 space-y-0.5", children: options.length > 0 ? (options.map((option) => {
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: "Choose file" }), _jsx("input", { type: type, onChange: handleFileChange, className: "hidden", id: id, ref: ref, disabled: disabled, accept: acceptValue, ...props }, key)] }), !hideUploadButton && (_jsx(Button, { onClick: () => {
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 && (_jsx("div", { className: "w-full", children: _jsx("p", { className: "text-sm text-muted-foreground w-full md:w-[65%]", children: helperText }) })), error && _jsx("div", { className: "text-red-500 text-sm", children: error })] }));
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
@@ -6714,10 +6714,6 @@ video {
6714
6714
  width: 420px;
6715
6715
  }
6716
6716
 
6717
- .md\:w-\[65\%\] {
6718
- width: 65%;
6719
- }
6720
-
6721
6717
  .md\:w-auto {
6722
6718
  width: auto;
6723
6719
  }
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.324",
4
+ "version": "0.8.326",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false