-
Notifications
You must be signed in to change notification settings - Fork 51
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