@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;
package/dist/index.d.ts CHANGED
@@ -165,6 +165,7 @@ type DropdownProps = {
165
165
  disabled?: boolean;
166
166
  error?: boolean;
167
167
  required?: boolean;
168
+ modal?: boolean;
168
169
  className?: string;
169
170
  optionContainerClassName?: string;
170
171
  optionItemClassName?: string;
@@ -194,6 +195,7 @@ declare const Dropdown: React__default.ForwardRefExoticComponent<{
194
195
  disabled?: boolean | undefined;
195
196
  error?: boolean | undefined;
196
197
  required?: boolean | undefined;
198
+ modal?: boolean | undefined;
197
199
  className?: string | undefined;
198
200
  optionContainerClassName?: string | undefined;
199
201
  optionItemClassName?: string | undefined;
@@ -2260,6 +2260,10 @@ input[type=number] {
2260
2260
  bottom: 40px;
2261
2261
  }
2262
2262
 
2263
+ .bottom-full {
2264
+ bottom: 100%;
2265
+ }
2266
+
2263
2267
  .left-0 {
2264
2268
  left: 0px;
2265
2269
  }
@@ -2312,6 +2316,10 @@ input[type=number] {
2312
2316
  top: 50%;
2313
2317
  }
2314
2318
 
2319
+ .top-full {
2320
+ top: 100%;
2321
+ }
2322
+
2315
2323
  .z-0 {
2316
2324
  z-index: 0;
2317
2325
  }
@@ -2328,6 +2336,10 @@ input[type=number] {
2328
2336
  z-index: 100;
2329
2337
  }
2330
2338
 
2339
+ .z-\[9999\] {
2340
+ z-index: 9999;
2341
+ }
2342
+
2331
2343
  .col-span-3 {
2332
2344
  grid-column: span 3 / span 3;
2333
2345
  }
@@ -2364,6 +2376,10 @@ input[type=number] {
2364
2376
  margin-top: -30px;
2365
2377
  }
2366
2378
 
2379
+ .mb-1 {
2380
+ margin-bottom: 0.25rem;
2381
+ }
2382
+
2367
2383
  .ml-2 {
2368
2384
  margin-left: 0.5rem;
2369
2385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.0.64",
3
+ "version": "0.0.66",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,6 +47,7 @@ export type DropdownProps = {
47
47
  disabled?: boolean;
48
48
  error?: boolean;
49
49
  required?: boolean;
50
+ modal?: boolean;
50
51
  className?: string;
51
52
  optionContainerClassName?: string;
52
53
  optionItemClassName?: string;
@@ -81,6 +82,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
81
82
  error = false,
82
83
  filterMode = false,
83
84
  required = true,
85
+ modal = false,
84
86
  onChangeText,
85
87
  onSelect,
86
88
  renderOptions: customRenderOptions,
@@ -102,7 +104,9 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
102
104
 
103
105
  const dropdownRef = useRef<HTMLUListElement>(null);
104
106
  const inputRef = useRef<HTMLInputElement>(null);
105
- const [dropdownStyles, setDropdownStyles] = useState({});
107
+ const [dropdownStyles, setDropdownStyles] = useState<CSSProperties>({});
108
+ const [isAbove, setIsAbove] = useState(false);
109
+ const [isInsideDialog, setIsInsideDialog] = useState(false);
106
110
 
107
111
  useImperativeHandle(ref, () => inputRef?.current as HTMLInputElement);
108
112
 
@@ -111,6 +115,19 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
111
115
  setTextValue(value?.label ?? "");
112
116
  }, [value]);
113
117
 
118
+ /** ✅ Auto-detect if inside a Dialog */
119
+ useEffect(() => {
120
+ let node: HTMLElement | null = inputRef.current;
121
+ while (node) {
122
+ if (node.getAttribute("role") === "dialog") {
123
+ setIsInsideDialog(true);
124
+ return;
125
+ }
126
+ node = node.parentElement;
127
+ }
128
+ setIsInsideDialog(false);
129
+ }, []);
130
+
114
131
  const handleOnChangeText = useCallback(
115
132
  (event: React.ChangeEvent<HTMLInputElement>) => {
116
133
  onChangeText?.(event);
@@ -128,6 +145,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
128
145
  setSelectedOption(option);
129
146
  setTextValue(option.label);
130
147
  onSelect?.(option);
148
+ setIsFocused(false);
131
149
  },
132
150
  [onSelect]
133
151
  );
@@ -140,40 +158,49 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
140
158
  );
141
159
  }, [options, filterMode, textValue]);
142
160
 
161
+ const usePortal = isInsideDialog ? false : modal;
162
+
143
163
  const updateDropdownPosition = useCallback(() => {
144
- if (inputRef.current) {
164
+ if (inputRef.current && dropdownRef.current) {
145
165
  const rect = inputRef.current.getBoundingClientRect();
146
- const dropdownHeight = dropdownRef.current?.offsetHeight || 0;
166
+ const dropdownHeight = dropdownRef.current.offsetHeight;
147
167
  const spaceBelow = window.innerHeight - rect.bottom;
148
168
  const spaceAbove = rect.top;
149
169
 
150
- const position =
151
- spaceBelow >= dropdownHeight || spaceBelow > spaceAbove
152
- ? {
153
- top: `${rect.bottom + window.scrollY}px`,
154
- left: `${rect.left + window.scrollX}px`,
155
- width: `${rect.width}px`,
156
- }
157
- : {
158
- top: `${rect.top - dropdownHeight + window.scrollY}px`,
159
- left: `${rect.left + window.scrollX}px`,
160
- width: `${rect.width}px`,
161
- };
162
-
163
- setDropdownStyles(position);
170
+ const shouldOpenAbove =
171
+ spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
172
+ setIsAbove(shouldOpenAbove);
173
+
174
+ if (usePortal) {
175
+ setDropdownStyles({
176
+ position: "absolute",
177
+ top: shouldOpenAbove
178
+ ? `${rect.top - dropdownHeight}px`
179
+ : `${rect.bottom}px`,
180
+ left: `${rect.left}px`,
181
+ width: `${rect.width}px`,
182
+ zIndex: 9999,
183
+ });
184
+ } else {
185
+ setDropdownStyles({
186
+ position: "absolute",
187
+ top: shouldOpenAbove ? `-${dropdownHeight}px` : "100%",
188
+ left: "0",
189
+ width: "100%",
190
+ zIndex: 9999,
191
+ });
192
+ }
164
193
  }
165
- }, []);
194
+ }, [modal, isInsideDialog, usePortal]);
166
195
 
167
196
  useEffect(() => {
168
197
  if (isFocused) {
169
198
  updateDropdownPosition();
170
199
  window.addEventListener("resize", updateDropdownPosition);
171
- window.addEventListener("scroll", updateDropdownPosition, true);
172
200
  }
173
201
 
174
202
  return () => {
175
203
  window.removeEventListener("resize", updateDropdownPosition);
176
- window.removeEventListener("scroll", updateDropdownPosition, true);
177
204
  };
178
205
  }, [isFocused, updateDropdownPosition]);
179
206
 
@@ -191,7 +218,8 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
191
218
  return (
192
219
  <ul
193
220
  className={cn(
194
- "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",
221
+ "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",
222
+ !usePortal && (isAbove ? "bottom-full mb-1" : "top-full mt-1"),
195
223
  optionContainerClassName
196
224
  )}
197
225
  style={dropdownStyles}
@@ -220,7 +248,9 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
220
248
  return (
221
249
  <li
222
250
  key={option.value}
223
- onMouseDown={() => handleOptionClick(option)}
251
+ onMouseDown={() => {
252
+ handleOptionClick(option);
253
+ }}
224
254
  className={cn(
225
255
  `px-4 py-2 hover:bg-primary-hover-bg cursor-pointer`,
226
256
  optionItemClassName,
@@ -289,7 +319,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
289
319
 
290
320
  const handleOnBlur = useCallback(
291
321
  (e: React.FocusEvent<HTMLInputElement, Element>) => {
292
- setIsFocused(false);
322
+ setTimeout(() => setIsFocused(false), 200);
293
323
  clearMismatchValue(e);
294
324
  props?.onBlur?.(e);
295
325
  },
@@ -305,7 +335,7 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
305
335
  );
306
336
 
307
337
  return (
308
- <div className={`relative ${fullwidth ? "w-full" : ""}`}>
338
+ <div className={`relative ${fullwidth ? "w-full" : ""}`}>
309
339
  <TextInput
310
340
  hasClearIcon={false}
311
341
  endIcon={
@@ -339,7 +369,14 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
339
369
  onBlur={handleOnBlur}
340
370
  onKeyDown={handleOnKeyDown}
341
371
  />
342
- {isFocused && <Portal.Root>{renderOptions()}</Portal.Root>}
372
+ {isFocused &&
373
+ (usePortal ? (
374
+ <Portal.Root container={document.body}>
375
+ {renderOptions()}
376
+ </Portal.Root>
377
+ ) : (
378
+ renderOptions()
379
+ ))}
343
380
  </div>
344
381
  );
345
382
  }