Skip to content

Commit 1bb581c

Browse files
authored
fix: correctly adjust left scroll position in nested scroll container (#64)
* fix: correctly adjust left scroll position in nested scroll container * also check for overflow='scroll' * only measure node when it is visible * chore: improve auto alignment example * chore: only render node when it has been measured in * chore: better wording for regression
1 parent a939554 commit 1bb581c

File tree

4 files changed

+155
-64
lines changed

4 files changed

+155
-64
lines changed

demo/src/AutoAlignment.js

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,34 @@ function AutoAlignment() {
3434

3535
<h4>Horizontal flipping</h4>
3636

37-
<div style={{ height: 350, width: 2000 }}>
38-
<Stick
39-
autoFlipHorizontally
40-
position="middle left"
41-
style={{ display: 'inline-block', marginLeft: 250 }}
42-
node={
43-
<div style={{ backgroundColor: '#ae0d5c', height: 50, width: 200 }}>
44-
This is the content of the node
45-
</div>
46-
}
47-
>
48-
<div
49-
style={{
50-
width: 200,
51-
height: 100,
52-
backgroundColor: 'rgb(24, 170, 177)',
53-
}}
37+
<div
38+
style={{ overflowX: 'auto', border: '1px solid black', padding: 25 }}
39+
>
40+
<div style={{ height: 350, width: 5000 }}>
41+
<Stick
42+
autoFlipHorizontally
43+
position="middle left"
44+
style={{ display: 'inline-block', marginLeft: 250 }}
45+
node={
46+
<div
47+
style={{ backgroundColor: '#ae0d5c', height: 50, width: 200 }}
48+
>
49+
This is the content of the node
50+
</div>
51+
}
5452
>
55-
The node of this stick should move to the right if it can't fit to
56-
the left
57-
</div>
58-
</Stick>
53+
<div
54+
style={{
55+
width: 200,
56+
height: 100,
57+
backgroundColor: 'rgb(24, 170, 177)',
58+
}}
59+
>
60+
The node of this stick should move to the right if it can't fit to
61+
the left
62+
</div>
63+
</Stick>
64+
</div>
5965
</div>
6066
</div>
6167
)

demo/src/regressions/Regressions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react'
33
import ButtonOverlay from './ButtonOverlay'
44
import FitOnPage from './FitOnPage'
55
import SameWidth from './SameWidth'
6+
import ScrollPosition from './ScrollPosition'
67
import StickInSvg from './StickInSvg'
78
import StickNodeWidth from './StickNodeWidth'
89
import StickOnHover from './StickOnHover'
@@ -22,6 +23,7 @@ export default function Regressions() {
2223
<StyledWithDataAttributes />
2324
<TransportToFixedContainer />
2425
<StickOnHover />
26+
<ScrollPosition />
2527
</div>
2628
)
2729
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react'
2+
3+
import Stick from '../../../es'
4+
import Regression from './Regression'
5+
6+
function ScrollPosition() {
7+
return (
8+
<Regression
9+
fixed
10+
allBrowsers
11+
version="3.0.3"
12+
title="Node should scroll with anchor node"
13+
description="Scroll to the side. The node should move with the anchor node."
14+
>
15+
<div style={{ overflow: 'auto', height: 200, border: '1px solid black' }}>
16+
<div
17+
style={{
18+
width: 5000,
19+
height: 5000,
20+
marginLeft: 20,
21+
marginTop: 20,
22+
display: 'flex',
23+
alignItems: 'flex-start',
24+
}}
25+
>
26+
<Stick
27+
node={<Node>This node should always stick with its anchor</Node>}
28+
>
29+
<Anchor>
30+
Scroll to the right. The node should move with this element.
31+
</Anchor>
32+
</Stick>
33+
</div>
34+
</div>
35+
</Regression>
36+
)
37+
}
38+
39+
const Anchor = ({ children }) => (
40+
<div
41+
style={{
42+
padding: 10,
43+
backgroundColor: 'rgb(24, 170, 177)',
44+
color: 'white',
45+
}}
46+
>
47+
{children}
48+
</div>
49+
)
50+
51+
const Node = ({ children }) => (
52+
<div
53+
style={{
54+
backgroundColor: '#ae0d5c',
55+
color: 'white',
56+
padding: 10,
57+
}}
58+
>
59+
{children}
60+
</div>
61+
)
62+
63+
export default ScrollPosition

src/StickPortal.js

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import React, {
1212
useRef,
1313
useState,
1414
} from 'react'
15-
import { inline } from 'substyle'
1615
import { createPortal } from 'react-dom'
16+
import { inline } from 'substyle'
1717

18-
import type { PositionT, StickPortalPropsT } from './flowTypes'
18+
import type { StickPortalPropsT } from './flowTypes'
1919
import { useWatcher } from './hooks'
2020
import { scrollX, scrollY } from './utils'
2121

@@ -36,8 +36,8 @@ function StickPortal(
3636
ref
3737
) {
3838
const nodeRef = useRef()
39-
const [top, setTop] = useState(0)
40-
const [left, setLeft] = useState(0)
39+
const [top, setTop] = useState(null)
40+
const [left, setLeft] = useState(null)
4141
const [visible, setVisible] = useState(!!node)
4242

4343
const [host, hostParent] = useHost(transportTo)
@@ -63,19 +63,14 @@ function StickPortal(
6363
}, [host, hostParent, visible])
6464

6565
const measure = useCallback(() => {
66-
if (!nodeRef.current) {
66+
const node = nodeRef.current
67+
68+
if (!node || !visible) {
6769
return
6870
}
6971

70-
const boundingRect = nodeRef.current.getBoundingClientRect()
71-
72-
const newTop = calculateTop(position, boundingRect, getFixedParent(host))
73-
const newLeft = calculateLeft(
74-
nodeRef.current,
75-
position,
76-
boundingRect,
77-
getFixedParent(host)
78-
)
72+
const newTop = calculateTop(node, position, host)
73+
const newLeft = calculateLeft(node, position, host)
7974

8075
if (newTop !== top) {
8176
setTop(newTop)
@@ -84,7 +79,7 @@ function StickPortal(
8479
if (newLeft !== left) {
8580
setLeft(newLeft)
8681
}
87-
}, [host, left, position, top])
82+
}, [host, left, position, top, visible])
8883

8984
useWatcher(measure, { updateOnAnimationFrame })
9085

@@ -105,22 +100,24 @@ function StickPortal(
105100
>
106101
{children}
107102

108-
<PortalContext.Provider value={host.parentNode}>
109-
{createPortal(
110-
<div
111-
ref={containerRef}
112-
data-sticknestingkey={nestingKey}
113-
{...inline(style('node'), {
114-
position: 'absolute',
115-
top,
116-
left,
117-
})}
118-
>
119-
{node}
120-
</div>,
121-
host
122-
)}
123-
</PortalContext.Provider>
103+
{top != null && left != null && (
104+
<PortalContext.Provider value={host.parentNode}>
105+
{createPortal(
106+
<div
107+
ref={containerRef}
108+
data-sticknestingkey={nestingKey}
109+
{...inline(style('node'), {
110+
position: 'absolute',
111+
top,
112+
left,
113+
})}
114+
>
115+
{node}
116+
</div>,
117+
host
118+
)}
119+
</PortalContext.Provider>
120+
)}
124121
</Component>
125122
)
126123
}
@@ -141,11 +138,10 @@ function useHost(transportTo) {
141138
return [host, hostParent]
142139
}
143140

144-
function calculateTop(
145-
position: PositionT,
146-
{ top, height, bottom }: ClientRect,
147-
fixedHost: ?Element
148-
) {
141+
function calculateTop(node, position, host) {
142+
const { top, height, bottom } = node.getBoundingClientRect()
143+
const fixedHost = getFixedParent(host)
144+
149145
let result = 0
150146
if (position.indexOf('top') !== -1) {
151147
result = top
@@ -166,12 +162,12 @@ function calculateTop(
166162
return result + scrollY()
167163
}
168164

169-
function calculateLeft(
170-
nodeRef,
171-
position: PositionT,
172-
{ left, width, right }: ClientRect,
173-
fixedHost: ?Element
174-
) {
165+
function calculateLeft(node, position, host) {
166+
const { left, width, right } = node.getBoundingClientRect()
167+
168+
const fixedHost = getFixedParent(host)
169+
const scrollHost = getScrollParent(node)
170+
175171
let result = 0
176172
if (position.indexOf('left') !== -1) {
177173
result = left
@@ -189,7 +185,31 @@ function calculateLeft(
189185
return result - hostLeft
190186
}
191187

192-
return result + scrollX(nodeRef)
188+
if (scrollHost) {
189+
return result + scrollX(node) - scrollHost.scrollLeft
190+
}
191+
192+
return result + scrollX(node)
193+
}
194+
195+
function getScrollParent(element) {
196+
if (!element) {
197+
return null
198+
}
199+
200+
if (element.nodeName === 'BODY' || element.nodeName === 'HTML') {
201+
return null
202+
}
203+
204+
const style = getComputedStyle(element)
205+
206+
if (style.overflowX === 'auto' || style.overflowX === 'scroll') {
207+
return element
208+
}
209+
210+
return element.parentNode instanceof Element
211+
? getScrollParent(element.parentNode)
212+
: null
193213
}
194214

195215
function getFixedParent(element: Element): ?Element {

0 commit comments

Comments
 (0)