@udixio/ui-react 2.9.14 → 2.9.16

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 (45) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +3875 -3750
  4. package/dist/lib/components/AnchorPositioner.d.ts +11 -0
  5. package/dist/lib/components/AnchorPositioner.d.ts.map +1 -0
  6. package/dist/lib/components/FabMenu.d.ts.map +1 -1
  7. package/dist/lib/components/TextField.d.ts +2 -2
  8. package/dist/lib/components/TextField.d.ts.map +1 -1
  9. package/dist/lib/components/Tooltip.d.ts +1 -1
  10. package/dist/lib/components/Tooltip.d.ts.map +1 -1
  11. package/dist/lib/components/index.d.ts +1 -0
  12. package/dist/lib/components/index.d.ts.map +1 -1
  13. package/dist/lib/effects/smooth-scroll.effect.d.ts +14 -0
  14. package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
  15. package/dist/lib/hooks/index.d.ts +0 -1
  16. package/dist/lib/hooks/index.d.ts.map +1 -1
  17. package/dist/lib/interfaces/text-field.interface.d.ts +1 -1
  18. package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
  19. package/dist/lib/interfaces/tooltip.interface.d.ts +2 -0
  20. package/dist/lib/interfaces/tooltip.interface.d.ts.map +1 -1
  21. package/dist/lib/styles/card.style.d.ts +2 -2
  22. package/dist/lib/styles/checkbox.style.d.ts +2 -2
  23. package/dist/lib/styles/fab.style.d.ts +2 -2
  24. package/dist/lib/styles/navigation-rail-item.style.d.ts +2 -2
  25. package/dist/lib/styles/side-sheet.style.d.ts +2 -2
  26. package/dist/lib/styles/slider.style.d.ts +2 -2
  27. package/dist/lib/styles/tab.style.d.ts +2 -2
  28. package/dist/lib/styles/text-field.style.d.ts +4 -4
  29. package/dist/lib/styles/tooltip.style.d.ts +8 -4
  30. package/dist/lib/styles/tooltip.style.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/lib/components/AnchorPositioner.tsx +132 -0
  33. package/src/lib/components/FabMenu.tsx +13 -10
  34. package/src/lib/components/TextField.tsx +132 -19
  35. package/src/lib/components/Tooltip.tsx +13 -13
  36. package/src/lib/components/index.ts +1 -0
  37. package/src/lib/effects/smooth-scroll.effect.tsx +15 -1
  38. package/src/lib/hooks/index.ts +0 -1
  39. package/src/lib/interfaces/text-field.interface.ts +1 -1
  40. package/src/lib/interfaces/tooltip.interface.ts +2 -0
  41. package/src/lib/styles/date-picker.style.ts +1 -1
  42. package/src/lib/styles/side-sheet.style.ts +2 -2
  43. package/dist/lib/hooks/useTooltipPosition.d.ts +0 -22
  44. package/dist/lib/hooks/useTooltipPosition.d.ts.map +0 -1
  45. package/src/lib/hooks/useTooltipPosition.ts +0 -95
@@ -1,13 +1,19 @@
1
- import React, { useEffect, useId, useState } from 'react';
1
+ import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
2
2
  import { Icon } from '../icon';
3
- import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons';
3
+ import {
4
+ faCalendarDays,
5
+ faCircleExclamation,
6
+ } from '@fortawesome/free-solid-svg-icons';
4
7
  import { motion } from 'motion/react';
8
+ import { DatePicker } from './DatePicker';
9
+ import { Button } from './Button';
5
10
 
6
11
  import TextareaAutosize from 'react-textarea-autosize';
7
12
  import { useTextFieldStyle } from '../styles/text-field.style';
8
13
  import { classNames } from '../utils';
9
14
  import { ReactProps } from '../utils/component';
10
- import { TextFieldInterface } from '../interfaces/text-field.interface';
15
+ import { AnchorPositioner } from './AnchorPositioner';
16
+ import { TextFieldInterface } from '../interfaces';
11
17
 
12
18
  /**
13
19
  * Text fields let users enter text into a UI
@@ -40,6 +46,7 @@ export const TextField = ({
40
46
  showSupportingText,
41
47
  id: idProp,
42
48
  style,
49
+ ref,
43
50
  ...restProps
44
51
  }: ReactProps<TextFieldInterface>) => {
45
52
  const generatedId = useId();
@@ -53,6 +60,12 @@ export const TextField = ({
53
60
  const [isFocused, setIsFocused] = useState(false);
54
61
  const [showErrorIcon, setShowErrorIcon] = useState(!!errorText?.length);
55
62
 
63
+ const internalRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
64
+ const inputRef = (ref as any) || internalRef;
65
+
66
+ const textFieldRef = useRef<HTMLDivElement>(null);
67
+ const calendarTriggerRef = useRef<HTMLDivElement>(null);
68
+
56
69
  const hasSupportingText =
57
70
  showSupportingText ?? (!!errorText?.length || !!supportingText?.length);
58
71
 
@@ -60,8 +73,6 @@ export const TextField = ({
60
73
  setShowErrorIcon(!!errorText?.length);
61
74
  }, [errorText]);
62
75
 
63
- const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
64
-
65
76
  const focusInput = () => {
66
77
  if (inputRef.current && !isFocused) {
67
78
  inputRef.current.focus();
@@ -96,6 +107,54 @@ export const TextField = ({
96
107
  }
97
108
  };
98
109
 
110
+ // Date Picker Logic
111
+ const isDateInput = type === 'date';
112
+ const [showDatePicker, setShowDatePicker] = useState(false);
113
+ const [tempDate, setTempDate] = useState<Date | null>(null);
114
+
115
+ const initialDateValue = useMemo(() => {
116
+ const val = String(value);
117
+ if (!val) return null;
118
+ const [y, m, d] = val.split('-').map(Number);
119
+ if (y && m && d) return new Date(y, m - 1, d);
120
+ return null;
121
+ }, [value]);
122
+
123
+ const handleDatePickerOpen = () => {
124
+ if (disabled) return;
125
+ setTempDate(initialDateValue);
126
+ setShowDatePicker(true);
127
+ };
128
+
129
+ const handleDateConfirm = () => {
130
+ const newValue = tempDate ? tempDate.toLocaleDateString('en-CA') : '';
131
+
132
+ if (!isControlled) {
133
+ setInternalValue(newValue);
134
+ }
135
+
136
+ if (onChange) {
137
+ // Create a synthetic event
138
+ const event = {
139
+ target: {
140
+ value: newValue,
141
+ name,
142
+ type,
143
+ },
144
+ } as React.ChangeEvent<HTMLInputElement>;
145
+ onChange(event);
146
+ }
147
+ setShowDatePicker(false);
148
+ };
149
+
150
+ const effectiveTrailingIcon =
151
+ isDateInput && !trailingIcon ? faCalendarDays : trailingIcon;
152
+
153
+ // Enhance styles for date input
154
+ const inputDateClass = isDateInput
155
+ ? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer'
156
+ : '';
157
+
99
158
  const styles = useTextFieldStyle({
100
159
  showSupportingText: hasSupportingText,
101
160
  isFocused,
@@ -110,7 +169,7 @@ export const TextField = ({
110
169
  supportingText,
111
170
  type,
112
171
  leadingIcon,
113
- trailingIcon,
172
+ trailingIcon: effectiveTrailingIcon,
114
173
  variant,
115
174
  errorText,
116
175
  value: String(value),
@@ -122,12 +181,14 @@ export const TextField = ({
122
181
  const textComponentProps = multiline ? {} : { type };
123
182
 
124
183
  const isFloating =
125
- isFocused || (typeof value === 'string' && value.length > 0);
184
+ isFocused ||
185
+ (typeof value === 'string' && value.length > 0) ||
186
+ type == 'date'; // Float label when picker open
126
187
  const showLegend = isFloating && variant === 'outlined';
127
188
  const showLabel = !showLegend;
128
189
 
129
190
  return (
130
- <div className={styles.textField} style={style}>
191
+ <div ref={textFieldRef} className={styles.textField} style={style}>
131
192
  <fieldset
132
193
  onClick={focusInput}
133
194
  className={styles.content}
@@ -200,11 +261,18 @@ export const TextField = ({
200
261
  ref={inputRef as any}
201
262
  value={value}
202
263
  onChange={handleChange}
203
- className={styles.input}
264
+ className={classNames(styles.input, inputDateClass)}
204
265
  id={id}
205
266
  name={name}
206
267
  placeholder={isFocused ? (placeholder ?? undefined) : ''}
207
- onFocus={handleOnFocus}
268
+ onFocus={(e) => {
269
+ handleOnFocus();
270
+ if (isDateInput) {
271
+ // Maybe open picker on focus? User preference.
272
+ // Often better on click/icon click.
273
+ // But let's stick to icon click for now or explicit open.
274
+ }
275
+ }}
208
276
  onBlur={handleBlur}
209
277
  disabled={disabled}
210
278
  autoComplete={autoComplete}
@@ -219,21 +287,31 @@ export const TextField = ({
219
287
 
220
288
  {!showErrorIcon && (
221
289
  <>
222
- {trailingIcon && (
290
+ {effectiveTrailingIcon && (
223
291
  <div
292
+ ref={isDateInput ? calendarTriggerRef : undefined}
224
293
  onClick={(event) => {
225
294
  event.stopPropagation();
295
+ if (isDateInput) handleDatePickerOpen();
226
296
  }}
227
- className={styles.trailingIcon}
228
- >
229
- {React.isValidElement(trailingIcon) ? (
230
- trailingIcon
231
- ) : (
232
- <Icon className={'h-5'} icon={trailingIcon}></Icon>
297
+ className={classNames(
298
+ styles.trailingIcon,
299
+ isDateInput && 'cursor-pointer',
233
300
  )}
301
+ >
302
+ <div className="flex items-center justify-center w-full h-full">
303
+ {React.isValidElement(effectiveTrailingIcon) ? (
304
+ effectiveTrailingIcon
305
+ ) : (
306
+ <Icon
307
+ className={'h-5'}
308
+ icon={effectiveTrailingIcon as any}
309
+ />
310
+ )}
311
+ </div>
234
312
  </div>
235
313
  )}
236
- {!trailingIcon && suffix && (
314
+ {!effectiveTrailingIcon && suffix && (
237
315
  <span className={styles.suffix}>{suffix}</span>
238
316
  )}
239
317
  </>
@@ -242,7 +320,7 @@ export const TextField = ({
242
320
  {showErrorIcon && (
243
321
  <div
244
322
  className={classNames(styles.trailingIcon, {
245
- ' absolute right-0': !trailingIcon,
323
+ ' absolute right-0': !effectiveTrailingIcon,
246
324
  })}
247
325
  >
248
326
  <Icon
@@ -252,6 +330,7 @@ export const TextField = ({
252
330
  </div>
253
331
  )}
254
332
  </fieldset>
333
+
255
334
  {hasSupportingText && (
256
335
  <p className={styles.supportingText} id={helperTextId}>
257
336
  {errorText?.length
@@ -261,6 +340,40 @@ export const TextField = ({
261
340
  : '\u00A0'}
262
341
  </p>
263
342
  )}
343
+
344
+ {isDateInput && showDatePicker && (
345
+ <>
346
+ <div
347
+ className="fixed inset-0 z-40 bg-transparent"
348
+ onClick={() => setShowDatePicker(false)}
349
+ />
350
+ <AnchorPositioner anchorRef={textFieldRef} position="bottom">
351
+ <div className="z-50 shadow-xl rounded-[28px] bg-surface-container-high overflow-hidden">
352
+ <DatePicker
353
+ className={''}
354
+ value={tempDate}
355
+ onChange={setTempDate}
356
+ />
357
+ <div className="flex justify-end gap-2 p-4 pt-0">
358
+ <Button
359
+ variant="text"
360
+ size="small"
361
+ onClick={() => setShowDatePicker(false)}
362
+ >
363
+ Cancel
364
+ </Button>
365
+ <Button
366
+ variant="filled"
367
+ size="small"
368
+ onClick={handleDateConfirm}
369
+ >
370
+ OK
371
+ </Button>
372
+ </div>
373
+ </div>
374
+ </AnchorPositioner>
375
+ </>
376
+ )}
264
377
  </div>
265
378
  );
266
379
  };
@@ -1,11 +1,11 @@
1
1
  import { cloneElement, isValidElement, useEffect, useRef } from 'react';
2
2
  import { MotionProps } from '../utils';
3
3
  import { Button } from './Button';
4
+ import { AnchorPositioner } from './AnchorPositioner';
4
5
  import { ToolTipInterface } from '../interfaces';
5
6
  import { useToolTipStyle } from '../styles';
6
7
  import { AnimatePresence, motion } from 'motion/react';
7
- import { SyncedFixedWrapper } from '../effects';
8
- import { useTooltipTrigger, useTooltipPosition } from '../hooks';
8
+ import { useTooltipTrigger } from '../hooks';
9
9
 
10
10
  /**
11
11
  * Tooltips display brief labels or messages
@@ -36,8 +36,12 @@ export const Tooltip = ({
36
36
  defaultOpen = false,
37
37
  onOpenChange,
38
38
  id,
39
+ anchorRef,
39
40
  ...props
40
41
  }: MotionProps<ToolTipInterface>) => {
42
+ const defaultPosition = variant === 'rich' ? 'bottom-right' : 'bottom';
43
+ const effectivePosition = positionProp || defaultPosition;
44
+
41
45
  transition = { duration: 0.3, ...transition };
42
46
 
43
47
  if (!children && !targetRef) {
@@ -50,6 +54,7 @@ export const Tooltip = ({
50
54
 
51
55
  const internalRef = useRef<HTMLElement | null>(null);
52
56
  const resolvedRef = targetRef || internalRef;
57
+ const positioningRef = anchorRef || resolvedRef;
53
58
 
54
59
  // Use the trigger hook for state management and accessibility
55
60
  const { triggerProps, tooltipProps, isOpen } = useTooltipTrigger({
@@ -62,14 +67,6 @@ export const Tooltip = ({
62
67
  id,
63
68
  });
64
69
 
65
- // Use the position hook for auto-positioning
66
- const { resolvedPosition } = useTooltipPosition({
67
- targetRef: resolvedRef,
68
- position: positionProp,
69
- variant,
70
- isOpen,
71
- });
72
-
73
70
  // Apply trigger props to the target element
74
71
  const enhancedChildren =
75
72
  !targetRef && isValidElement(children)
@@ -150,7 +147,7 @@ export const Tooltip = ({
150
147
  className,
151
148
  title,
152
149
  text,
153
- position: resolvedPosition,
150
+ position: effectivePosition,
154
151
  trigger,
155
152
  targetRef: targetRef as any,
156
153
  children: children as any,
@@ -172,7 +169,10 @@ export const Tooltip = ({
172
169
  {enhancedChildren}
173
170
  <AnimatePresence>
174
171
  {isOpen && (
175
- <SyncedFixedWrapper targetRef={resolvedRef}>
172
+ <AnchorPositioner
173
+ anchorRef={positioningRef}
174
+ position={effectivePosition}
175
+ >
176
176
  <motion.div
177
177
  initial={'close'}
178
178
  variants={variants}
@@ -209,7 +209,7 @@ export const Tooltip = ({
209
209
  )}
210
210
  </div>
211
211
  </motion.div>
212
- </SyncedFixedWrapper>
212
+ </AnchorPositioner>
213
213
  )}
214
214
  </AnimatePresence>
215
215
  </>
@@ -1,3 +1,4 @@
1
+ export * from './AnchorPositioner';
1
2
  export * from './Button';
2
3
  export * from './Card';
3
4
  export * from './Carousel';
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, ReactNode } from 'react';
1
+ import { ReactNode, useEffect, useRef } from 'react';
2
2
  import Lenis from 'lenis';
3
3
 
4
4
  export type SmoothScrollProps = {
@@ -36,6 +36,20 @@ export type SmoothScrollProps = {
36
36
  /**
37
37
  * SmoothScroll component using Lenis for smooth scrolling.
38
38
  * This component enables smooth scrolling at the document level.
39
+ *
40
+ * @warning **Use with caution.** Overriding native scroll behavior can cause:
41
+ * - **Accessibility issues**: Screen readers, keyboard navigation, and assistive technologies
42
+ * may not work correctly with custom scroll implementations.
43
+ * - **Anchor links broken**: `scrollIntoView()`, hash navigation (`#section`), and
44
+ * `window.scrollTo()` may behave unexpectedly or be ignored.
45
+ * - **Third-party library conflicts**: Libraries relying on native scroll events
46
+ * (infinite scroll, lazy loading, scroll-triggered animations) may malfunction.
47
+ * - **Browser features disabled**: Find-in-page (Ctrl+F), autoscroll, and native
48
+ * momentum scrolling on trackpads may not work as expected.
49
+ * - **Performance overhead**: The RAF loop runs continuously, which may impact
50
+ * battery life and performance on low-end devices.
51
+ * - **Mobile issues**: Touch gestures, pull-to-refresh, and overscroll behaviors
52
+ * can conflict with smooth scroll implementations.
39
53
  */
40
54
  export const SmoothScroll = ({
41
55
  transition = 1.2,
@@ -4,7 +4,6 @@ export type {
4
4
  UseTooltipTriggerReturn,
5
5
  } from './useTooltipTrigger';
6
6
 
7
- export { useTooltipPosition } from './useTooltipPosition';
8
7
  export type {
9
8
  UseTooltipPositionOptions,
10
9
  UseTooltipPositionReturn,
@@ -22,7 +22,7 @@ type Props = {
22
22
  id?: string;
23
23
  style?: React.CSSProperties;
24
24
  variant?: TextFieldVariant;
25
- type?: 'text' | 'password' | 'number';
25
+ type?: 'text' | 'password' | 'number' | 'date';
26
26
  autoComplete?: 'on' | 'off' | string;
27
27
  multiline?: boolean;
28
28
  };
@@ -38,6 +38,8 @@ export type ToolTipInterface<T extends HTMLElement = any> = {
38
38
  onOpenChange?: (open: boolean) => void;
39
39
  /** Custom ID for accessibility linking. Auto-generated if not provided. */
40
40
  id?: string;
41
+ /** Custom anchor for positioning. Defaults to the trigger element. */
42
+ anchorRef?: RefObject<HTMLElement>;
41
43
  } & (
42
44
  | {
43
45
  children?: never;
@@ -10,7 +10,7 @@ const datePickerConfig: ClassNameComponent<DatePickerInterface> = ({
10
10
  hasSelected,
11
11
  }) => ({
12
12
  datePicker: classNames(
13
- 'inline-flex flex-col bg-surface-container-high rounded-[28px] p-3 shadow-sm select-none', // Using shadow-sm as placeholder for elevation
13
+ 'inline-flex flex-col bg-surface-container-high rounded-[28px] p-3 select-none', // Using shadow-sm as placeholder for elevation
14
14
  'min-w-[320px]',
15
15
  ),
16
16
  header: classNames('flex items-center justify-between h-12 mb-2 px-2'),
@@ -25,9 +25,9 @@ export const sideSheetConfig: ClassNameComponent<SideSheetInterface> = ({
25
25
  },
26
26
  ],
27
27
  ),
28
- container: classNames('w-full overflow-hidden', {}),
29
- content: classNames('w-fit '),
28
+ container: classNames('w-full overflow-hidden flex flex-col', {}),
30
29
  header: classNames('p-4 flex items-center gap-2'),
30
+ content: classNames('flex-1 overflow-y-auto'),
31
31
  title: classNames('text-on-surface-variant text-title-large'),
32
32
  closeButton: classNames('ml-auto'),
33
33
  divider: classNames({ hidden: variant == 'modal' }),
@@ -1,22 +0,0 @@
1
- import { RefObject } from 'react';
2
- type Position = 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
3
- type Variant = 'plain' | 'rich';
4
- export interface UseTooltipPositionOptions {
5
- targetRef: RefObject<HTMLElement | null>;
6
- position?: Position;
7
- variant?: Variant;
8
- isOpen: boolean;
9
- }
10
- export interface UseTooltipPositionReturn {
11
- resolvedPosition: Position;
12
- }
13
- /**
14
- * Hook to calculate tooltip position using useLayoutEffect.
15
- * Auto-flips position if not enough viewport space.
16
- *
17
- * For plain variant: prefers left/right, falls back to top/bottom
18
- * For rich variant: uses corner positions (top-left, top-right, bottom-left, bottom-right)
19
- */
20
- export declare function useTooltipPosition({ targetRef, position: positionProp, variant, isOpen, }: UseTooltipPositionOptions): UseTooltipPositionReturn;
21
- export {};
22
- //# sourceMappingURL=useTooltipPosition.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useTooltipPosition.d.ts","sourceRoot":"","sources":["../../../src/lib/hooks/useTooltipPosition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAA6B,MAAM,OAAO,CAAC;AAE7D,KAAK,QAAQ,GACT,KAAK,GACL,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,WAAW,GACX,aAAa,GACb,cAAc,CAAC;AAEnB,KAAK,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhC,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,EAAE,QAAQ,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,QAAQ,EAAE,YAAY,EACtB,OAAiB,EACjB,MAAM,GACP,EAAE,yBAAyB,GAAG,wBAAwB,CAyDtD"}
@@ -1,95 +0,0 @@
1
- import { RefObject, useLayoutEffect, useState } from 'react';
2
-
3
- type Position =
4
- | 'top'
5
- | 'bottom'
6
- | 'left'
7
- | 'right'
8
- | 'top-left'
9
- | 'top-right'
10
- | 'bottom-left'
11
- | 'bottom-right';
12
-
13
- type Variant = 'plain' | 'rich';
14
-
15
- export interface UseTooltipPositionOptions {
16
- targetRef: RefObject<HTMLElement | null>;
17
- position?: Position;
18
- variant?: Variant;
19
- isOpen: boolean;
20
- }
21
-
22
- export interface UseTooltipPositionReturn {
23
- resolvedPosition: Position;
24
- }
25
-
26
- /**
27
- * Hook to calculate tooltip position using useLayoutEffect.
28
- * Auto-flips position if not enough viewport space.
29
- *
30
- * For plain variant: prefers left/right, falls back to top/bottom
31
- * For rich variant: uses corner positions (top-left, top-right, bottom-left, bottom-right)
32
- */
33
- export function useTooltipPosition({
34
- targetRef,
35
- position: positionProp,
36
- variant = 'plain',
37
- isOpen,
38
- }: UseTooltipPositionOptions): UseTooltipPositionReturn {
39
- const [resolvedPosition, setResolvedPosition] = useState<Position>(
40
- positionProp ?? 'bottom',
41
- );
42
-
43
- useLayoutEffect(() => {
44
- // If position is explicitly set, use it
45
- if (positionProp) {
46
- setResolvedPosition(positionProp);
47
- return;
48
- }
49
-
50
- // Only calculate if open and we have a target
51
- if (!isOpen || !targetRef.current || typeof window === 'undefined') {
52
- return;
53
- }
54
-
55
- const targetElement = targetRef.current;
56
- const rect = targetElement.getBoundingClientRect();
57
-
58
- const viewportWidth = window.innerWidth;
59
- const viewportHeight = window.innerHeight;
60
-
61
- // Normalized position (0-1 range)
62
- const x = rect.left / viewportWidth;
63
- const y = rect.top / viewportHeight;
64
-
65
- let newPosition: Position;
66
-
67
- if (variant === 'plain') {
68
- // Plain variant: prefer horizontal positioning, fall back to vertical
69
- if (x < 1 / 3) {
70
- newPosition = 'right';
71
- } else if (x > 2 / 3) {
72
- newPosition = 'left';
73
- } else {
74
- newPosition = y > 0.5 ? 'top' : 'bottom';
75
- }
76
- } else {
77
- // Rich variant: use corner positions
78
- if (x < 0.5 && y < 0.5) {
79
- newPosition = 'bottom-right';
80
- } else if (x >= 0.5 && y < 0.5) {
81
- newPosition = 'bottom-left';
82
- } else if (x >= 0.5 && y >= 0.5) {
83
- newPosition = 'top-left';
84
- } else {
85
- newPosition = 'top-right';
86
- }
87
- }
88
-
89
- setResolvedPosition(newPosition);
90
- }, [isOpen, targetRef, positionProp, variant]);
91
-
92
- return {
93
- resolvedPosition,
94
- };
95
- }