everweij / react-laag

Hooks to build things like tooltips, dropdown menu's and popovers in React
https://www.react-laag.com
MIT License
907 stars 47 forks source link

How to pass a clientRect in v2? #100

Open nathggns opened 10 months ago

nathggns commented 10 months ago

I'm migrating from useToggleLayer to useLayer as upgrading from v1 to v2, but I need to be able to pass a specific clientRect when opening. This API appears to have disappeared in the upgrade, but no alternative is mentioned in the migration guide

There's a closed issue at https://github.com/everweij/react-laag/issues/83 for this but no suggestion to fix, so I'm reopening.

rseyferth commented 7 months ago

If anyone is still interested, this hook should work alright. (I used useMousePositionAsTrigger as an example)

import useResizeObserver from '@react-hook/resize-observer'
import * as React from 'react'
import { IBounds } from 'react-laag'

export type UseRefAsTriggerResult = {
    ref: React.RefObject<HTMLElement>
    hasRefBounds: boolean
    trigger: {
        getBounds: () => IBounds
        getParent?: () => HTMLElement
    }
}

const EMPTY_BOUNDS: IBounds = {
    top: 0,
    left: 0,
    right: 1,
    bottom: 1,
    width: 1,
    height: 1,
}

export const useRefAsTrigger = (propRef?: React.RefObject<HTMLElement>): UseRefAsTriggerResult => {
    // Use the prop ref if it's provided, otherwise create a new one
    const _ref = React.useRef<HTMLElement>(null)
    const ref = propRef ?? _ref

    const [bounds, setBounds] = React.useState<IBounds>(EMPTY_BOUNDS)

    const setBoundsFromElement = React.useCallback((el: HTMLElement) => {
        const rect = el.getBoundingClientRect()
        setBounds({ bottom: rect.bottom, top: rect.top, left: rect.left, right: rect.right, height: rect.height, width: rect.width })
    }, [])

    // Set the bounds from the ref
    useResizeObserver(ref, e => {
        setBoundsFromElement(e.target as HTMLElement)
    })
    React.useEffect(() => {
        if (ref.current) setBoundsFromElement(ref.current)
    }, [ref])

    const hasRefBounds = bounds !== EMPTY_BOUNDS

    return { ref, hasRefBounds, trigger: { getBounds: () => bounds, getParent: ref.current ? () => ref.current! : undefined } }
}

Basic usage:


const { trigger, hasRefBounds } = useRefAsTrigger(buttonRef)
const { renderLayer, layerProps } = useLayer({
  isOpen: hasRefBounds,
  trigger,
})