Skip to content

Commit 74705a7

Browse files
waleedlatif1claude
andcommitted
refactor(emcn,resource-header): address PR #4844 review feedback
- useIsOverflowing now uses a callback ref so the ResizeObserver follows the element across mount/unmount/reassignment instead of capturing it once at mount. Safe for conditionally rendered consumers of the shared hook. (greptile P2) - Move POPOVER_ANIMATION_CLASSES out of chip-date-picker implementation internals into emcn/components/popover/popover-animation.ts, exported from the @/components/emcn barrel. Consumers now import from the module boundary. (greptile P2) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8da7e34 commit 74705a7

7 files changed

Lines changed: 56 additions & 29 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/resource/components/floating-overflow-text.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import type React from 'react'
4-
import { memo, useRef } from 'react'
4+
import { memo } from 'react'
55
import { cn } from '@/lib/core/utils/cn'
66
import {
77
FloatingTooltip,
@@ -30,10 +30,9 @@ export const FloatingOverflowText = memo(function FloatingOverflowText({
3030
className,
3131
showWhen,
3232
}: FloatingOverflowTextProps) {
33-
const textRef = useRef<HTMLSpanElement>(null)
34-
const isOverflowing = useIsOverflowing(textRef)
33+
const { ref: textRef, node, isOverflowing } = useIsOverflowing<HTMLSpanElement>()
3534
const { state, handlers } = useFloatingTooltip(() => {
36-
const element = textRef.current
35+
const element = node.current
3736
if (!element || label.length === 0) return false
3837
return Boolean(showWhen) || isTextClipped(element)
3938
})

apps/sim/app/workspace/[workspaceId]/components/resource/components/floating-tooltip.tsx

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { memo, type RefObject, useEffect, useMemo, useRef, useState } from 'react'
3+
import { memo, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createPortal } from 'react-dom'
55
import { cn } from '@/lib/core/utils/cn'
66

@@ -136,30 +136,52 @@ export function useFloatingTooltip(canShow: (target: HTMLElement) => boolean): {
136136
}
137137

138138
/**
139-
* Tracks whether a referenced element's text is horizontally clipped, staying in
140-
* sync via a `ResizeObserver` and window resizes.
139+
* Tracks whether an element's text is horizontally clipped, re-measuring via a
140+
* `ResizeObserver` and window resizes.
141+
*
142+
* Returns a callback `ref` to attach to the element — the observer follows the
143+
* element across mount, unmount, and reassignment, so it is safe to use on
144+
* conditionally rendered children. `node` is a stable ref for reading the
145+
* current element (e.g. for live measurements in event handlers).
141146
*/
142-
export function useIsOverflowing(ref: RefObject<HTMLElement | null>): boolean {
147+
export function useIsOverflowing<T extends HTMLElement = HTMLElement>(): {
148+
ref: (node: T | null) => void
149+
node: RefObject<T | null>
150+
isOverflowing: boolean
151+
} {
143152
const [isOverflowing, setIsOverflowing] = useState(false)
153+
const nodeRef = useRef<T | null>(null)
154+
const observerRef = useRef<ResizeObserver | null>(null)
144155

145-
useEffect(() => {
146-
const element = ref.current
147-
if (!element) return
148-
149-
const update = () => setIsOverflowing(isTextClipped(element))
150-
update()
156+
const measure = useCallback(() => {
157+
const element = nodeRef.current
158+
if (element) setIsOverflowing(isTextClipped(element))
159+
}, [])
151160

152-
const resizeObserver = new ResizeObserver(update)
153-
resizeObserver.observe(element)
154-
window.addEventListener('resize', update)
161+
const ref = useCallback(
162+
(node: T | null) => {
163+
observerRef.current?.disconnect()
164+
observerRef.current = null
165+
nodeRef.current = node
166+
if (!node) return
167+
168+
measure()
169+
const observer = new ResizeObserver(measure)
170+
observer.observe(node)
171+
observerRef.current = observer
172+
},
173+
[measure]
174+
)
155175

176+
useEffect(() => {
177+
window.addEventListener('resize', measure)
156178
return () => {
157-
resizeObserver.disconnect()
158-
window.removeEventListener('resize', update)
179+
window.removeEventListener('resize', measure)
180+
observerRef.current?.disconnect()
159181
}
160-
}, [ref])
182+
}, [measure])
161183

162-
return isOverflowing
184+
return { ref, node: nodeRef, isOverflowing }
163185
}
164186

165187
/** Whether an element's content is wider than its visible box. */

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import {
1010
DropdownMenuItem,
1111
DropdownMenuTrigger,
1212
Plus,
13+
POPOVER_ANIMATION_CLASSES,
1314
Popover,
1415
PopoverAnchor,
1516
PopoverContent,
1617
PopoverItem,
1718
PopoverSection,
1819
} from '@/components/emcn'
19-
import { POPOVER_ANIMATION_CLASSES } from '@/components/emcn/components/chip-date-picker/chip-date-picker'
2020
import { cn } from '@/lib/core/utils/cn'
2121
import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input'
2222
import { FloatingOverflowText } from '@/app/workspace/[workspaceId]/components/resource/components/floating-overflow-text'
@@ -259,10 +259,9 @@ const BreadcrumbSegment = memo(function BreadcrumbSegment({
259259
editing,
260260
className,
261261
}: BreadcrumbSegmentProps) {
262-
const labelRef = useRef<HTMLSpanElement>(null)
263-
const isOverflowing = useIsOverflowing(labelRef)
262+
const { ref: labelRef, node: labelNode, isOverflowing } = useIsOverflowing<HTMLSpanElement>()
264263
const { state: tooltipState, handlers: tooltipHandlers } = useFloatingTooltip((target) =>
265-
isBreadcrumbTextClipped(labelRef.current, target)
264+
isBreadcrumbTextClipped(labelNode.current, target)
266265
)
267266

268267
if (editing?.isEditing) {

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import {
1111
DropdownMenuSeparator,
1212
DropdownMenuTrigger,
1313
ListFilter,
14+
POPOVER_ANIMATION_CLASSES,
1415
Search,
1516
X,
1617
} from '@/components/emcn'
17-
import { POPOVER_ANIMATION_CLASSES } from '@/components/emcn/components/chip-date-picker/chip-date-picker'
1818
import { cn } from '@/lib/core/utils/cn'
1919
import { FloatingOverflowText } from '@/app/workspace/[workspaceId]/components/resource/components/floating-overflow-text'
2020

apps/sim/components/emcn/components/chip-date-picker/chip-date-picker.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import { forwardRef, useState } from 'react'
44
import * as PopoverPrimitive from '@radix-ui/react-popover'
55
import { Calendar, formatDateLabel } from '@/components/emcn/components/calendar/calendar'
66
import { chipVariants, TRIGGER_BORDER_CLASS } from '@/components/emcn/components/chip/chip'
7+
import { POPOVER_ANIMATION_CLASSES } from '@/components/emcn/components/popover/popover-animation'
78
import { ChevronDown } from '@/components/emcn/icons'
89
import { cn } from '@/lib/core/utils/cn'
910

10-
export const POPOVER_ANIMATION_CLASSES =
11-
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'
12-
1311
export interface ChipDatePickerProps {
1412
/** Selected date as a `YYYY-MM-DD` string. */
1513
value?: string

apps/sim/components/emcn/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export {
136136
PopoverTrigger,
137137
usePopoverContext,
138138
} from './popover/popover'
139+
export { POPOVER_ANIMATION_CLASSES } from './popover/popover-animation'
139140
export {
140141
SModal,
141142
SModalClose,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Radix popover open/close animation classes — fade + zoom + directional slide,
3+
* with `motion-reduce` opt-out. Shared across emcn popover-style surfaces
4+
* (`Popover`, `ChipDatePicker`, and consumers that build their own popover
5+
* content). Apply alongside a surface's own layout/background classes.
6+
*/
7+
export const POPOVER_ANIMATION_CLASSES =
8+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'

0 commit comments

Comments
 (0)