Skip to content

Commit d3cefb4

Browse files
committed
fix(ItemBase): tooltip logic * 3
1 parent 19d17bf commit d3cefb4

File tree

2 files changed

+116
-12
lines changed

2 files changed

+116
-12
lines changed

src/components/content/ItemBase/ItemBase.stories.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { IconCoin, IconSettings, IconUser } from '@tabler/icons-react';
2+
import { useState } from 'react';
23
import { expect, userEvent, waitFor, within } from 'storybook/test';
34

45
import { DirectionIcon } from '../../../icons';
56
import { baseProps } from '../../../stories/lists/baseProps';
7+
import { Button } from '../../actions';
68
import { Space } from '../../layout/Space';
79
import { Title } from '../Title';
810

@@ -1105,3 +1107,66 @@ WithAutoTooltip.parameters = {
11051107
},
11061108
},
11071109
};
1110+
1111+
export const DynamicAutoTooltip: StoryFn<CubeItemBaseProps> = () => {
1112+
const [width, setWidth] = useState('400px');
1113+
1114+
return (
1115+
<div>
1116+
<ItemBase
1117+
qa="dynamic-tooltip-item"
1118+
icon={<IconUser />}
1119+
style={{ width }}
1120+
tooltip={{ delay: 0 }}
1121+
>
1122+
This is a very long label that will eventually overflow
1123+
</ItemBase>
1124+
<Button
1125+
qa="resize-button"
1126+
onPress={() =>
1127+
width === '400px' ? setWidth('150px') : setWidth('400px')
1128+
}
1129+
>
1130+
Resize
1131+
</Button>
1132+
</div>
1133+
);
1134+
};
1135+
1136+
// DynamicAutoTooltip.play = async ({ canvasElement }) => {
1137+
// const canvas = within(canvasElement);
1138+
// await timeout(250);
1139+
1140+
// const item = await canvas.findByTestId('dynamic-tooltip-item');
1141+
1142+
// // Test 1: No tooltip when wide
1143+
// // this is a weird hack that makes tooltip working properly on page load
1144+
// await userEvent.unhover(item);
1145+
// await userEvent.hover(item);
1146+
// await timeout(1000);
1147+
// expect(canvas.queryByRole('tooltip')).toBe(null);
1148+
1149+
// // Change width to trigger overflow
1150+
// await userEvent.unhover(item);
1151+
// const resizeButton = await canvas.findByTestId('resize-button');
1152+
// await userEvent.click(resizeButton);
1153+
// // Unhover button after clicking to clear any hover state
1154+
// await userEvent.unhover(resizeButton);
1155+
// await timeout(1000);
1156+
1157+
// // Test 2: Tooltip appears when narrow
1158+
// // this is a weird hack that makes tooltip working properly
1159+
// await userEvent.unhover(item);
1160+
// await userEvent.hover(item);
1161+
1162+
// await waitFor(() => expect(canvas.getByRole('tooltip')).toBeVisible());
1163+
// };
1164+
1165+
DynamicAutoTooltip.parameters = {
1166+
docs: {
1167+
description: {
1168+
story:
1169+
'Tests the dynamic auto tooltip behavior that responds to width changes. Initially the item is wide and no tooltip appears. When the width is reduced, the label overflows and the tooltip automatically appears on hover.',
1170+
},
1171+
},
1172+
};

src/components/content/ItemBase/ItemBase.tsx

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useCallback,
88
useEffect,
99
useMemo,
10+
useRef,
1011
useState,
1112
} from 'react';
1213
import { OverlayProps } from 'react-aria';
@@ -393,6 +394,8 @@ export function useAutoTooltip({
393394
// Track label overflow for auto tooltip (only when enabled)
394395
const mergedLabelRef = useCombinedRefs((labelProps as any)?.ref);
395396
const [isLabelOverflowed, setIsLabelOverflowed] = useState(false);
397+
const observedElementRef = useRef<HTMLElement | null>(null);
398+
const resizeObserverRef = useRef<ResizeObserver | null>(null);
396399

397400
const checkLabelOverflow = useCallback(() => {
398401
const label = mergedLabelRef.current;
@@ -402,7 +405,6 @@ export function useAutoTooltip({
402405
}
403406

404407
const hasOverflow = label.scrollWidth > label.clientWidth;
405-
406408
setIsLabelOverflowed(hasOverflow);
407409
}, [mergedLabelRef]);
408410

@@ -412,24 +414,61 @@ export function useAutoTooltip({
412414
}
413415
}, [isAutoTooltipEnabled, checkLabelOverflow]);
414416

415-
useEffect(() => {
416-
if (!isAutoTooltipEnabled) return;
417-
418-
const label = mergedLabelRef.current;
419-
if (!label) return;
417+
// Attach ResizeObserver via callback ref to handle DOM node changes
418+
const handleLabelElementRef = useCallback(
419+
(element: HTMLElement | null) => {
420+
// Sync to combined ref so external refs receive the node
421+
(mergedLabelRef as any).current = element;
422+
423+
// Disconnect previous observer
424+
if (resizeObserverRef.current) {
425+
try {
426+
resizeObserverRef.current.disconnect();
427+
} catch {
428+
// do nothing
429+
}
430+
resizeObserverRef.current = null;
431+
}
420432

421-
const resizeObserver = new ResizeObserver(checkLabelOverflow);
422-
resizeObserver.observe(label);
433+
observedElementRef.current = element;
434+
435+
if (element && isAutoTooltipEnabled) {
436+
// Create a fresh observer to capture the latest callback
437+
const obs = new ResizeObserver(() => {
438+
checkLabelOverflow();
439+
});
440+
resizeObserverRef.current = obs;
441+
obs.observe(element);
442+
// Initial check
443+
checkLabelOverflow();
444+
} else {
445+
setIsLabelOverflowed(false);
446+
}
447+
},
448+
[mergedLabelRef, isAutoTooltipEnabled, checkLabelOverflow],
449+
);
423450

424-
return () => resizeObserver.disconnect();
425-
}, [isAutoTooltipEnabled, checkLabelOverflow, mergedLabelRef]);
451+
// Cleanup on unmount
452+
useEffect(() => {
453+
return () => {
454+
if (resizeObserverRef.current) {
455+
try {
456+
resizeObserverRef.current.disconnect();
457+
} catch {
458+
// do nothing
459+
}
460+
resizeObserverRef.current = null;
461+
}
462+
observedElementRef.current = null;
463+
};
464+
}, []);
426465

427466
const finalLabelProps = useMemo(() => {
428467
return {
429468
...(labelProps || {}),
430-
ref: mergedLabelRef,
469+
ref: handleLabelElementRef,
431470
} as Props & { ref?: any };
432-
}, [labelProps, mergedLabelRef]);
471+
}, [labelProps, handleLabelElementRef]);
433472

434473
const renderWithTooltip = useCallback(
435474
(

0 commit comments

Comments
 (0)