bvaughn / react-resizable-panels

https://react-resizable-panels.vercel.app/
MIT License
3.87k stars 135 forks source link

onCollapse doesn't fire when browser window is not full screen #265

Closed viveleroi closed 8 months ago

viveleroi commented 8 months ago

v1.0.8, Chrome 120 on Windows 11

Someone recently noticed that when a browser is not fullscreen, collapsing the panel works never fires the onCollapse event.

This doesn't appear to be size-related, even having the window 90% of the screen doesn't change behavior, it has to be fully maximized.

We translate pixel sizes into percents using the window innerWidth, so our collapse size logic looks like this:

const navCollapseSize = (36 / width) * 100

Here is an example of a non-maximized test:

No matter how the panel is collapsed - via ref.current.collapse() or by resizing it too small, the onCollapse never fires.

Here is an example of a maximized browser test:

export const Drawer = ({
  children,
  className,
  collapseButtonProps,
  resizable = true,
  side = 'left',
  title
}: DrawerProps): JSX.Element => {
  const ref = useRef<ImperativePanelHandle>(null)
  const [isCollapsed, setIsCollapsed] = useState(false)

  // Work around the initial width value always being 0
  // https://github.com/juliencrn/usehooks-ts/issues/212
  let { width } = useWindowSize()
  width = width === 0 ? window.innerWidth : width

  // The resizer uses percentages so we need to convert pixel sizes
  const navMinSize = (300 / width) * 100
  const navCollapseSize = (36 / width) * 100

  console.log(width, navMinSize, navCollapseSize)

  const toggleCollapse = useCallback((): void => {
    const panel = ref.current
    if (panel) {
      if (panel.isCollapsed()) {
        panel.expand()
      } else {
        panel.collapse()
      }
    }
  }, [ref])

  const content = (
    <div
      className={clsx(className, styles[side], styles.drawer, {
        [styles.collapsed]: isCollapsed
      })}>
      <header className={styles.header}>
        {title}
        {resizable && (
          <Button Icon={IconToggle} onClick={toggleCollapse} uiVariant='secondary' {...collapseButtonProps} />
        )}
      </header>
      <ErrorBoundary fallbackRender={FallbackOverlay}>
        <div className={styles.drawerContent}>{children}</div>
      </ErrorBoundary>
    </div>
  )

  if (resizable) {
    return (
      <>
        {side === 'right' && <ResizeHandle />}
        <Panel
          className='flex'
          collapsedSize={navCollapseSize}
          collapsible
          defaultSize={navMinSize}
          id={`${side}Drawer`}
          maxSize={60}
          minSize={navMinSize}
          order={side === 'left' ? 1 : 3}
          ref={ref}
          onCollapse={() => {
            console.log('onCollapse')
            setIsCollapsed(true)
          }}
          onExpand={() => {
            console.log('onExpand')
            setIsCollapsed(false)
          }}>
          {content}
        </Panel>
        {side === 'left' && <ResizeHandle />}
      </>
    )
  } else {
    return content
  }
}

This was discovered while testing our update to v1.0.8 and relates to #264

bvaughn commented 8 months ago

Please share a fully runnable code example (Code Sandbox is preferable, or Replay) so I can look at the bug you're reporting without spending time trying to fill in the missing gaps.

viveleroi commented 8 months ago

This a precision problem and is really on the user to fix. I would suggest notating this in any examples about using pixels.

In my example, the percent calculation is not being rounded, and these cause your internal number comparison conditions to fail.

Specifically, this logic:

if (onCollapse && (lastNotifiedSize == null || lastNotifiedSize !== collapsedSize) && size === collapsedSize) {
  onCollapse();
}

In one of my tests, the values worked out like this:

These caused the condition to be false because size === collapsedSize.

Wrapping our percent calculations in lodash's round method to precision=2 solves this issue.

I'll leave this open in case you want to use it for documentation work, but feel free to close otherwise.

bvaughn commented 8 months ago

Ah! Thanks for that added context. I've tried to use a fuzzyNumbersEqual helper method I wrote to smooth over precision issues, but it seems I missed some places where I should have been using it.

To be fair, I didn't expect precision issues to arise from user-provided values (like collapsedSize) but I can see where pixel-to-percentage conversions could lead to that.

bvaughn commented 8 months ago

I don't know if I want to try to support this. It would require a lot of changes. Anywhere I compare constraints would have to be changed to use the fuzzy compare. It seems easy to regress on (miss a spot) and cumbersome to enforce via test coverage.

I think I'd consider this an unsupported case, strictly required for pixel constraints (which aren't supported) and suggest that you reduce the precision on the panel constraint props you are specifying ahead of time (e.g. don't set collapsedSize={1.89873417721519}, set collapsedSize={1.89}).