@udixio/ui-react 2.9.14 → 2.9.15
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/CHANGELOG.md +14 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +3866 -3744
- package/dist/lib/components/AnchorPositioner.d.ts +11 -0
- package/dist/lib/components/AnchorPositioner.d.ts.map +1 -0
- package/dist/lib/components/TextField.d.ts +1 -2
- package/dist/lib/components/TextField.d.ts.map +1 -1
- package/dist/lib/components/Tooltip.d.ts +1 -1
- package/dist/lib/components/Tooltip.d.ts.map +1 -1
- package/dist/lib/components/index.d.ts +1 -0
- package/dist/lib/components/index.d.ts.map +1 -1
- package/dist/lib/effects/smooth-scroll.effect.d.ts +14 -0
- package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
- package/dist/lib/hooks/index.d.ts +0 -1
- package/dist/lib/hooks/index.d.ts.map +1 -1
- package/dist/lib/interfaces/text-field.interface.d.ts +1 -1
- package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/tooltip.interface.d.ts +2 -0
- package/dist/lib/interfaces/tooltip.interface.d.ts.map +1 -1
- package/dist/lib/styles/card.style.d.ts +2 -2
- package/dist/lib/styles/checkbox.style.d.ts +2 -2
- package/dist/lib/styles/fab.style.d.ts +2 -2
- package/dist/lib/styles/navigation-rail-item.style.d.ts +2 -2
- package/dist/lib/styles/side-sheet.style.d.ts +2 -2
- package/dist/lib/styles/slider.style.d.ts +2 -2
- package/dist/lib/styles/tab.style.d.ts +2 -2
- package/dist/lib/styles/text-field.style.d.ts +4 -4
- package/dist/lib/styles/tooltip.style.d.ts +8 -4
- package/dist/lib/styles/tooltip.style.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/lib/components/AnchorPositioner.tsx +132 -0
- package/src/lib/components/TextField.tsx +131 -19
- package/src/lib/components/Tooltip.tsx +13 -13
- package/src/lib/components/index.ts +1 -0
- package/src/lib/effects/smooth-scroll.effect.tsx +15 -1
- package/src/lib/hooks/index.ts +0 -1
- package/src/lib/interfaces/text-field.interface.ts +1 -1
- package/src/lib/interfaces/tooltip.interface.ts +2 -0
- package/src/lib/styles/date-picker.style.ts +1 -1
- package/src/lib/styles/side-sheet.style.ts +2 -2
- package/dist/lib/hooks/useTooltipPosition.d.ts +0 -22
- package/dist/lib/hooks/useTooltipPosition.d.ts.map +0 -1
- package/src/lib/hooks/useTooltipPosition.ts +0 -95
|
@@ -1,13 +1,18 @@
|
|
|
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 {
|
|
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 {
|
|
15
|
+
import { AnchorPositioner } from './AnchorPositioner';
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Text fields let users enter text into a UI
|
|
@@ -40,6 +45,7 @@ export const TextField = ({
|
|
|
40
45
|
showSupportingText,
|
|
41
46
|
id: idProp,
|
|
42
47
|
style,
|
|
48
|
+
ref,
|
|
43
49
|
...restProps
|
|
44
50
|
}: ReactProps<TextFieldInterface>) => {
|
|
45
51
|
const generatedId = useId();
|
|
@@ -53,6 +59,12 @@ export const TextField = ({
|
|
|
53
59
|
const [isFocused, setIsFocused] = useState(false);
|
|
54
60
|
const [showErrorIcon, setShowErrorIcon] = useState(!!errorText?.length);
|
|
55
61
|
|
|
62
|
+
const internalRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
63
|
+
const inputRef = (ref as any) || internalRef;
|
|
64
|
+
|
|
65
|
+
const textFieldRef = useRef<HTMLDivElement>(null);
|
|
66
|
+
const calendarTriggerRef = useRef<HTMLDivElement>(null);
|
|
67
|
+
|
|
56
68
|
const hasSupportingText =
|
|
57
69
|
showSupportingText ?? (!!errorText?.length || !!supportingText?.length);
|
|
58
70
|
|
|
@@ -60,8 +72,6 @@ export const TextField = ({
|
|
|
60
72
|
setShowErrorIcon(!!errorText?.length);
|
|
61
73
|
}, [errorText]);
|
|
62
74
|
|
|
63
|
-
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
64
|
-
|
|
65
75
|
const focusInput = () => {
|
|
66
76
|
if (inputRef.current && !isFocused) {
|
|
67
77
|
inputRef.current.focus();
|
|
@@ -96,6 +106,54 @@ export const TextField = ({
|
|
|
96
106
|
}
|
|
97
107
|
};
|
|
98
108
|
|
|
109
|
+
// Date Picker Logic
|
|
110
|
+
const isDateInput = type === 'date';
|
|
111
|
+
const [showDatePicker, setShowDatePicker] = useState(false);
|
|
112
|
+
const [tempDate, setTempDate] = useState<Date | null>(null);
|
|
113
|
+
|
|
114
|
+
const initialDateValue = useMemo(() => {
|
|
115
|
+
const val = String(value);
|
|
116
|
+
if (!val) return null;
|
|
117
|
+
const [y, m, d] = val.split('-').map(Number);
|
|
118
|
+
if (y && m && d) return new Date(y, m - 1, d);
|
|
119
|
+
return null;
|
|
120
|
+
}, [value]);
|
|
121
|
+
|
|
122
|
+
const handleDatePickerOpen = () => {
|
|
123
|
+
if (disabled) return;
|
|
124
|
+
setTempDate(initialDateValue);
|
|
125
|
+
setShowDatePicker(true);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleDateConfirm = () => {
|
|
129
|
+
const newValue = tempDate ? tempDate.toLocaleDateString('en-CA') : '';
|
|
130
|
+
|
|
131
|
+
if (!isControlled) {
|
|
132
|
+
setInternalValue(newValue);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (onChange) {
|
|
136
|
+
// Create a synthetic event
|
|
137
|
+
const event = {
|
|
138
|
+
target: {
|
|
139
|
+
value: newValue,
|
|
140
|
+
name,
|
|
141
|
+
type,
|
|
142
|
+
},
|
|
143
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
144
|
+
onChange(event);
|
|
145
|
+
}
|
|
146
|
+
setShowDatePicker(false);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const effectiveTrailingIcon =
|
|
150
|
+
isDateInput && !trailingIcon ? faCalendarDays : trailingIcon;
|
|
151
|
+
|
|
152
|
+
// Enhance styles for date input
|
|
153
|
+
const inputDateClass = isDateInput
|
|
154
|
+
? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer'
|
|
155
|
+
: '';
|
|
156
|
+
|
|
99
157
|
const styles = useTextFieldStyle({
|
|
100
158
|
showSupportingText: hasSupportingText,
|
|
101
159
|
isFocused,
|
|
@@ -110,7 +168,7 @@ export const TextField = ({
|
|
|
110
168
|
supportingText,
|
|
111
169
|
type,
|
|
112
170
|
leadingIcon,
|
|
113
|
-
trailingIcon,
|
|
171
|
+
trailingIcon: effectiveTrailingIcon,
|
|
114
172
|
variant,
|
|
115
173
|
errorText,
|
|
116
174
|
value: String(value),
|
|
@@ -122,12 +180,14 @@ export const TextField = ({
|
|
|
122
180
|
const textComponentProps = multiline ? {} : { type };
|
|
123
181
|
|
|
124
182
|
const isFloating =
|
|
125
|
-
isFocused ||
|
|
183
|
+
isFocused ||
|
|
184
|
+
(typeof value === 'string' && value.length > 0) ||
|
|
185
|
+
type == 'date'; // Float label when picker open
|
|
126
186
|
const showLegend = isFloating && variant === 'outlined';
|
|
127
187
|
const showLabel = !showLegend;
|
|
128
188
|
|
|
129
189
|
return (
|
|
130
|
-
<div className={styles.textField} style={style}>
|
|
190
|
+
<div ref={textFieldRef} className={styles.textField} style={style}>
|
|
131
191
|
<fieldset
|
|
132
192
|
onClick={focusInput}
|
|
133
193
|
className={styles.content}
|
|
@@ -200,11 +260,18 @@ export const TextField = ({
|
|
|
200
260
|
ref={inputRef as any}
|
|
201
261
|
value={value}
|
|
202
262
|
onChange={handleChange}
|
|
203
|
-
className={styles.input}
|
|
263
|
+
className={classNames(styles.input, inputDateClass)}
|
|
204
264
|
id={id}
|
|
205
265
|
name={name}
|
|
206
266
|
placeholder={isFocused ? (placeholder ?? undefined) : ''}
|
|
207
|
-
onFocus={
|
|
267
|
+
onFocus={(e) => {
|
|
268
|
+
handleOnFocus();
|
|
269
|
+
if (isDateInput) {
|
|
270
|
+
// Maybe open picker on focus? User preference.
|
|
271
|
+
// Often better on click/icon click.
|
|
272
|
+
// But let's stick to icon click for now or explicit open.
|
|
273
|
+
}
|
|
274
|
+
}}
|
|
208
275
|
onBlur={handleBlur}
|
|
209
276
|
disabled={disabled}
|
|
210
277
|
autoComplete={autoComplete}
|
|
@@ -219,21 +286,31 @@ export const TextField = ({
|
|
|
219
286
|
|
|
220
287
|
{!showErrorIcon && (
|
|
221
288
|
<>
|
|
222
|
-
{
|
|
289
|
+
{effectiveTrailingIcon && (
|
|
223
290
|
<div
|
|
291
|
+
ref={isDateInput ? calendarTriggerRef : undefined}
|
|
224
292
|
onClick={(event) => {
|
|
225
293
|
event.stopPropagation();
|
|
294
|
+
if (isDateInput) handleDatePickerOpen();
|
|
226
295
|
}}
|
|
227
|
-
className={
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
trailingIcon
|
|
231
|
-
) : (
|
|
232
|
-
<Icon className={'h-5'} icon={trailingIcon}></Icon>
|
|
296
|
+
className={classNames(
|
|
297
|
+
styles.trailingIcon,
|
|
298
|
+
isDateInput && 'cursor-pointer',
|
|
233
299
|
)}
|
|
300
|
+
>
|
|
301
|
+
<div className="flex items-center justify-center w-full h-full">
|
|
302
|
+
{React.isValidElement(effectiveTrailingIcon) ? (
|
|
303
|
+
effectiveTrailingIcon
|
|
304
|
+
) : (
|
|
305
|
+
<Icon
|
|
306
|
+
className={'h-5'}
|
|
307
|
+
icon={effectiveTrailingIcon as any}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
234
311
|
</div>
|
|
235
312
|
)}
|
|
236
|
-
{!
|
|
313
|
+
{!effectiveTrailingIcon && suffix && (
|
|
237
314
|
<span className={styles.suffix}>{suffix}</span>
|
|
238
315
|
)}
|
|
239
316
|
</>
|
|
@@ -242,7 +319,7 @@ export const TextField = ({
|
|
|
242
319
|
{showErrorIcon && (
|
|
243
320
|
<div
|
|
244
321
|
className={classNames(styles.trailingIcon, {
|
|
245
|
-
' absolute right-0': !
|
|
322
|
+
' absolute right-0': !effectiveTrailingIcon,
|
|
246
323
|
})}
|
|
247
324
|
>
|
|
248
325
|
<Icon
|
|
@@ -252,6 +329,7 @@ export const TextField = ({
|
|
|
252
329
|
</div>
|
|
253
330
|
)}
|
|
254
331
|
</fieldset>
|
|
332
|
+
|
|
255
333
|
{hasSupportingText && (
|
|
256
334
|
<p className={styles.supportingText} id={helperTextId}>
|
|
257
335
|
{errorText?.length
|
|
@@ -261,6 +339,40 @@ export const TextField = ({
|
|
|
261
339
|
: '\u00A0'}
|
|
262
340
|
</p>
|
|
263
341
|
)}
|
|
342
|
+
|
|
343
|
+
{isDateInput && showDatePicker && (
|
|
344
|
+
<>
|
|
345
|
+
<div
|
|
346
|
+
className="fixed inset-0 z-40 bg-transparent"
|
|
347
|
+
onClick={() => setShowDatePicker(false)}
|
|
348
|
+
/>
|
|
349
|
+
<AnchorPositioner anchorRef={textFieldRef} position="bottom">
|
|
350
|
+
<div className="z-50 shadow-xl rounded-[28px] bg-surface-container-high overflow-hidden">
|
|
351
|
+
<DatePicker
|
|
352
|
+
className={''}
|
|
353
|
+
value={tempDate}
|
|
354
|
+
onChange={setTempDate}
|
|
355
|
+
/>
|
|
356
|
+
<div className="flex justify-end gap-2 p-4 pt-0">
|
|
357
|
+
<Button
|
|
358
|
+
variant="text"
|
|
359
|
+
size="small"
|
|
360
|
+
onClick={() => setShowDatePicker(false)}
|
|
361
|
+
>
|
|
362
|
+
Cancel
|
|
363
|
+
</Button>
|
|
364
|
+
<Button
|
|
365
|
+
variant="filled"
|
|
366
|
+
size="small"
|
|
367
|
+
onClick={handleDateConfirm}
|
|
368
|
+
>
|
|
369
|
+
OK
|
|
370
|
+
</Button>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</AnchorPositioner>
|
|
374
|
+
</>
|
|
375
|
+
)}
|
|
264
376
|
</div>
|
|
265
377
|
);
|
|
266
378
|
};
|
|
@@ -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 {
|
|
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:
|
|
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
|
-
<
|
|
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
|
-
</
|
|
212
|
+
</AnchorPositioner>
|
|
213
213
|
)}
|
|
214
214
|
</AnimatePresence>
|
|
215
215
|
</>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef
|
|
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,
|
package/src/lib/hooks/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
}
|