-
Notifications
You must be signed in to change notification settings - Fork 53
Description
Summary
Overlay 컴포넌트가 TextField 같이 포커스 가능한 컴포넌트와 같이 사용될 때 레이아웃이 시프트 되는 버그가 있습니다.
Reproduction process
재현 방법
container로 지정한 엘리먼트의 경계에 가까이 오버레이를 위치시킵니다.- 오버레이안에 너비가 넓은
TextField컴포넌트를 사용하고autoFocus속성을true로 줍니다. - 오버레이를 열때마다 간헐적으로 레이아웃이 시프트됩니다.
2025_03_28_05_10_PM_1743149420.mp4
원인
오버레이를 띄울 때 2단계에 걸쳐서 보여주면서 transform 속성이 짧은 순간에 바뀌는 것이 원인입니다.
bezier-react/packages/bezier-react/src/components/Overlay/Overlay.tsx
Lines 194 to 263 in 32c5e51
| /** | |
| * Case 1: show === true | |
| * show -> shouldRender -> shouldShow | |
| * shouldRender 를 true 로 설정하고, 직후에 shouldShow 를 true 로 설정하여 transition 유발 | |
| * | |
| * Case 2: show === false | |
| * show -> shouldShow -> (...) -> shouldRender | |
| * shouldShow 를 false 로 설정하고, shouldRender 는 transition 필요 여부에 따라 다르게 결정함 | |
| * Case 2-1: withTransition === true | |
| * shouldShow -> onTransitionEnd -> shouldRender | |
| * onTransitionEnd handler 를 이용해 transition 이 끝난 다음 shouldRender 를 false 로 설정 | |
| * Case 2-2: withTransition === false | |
| * shouldShow && shouldRender | |
| * transition 을 기다릴 필요가 없으므로 바로 shouldRender 를 false 로 설정 | |
| */ | |
| useEffect(() => { | |
| if (show) { | |
| if (shouldRender) { | |
| window.requestAnimationFrame(() => setShouldShow(true)) | |
| } else { | |
| window.requestAnimationFrame(() => setShouldRender(true)) | |
| } | |
| } | |
| if (!show) { | |
| window.requestAnimationFrame(() => setShouldShow(false)) | |
| if (!withTransition) { | |
| window.requestAnimationFrame(() => setShouldRender(false)) | |
| } | |
| } | |
| }, [show, withTransition, shouldRender, shouldShow, window]) | |
| const themeName = useThemeName() | |
| if (!shouldRender) { | |
| return null | |
| } | |
| const Content = ( | |
| <ThemeProvider themeName={themeName}> | |
| <div | |
| className={classNames( | |
| styles.Overlay, | |
| !shouldShow && styles.hidden, | |
| withTransition && styles.transition, | |
| className | |
| )} | |
| style={{ | |
| ...style, | |
| ...getOverlayStyle({ | |
| containerRect: containerRect.current, | |
| targetRect: targetRect.current, | |
| overlay: overlayRef.current, | |
| position, | |
| marginX, | |
| marginY, | |
| keepInContainer, | |
| show: shouldShow, | |
| }), | |
| }} | |
| ref={mergedRef} | |
| data-testid={OVERLAY_TEST_ID} | |
| onTransitionEnd={handleTransitionEnd} | |
| {...rest} | |
| > | |
| {children} | |
| </div> | |
| </ThemeProvider> | |
| ) |
-
shouldRender = true, shouldShow = false
getOverlayStyle 가 호출될 때 overlayRef 가 아직 null 인 상태이기 때문에 getOverlayStyle 의 결과로 transform: translateX(0px) translateY(0px) 이 나오게 되고, opacity: 0 인 상태로 target 바로 위에 오버레이가 뜹니다. 사용자에는 아직 보이지 않지만 DOM 에 마운트는 되어 있는 상태입니다. -
shouldRender = true, shouldShow = true
overlayRef 가 엘리먼트를 참조한 상태이기 때문에 getOverlayStyle이 오버레이의 position 속성에 따라 적절한 translate 값을 반환하게 되고, 그 결과 오버레이 위치가 바뀌면서 사용자에게 보이게 됩니다.
이렇게 2단계에 걸치는 렌더링은 대부분의 경우에는 문제가 안되지만, TextField 컴포넌트처럼 autoFocus 할 수 있는 요소와 같이 쓰면 첫 번째 단계에서 포커스 되면서 스크롤이 움직이게 됩니다.
제안
장기적으로는 floating-ui 같은 외부 라이브러리를 사용해서 리팩토링 하면 좋겠으나, 지금 당장에 간단하게 고치려면 첫 번째 단계 렌더링때 inert 속성을 활용해서 포커싱을 못하게 하면 될 것 같습니다. 사파리 호환성이 15.4 이상이어야 하는 점이 걸리는데, browserlist 에서 15.4 이상을 명시하고 있기 때문에 상관 없을 것 같기도 하네요. 혹시 어떻게 생각하시나요? @sungik-choi
Version of bezier-react
3.1.0
Browser
No response
Operating system
- macOS
- Windows
- Linux
Additonal Information
No response