@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.
- package/CHANGELOG.md +28 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +3875 -3750
- package/dist/lib/components/AnchorPositioner.d.ts +11 -0
- package/dist/lib/components/AnchorPositioner.d.ts.map +1 -0
- package/dist/lib/components/FabMenu.d.ts.map +1 -1
- package/dist/lib/components/TextField.d.ts +2 -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 +1 -1
- package/src/lib/components/AnchorPositioner.tsx +132 -0
- package/src/lib/components/FabMenu.tsx +13 -10
- package/src/lib/components/TextField.tsx +132 -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,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 {
|
|
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';
|
|
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 ||
|
|
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={
|
|
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
|
-
{
|
|
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={
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
{!
|
|
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': !
|
|
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 {
|
|
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
|
-
}
|