@rovula/ui 0.0.64 → 0.0.66

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.
@@ -24,6 +24,7 @@ export type DropdownProps = {
24
24
  disabled?: boolean;
25
25
  error?: boolean;
26
26
  required?: boolean;
27
+ modal?: boolean;
27
28
  className?: string;
28
29
  optionContainerClassName?: string;
29
30
  optionItemClassName?: string;
@@ -53,6 +54,7 @@ declare const Dropdown: React.ForwardRefExoticComponent<{
53
54
  disabled?: boolean | undefined;
54
55
  error?: boolean | undefined;
55
56
  required?: boolean | undefined;
57
+ modal?: boolean | undefined;
56
58
  className?: string | undefined;
57
59
  optionContainerClassName?: string | undefined;
58
60
  optionItemClassName?: string | undefined;
@@ -15,6 +15,7 @@ declare const meta: {
15
15
  disabled?: boolean | undefined;
16
16
  error?: boolean | undefined;
17
17
  required?: boolean | undefined;
18
+ modal?: boolean | undefined;
18
19
  className?: string | undefined;
19
20
  optionContainerClassName?: string | undefined;
20
21
  optionItemClassName?: string | undefined;
@@ -48,6 +49,7 @@ declare const meta: {
48
49
  disabled?: boolean | undefined;
49
50
  error?: boolean | undefined;
50
51
  required?: boolean | undefined;
52
+ modal?: boolean | undefined;
51
53
  className?: string | undefined;
52
54
  optionContainerClassName?: string | undefined;
53
55
  optionItemClassName?: string | undefined;
@@ -317,6 +317,7 @@ declare const meta: {
317
317
  renderEndIcon?: (() => React.ReactNode) | undefined;
318
318
  onClickStartIcon?: (() => void) | undefined;
319
319
  options: Options[];
320
+ modal?: boolean | undefined;
320
321
  onChangeText?: React.ChangeEventHandler<HTMLInputElement> | undefined;
321
322
  renderOptions?: ((value: {
322
323
  optionsFiltered: Options[];
@@ -15,7 +15,7 @@ declare const meta: {
15
15
  id?: string | undefined;
16
16
  lang?: string | undefined;
17
17
  style?: React.CSSProperties | undefined;
18
- type?: "foreground" | "background" | undefined;
18
+ type?: "background" | "foreground" | undefined;
19
19
  role?: React.AriaRole | undefined;
20
20
  tabIndex?: number | undefined;
21
21
  "aria-activedescendant"?: string | undefined;
@@ -17,7 +17,7 @@ import { customInputVariant, dropdownIconVariant, iconWrapperVariant, } from "./
17
17
  import { ChevronDownIcon } from "@heroicons/react/16/solid";
18
18
  import { cn } from "@/utils/cn";
19
19
  const Dropdown = forwardRef((_a, ref) => {
20
- var { id, options = [], value, label, size = "md", rounded = "normal", variant = "outline", helperText, errorMessage, fullwidth = true, disabled = false, error = false, filterMode = false, required = true, onChangeText, onSelect, renderOptions: customRenderOptions, optionContainerClassName, optionItemClassName, optionNotFoundItemClassName } = _a, props = __rest(_a, ["id", "options", "value", "label", "size", "rounded", "variant", "helperText", "errorMessage", "fullwidth", "disabled", "error", "filterMode", "required", "onChangeText", "onSelect", "renderOptions", "optionContainerClassName", "optionItemClassName", "optionNotFoundItemClassName"]);
20
+ var { id, options = [], value, label, size = "md", rounded = "normal", variant = "outline", helperText, errorMessage, fullwidth = true, disabled = false, error = false, filterMode = false, required = true, modal = false, onChangeText, onSelect, renderOptions: customRenderOptions, optionContainerClassName, optionItemClassName, optionNotFoundItemClassName } = _a, props = __rest(_a, ["id", "options", "value", "label", "size", "rounded", "variant", "helperText", "errorMessage", "fullwidth", "disabled", "error", "filterMode", "required", "modal", "onChangeText", "onSelect", "renderOptions", "optionContainerClassName", "optionItemClassName", "optionNotFoundItemClassName"]);
21
21
  const _id = id || `${label}-select`;
22
22
  const [isFocused, setIsFocused] = useState(false);
23
23
  const [selectedOption, setSelectedOption] = useState(null);
@@ -26,12 +26,26 @@ const Dropdown = forwardRef((_a, ref) => {
26
26
  const dropdownRef = useRef(null);
27
27
  const inputRef = useRef(null);
28
28
  const [dropdownStyles, setDropdownStyles] = useState({});
29
+ const [isAbove, setIsAbove] = useState(false);
30
+ const [isInsideDialog, setIsInsideDialog] = useState(false);
29
31
  useImperativeHandle(ref, () => inputRef === null || inputRef === void 0 ? void 0 : inputRef.current);
30
32
  useEffect(() => {
31
33
  var _a;
32
34
  setSelectedOption(value);
33
35
  setTextValue((_a = value === null || value === void 0 ? void 0 : value.label) !== null && _a !== void 0 ? _a : "");
34
36
  }, [value]);
37
+ /** ✅ Auto-detect if inside a Dialog */
38
+ useEffect(() => {
39
+ let node = inputRef.current;
40
+ while (node) {
41
+ if (node.getAttribute("role") === "dialog") {
42
+ setIsInsideDialog(true);
43
+ return;
44
+ }
45
+ node = node.parentElement;
46
+ }
47
+ setIsInsideDialog(false);
48
+ }, []);
35
49
  const handleOnChangeText = useCallback((event) => {
36
50
  onChangeText === null || onChangeText === void 0 ? void 0 : onChangeText(event);
37
51
  setTextValue(event.target.value);
@@ -43,6 +57,7 @@ const Dropdown = forwardRef((_a, ref) => {
43
57
  setSelectedOption(option);
44
58
  setTextValue(option.label);
45
59
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(option);
60
+ setIsFocused(false);
46
61
  }, [onSelect]);
47
62
  const optionsFiltered = useMemo(() => {
48
63
  return options.filter((option) => {
@@ -51,36 +66,44 @@ const Dropdown = forwardRef((_a, ref) => {
51
66
  ((_a = option.label) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(textValue === null || textValue === void 0 ? void 0 : textValue.toLowerCase()));
52
67
  });
53
68
  }, [options, filterMode, textValue]);
69
+ const usePortal = isInsideDialog ? false : modal;
54
70
  const updateDropdownPosition = useCallback(() => {
55
- var _a;
56
- if (inputRef.current) {
71
+ if (inputRef.current && dropdownRef.current) {
57
72
  const rect = inputRef.current.getBoundingClientRect();
58
- const dropdownHeight = ((_a = dropdownRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
73
+ const dropdownHeight = dropdownRef.current.offsetHeight;
59
74
  const spaceBelow = window.innerHeight - rect.bottom;
60
75
  const spaceAbove = rect.top;
61
- const position = spaceBelow >= dropdownHeight || spaceBelow > spaceAbove
62
- ? {
63
- top: `${rect.bottom + window.scrollY}px`,
64
- left: `${rect.left + window.scrollX}px`,
65
- width: `${rect.width}px`,
66
- }
67
- : {
68
- top: `${rect.top - dropdownHeight + window.scrollY}px`,
69
- left: `${rect.left + window.scrollX}px`,
76
+ const shouldOpenAbove = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
77
+ setIsAbove(shouldOpenAbove);
78
+ if (usePortal) {
79
+ setDropdownStyles({
80
+ position: "absolute",
81
+ top: shouldOpenAbove
82
+ ? `${rect.top - dropdownHeight}px`
83
+ : `${rect.bottom}px`,
84
+ left: `${rect.left}px`,
70
85
  width: `${rect.width}px`,
71
- };
72
- setDropdownStyles(position);
86
+ zIndex: 9999,
87
+ });
88
+ }
89
+ else {
90
+ setDropdownStyles({
91
+ position: "absolute",
92
+ top: shouldOpenAbove ? `-${dropdownHeight}px` : "100%",
93
+ left: "0",
94
+ width: "100%",
95
+ zIndex: 9999,
96
+ });
97
+ }
73
98
  }
74
- }, []);
99
+ }, [modal, isInsideDialog, usePortal]);
75
100
  useEffect(() => {
76
101
  if (isFocused) {
77
102
  updateDropdownPosition();
78
103
  window.addEventListener("resize", updateDropdownPosition);
79
- window.addEventListener("scroll", updateDropdownPosition, true);
80
104
  }
81
105
  return () => {
82
106
  window.removeEventListener("resize", updateDropdownPosition);
83
- window.removeEventListener("scroll", updateDropdownPosition, true);
84
107
  };
85
108
  }, [isFocused, updateDropdownPosition]);
86
109
  const renderOptions = () => {
@@ -93,7 +116,7 @@ const Dropdown = forwardRef((_a, ref) => {
93
116
  dropdownRef,
94
117
  });
95
118
  }
96
- return (_jsxs("ul", { className: cn("absolute mt-1 w-full bg-base-popup border border-base-popup text-base-popup-foreground rounded-md shadow-md z-50 max-h-60 overflow-y-auto", optionContainerClassName), style: dropdownStyles, ref: dropdownRef, children: [optionsFiltered.map((option) => {
119
+ return (_jsxs("ul", { className: cn("absolute mt-1 w-full bg-base-popup border border-base-popup text-base-popup-foreground rounded-md shadow-md z-[9999] max-h-60 overflow-y-auto", !usePortal && (isAbove ? "bottom-full mb-1" : "top-full mt-1"), optionContainerClassName), style: dropdownStyles, ref: dropdownRef, children: [optionsFiltered.map((option) => {
97
120
  if (option.renderLabel) {
98
121
  return (_jsx(Fragment, { children: option.renderLabel({
99
122
  value: option.value,
@@ -104,7 +127,9 @@ const Dropdown = forwardRef((_a, ref) => {
104
127
  }),
105
128
  }) }, option.value));
106
129
  }
107
- return (_jsx("li", { onMouseDown: () => handleOptionClick(option), className: cn(`px-4 py-2 hover:bg-primary-hover-bg cursor-pointer`, optionItemClassName, {
130
+ return (_jsx("li", { onMouseDown: () => {
131
+ handleOptionClick(option);
132
+ }, className: cn(`px-4 py-2 hover:bg-primary-hover-bg cursor-pointer`, optionItemClassName, {
108
133
  "bg-base-popup-highligh": (selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.value) === option.value,
109
134
  }), children: option.label }, option.value));
110
135
  }), optionsFiltered.length === 0 && (_jsx("li", { className: cn("px-4 py-14 text-center text-input-text", optionNotFoundItemClassName), children: "Not found" }))] }));
@@ -136,7 +161,7 @@ const Dropdown = forwardRef((_a, ref) => {
136
161
  }, [optionsFiltered, textValue]);
137
162
  const handleOnBlur = useCallback((e) => {
138
163
  var _a;
139
- setIsFocused(false);
164
+ setTimeout(() => setIsFocused(false), 200);
140
165
  clearMismatchValue(e);
141
166
  (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
142
167
  }, [props === null || props === void 0 ? void 0 : props.onBlur, clearMismatchValue]);
@@ -145,6 +170,7 @@ const Dropdown = forwardRef((_a, ref) => {
145
170
  keyCode.current = e.code;
146
171
  (_a = props === null || props === void 0 ? void 0 : props.onKeyDown) === null || _a === void 0 ? void 0 : _a.call(props, e);
147
172
  }, [props === null || props === void 0 ? void 0 : props.onKeyDown]);
148
- return (_jsxs("div", { className: `relative ${fullwidth ? "w-full" : ""}`, children: [_jsx(TextInput, Object.assign({ hasClearIcon: false, endIcon: _jsx("div", { className: iconWrapperVariant({ size }), children: _jsx(ChevronDownIcon, { className: dropdownIconVariant({ size, isFocus: isFocused }) }) }) }, props, { ref: inputRef, readOnly: !filterMode, value: textValue, onChange: handleOnChangeText, label: label, placeholder: " ", type: "text", autoComplete: "off", rounded: rounded, variant: variant, helperText: helperText, errorMessage: errorMessage, fullwidth: fullwidth, error: error, required: required, id: _id, disabled: disabled, size: size, className: customInputVariant({ size }), onFocus: handleOnFocus, onBlur: handleOnBlur, onKeyDown: handleOnKeyDown })), isFocused && _jsx(Portal.Root, { children: renderOptions() })] }));
173
+ return (_jsxs("div", { className: `relative ${fullwidth ? "w-full" : ""}`, children: [_jsx(TextInput, Object.assign({ hasClearIcon: false, endIcon: _jsx("div", { className: iconWrapperVariant({ size }), children: _jsx(ChevronDownIcon, { className: dropdownIconVariant({ size, isFocus: isFocused }) }) }) }, props, { ref: inputRef, readOnly: !filterMode, value: textValue, onChange: handleOnChangeText, label: label, placeholder: " ", type: "text", autoComplete: "off", rounded: rounded, variant: variant, helperText: helperText, errorMessage: errorMessage, fullwidth: fullwidth, error: error, required: required, id: _id, disabled: disabled, size: size, className: customInputVariant({ size }), onFocus: handleOnFocus, onBlur: handleOnBlur, onKeyDown: handleOnKeyDown })), isFocused &&
174
+ (usePortal ? (_jsx(Portal.Root, { container: document.body, children: renderOptions() })) : (renderOptions()))] }));
149
175
  });
150
176
  export default Dropdown;
@@ -599,6 +599,9 @@ input[type=number] {
599
599
  .bottom-\[40px\]{
600
600
  bottom: 40px;
601
601
  }
602
+ .bottom-full{
603
+ bottom: 100%;
604
+ }
602
605
  .left-0{
603
606
  left: 0px;
604
607
  }
@@ -638,6 +641,9 @@ input[type=number] {
638
641
  .top-\[50\%\]{
639
642
  top: 50%;
640
643
  }
644
+ .top-full{
645
+ top: 100%;
646
+ }
641
647
  .z-0{
642
648
  z-index: 0;
643
649
  }
@@ -650,6 +656,9 @@ input[type=number] {
650
656
  .z-\[100\]{
651
657
  z-index: 100;
652
658
  }
659
+ .z-\[9999\]{
660
+ z-index: 9999;
661
+ }
653
662
  .col-span-3{
654
663
  grid-column: span 3 / span 3;
655
664
  }
@@ -678,6 +687,9 @@ input[type=number] {
678
687
  .-mt-\[30px\]{
679
688
  margin-top: -30px;
680
689
  }
690
+ .mb-1{
691
+ margin-bottom: 0.25rem;
692
+ }
681
693
  .ml-2{
682
694
  margin-left: 0.5rem;
683
695
  }