Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit b878949

Browse files
authored
Merge pull request #141 from ckeditor/t/139a
Fix: getOptimalPosition() utility does not work when the parent element has a scroll. Closes #139.
2 parents d4f016d + 64ba251 commit b878949

File tree

5 files changed

+249
-65
lines changed

5 files changed

+249
-65
lines changed

src/dom/position.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,32 @@ export function getOptimalPosition( { element, target, positions, limiter, fitIn
9999

100100
let { left, top } = getAbsoluteRectCoordinates( bestPosition );
101101

102-
// (#126) If there's some positioned ancestor of the panel, then its rect must be taken into
103-
// consideration. `Rect` is always relative to the viewport while `position: absolute` works
104-
// with respect to that positioned ancestor.
105102
if ( positionedElementAncestor ) {
106103
const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) );
104+
const ancestorComputedStyles = global.window.getComputedStyle( positionedElementAncestor );
107105

106+
// (https://github.com/ckeditor/ckeditor5-ui-default/issues/126)
107+
// If there's some positioned ancestor of the panel, then its `Rect` must be taken into
108+
// consideration. `Rect` is always relative to the viewport while `position: absolute` works
109+
// with respect to that positioned ancestor.
108110
left -= ancestorPosition.left;
109111
top -= ancestorPosition.top;
112+
113+
// (https://github.com/ckeditor/ckeditor5-utils/issues/139)
114+
// If there's some positioned ancestor of the panel, not only its position must be taken into
115+
// consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect`
116+
// is relative to the viewport (it doesn't care about scrolling), while `position: absolute`
117+
// must compensate that scrolling.
118+
left += positionedElementAncestor.scrollLeft;
119+
top += positionedElementAncestor.scrollTop;
120+
121+
// (https://github.com/ckeditor/ckeditor5-utils/issues/139)
122+
// If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth`
123+
// while `position: absolute` positioning does not consider it.
124+
// E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element,
125+
// not upper-left corner of its border.
126+
left -= parseInt( ancestorComputedStyles.borderLeftWidth, 10 );
127+
top -= parseInt( ancestorComputedStyles.borderTopWidth, 10 );
110128
}
111129

112130
return { left, top, name };

tests/dom/position.js

Lines changed: 103 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,19 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
/* global document, window */
7-
86
import global from '../../src/dom/global';
97
import { getOptimalPosition } from '../../src/dom/position';
10-
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
11-
12-
testUtils.createSinonSandbox();
138

14-
let element, target, limiter, windowStub;
9+
let element, target, limiter;
1510

16-
describe( 'getOptimalPosition', () => {
11+
describe( 'getOptimalPosition()', () => {
1712
beforeEach( () => {
18-
windowStub = {
13+
stubWindow( {
1914
innerWidth: 10000,
2015
innerHeight: 10000,
2116
scrollX: 0,
2217
scrollY: 0
23-
};
24-
25-
testUtils.sinon.stub( global, 'window', windowStub );
18+
} );
2619
} );
2720

2821
describe( 'for single position', () => {
@@ -37,7 +30,7 @@ describe( 'getOptimalPosition', () => {
3730
} );
3831

3932
it( 'should return coordinates (window scroll)', () => {
40-
Object.assign( windowStub, {
33+
stubWindow( {
4134
innerWidth: 10000,
4235
innerHeight: 10000,
4336
scrollX: 100,
@@ -51,41 +44,67 @@ describe( 'getOptimalPosition', () => {
5144
} );
5245
} );
5346

54-
it( 'should return coordinates (positioned element parent)', () => {
55-
const positionedParent = document.createElement( 'div' );
56-
57-
Object.assign( windowStub, {
58-
innerWidth: 10000,
59-
innerHeight: 10000,
60-
scrollX: 1000,
61-
scrollY: 1000,
62-
getComputedStyle: ( el ) => {
63-
return window.getComputedStyle( el );
64-
}
65-
} );
66-
67-
Object.assign( positionedParent.style, {
68-
position: 'absolute',
69-
top: '1000px',
70-
left: '1000px'
47+
describe( 'positioned element parent', () => {
48+
let parent;
49+
50+
it( 'should return coordinates', () => {
51+
stubWindow( {
52+
innerWidth: 10000,
53+
innerHeight: 10000,
54+
scrollX: 1000,
55+
scrollY: 1000
56+
} );
57+
58+
parent = getElement( {
59+
top: 1000,
60+
right: 1010,
61+
bottom: 1010,
62+
left: 1000,
63+
width: 10,
64+
height: 10
65+
}, {
66+
position: 'absolute'
67+
} );
68+
69+
element.parentElement = parent;
70+
71+
assertPosition( { element, target, positions: [ attachLeft ] }, {
72+
top: -900,
73+
left: -920,
74+
name: 'left'
75+
} );
7176
} );
7277

73-
document.body.appendChild( positionedParent );
74-
positionedParent.appendChild( element );
75-
76-
stubElementRect( positionedParent, {
77-
top: 1000,
78-
right: 1010,
79-
bottom: 1010,
80-
left: 1000,
81-
width: 10,
82-
height: 10
83-
} );
84-
85-
assertPosition( { element, target, positions: [ attachLeft ] }, {
86-
top: -900,
87-
left: -920,
88-
name: 'left'
78+
it( 'should return coordinates (scroll and border)', () => {
79+
stubWindow( {
80+
innerWidth: 10000,
81+
innerHeight: 10000,
82+
scrollX: 1000,
83+
scrollY: 1000
84+
} );
85+
86+
parent = getElement( {
87+
top: 0,
88+
right: 10,
89+
bottom: 10,
90+
left: 0,
91+
width: 10,
92+
height: 10,
93+
scrollTop: 100,
94+
scrollLeft: 200
95+
}, {
96+
position: 'absolute',
97+
borderLeftWidth: '20px',
98+
borderTopWidth: '40px',
99+
} );
100+
101+
element.parentElement = parent;
102+
103+
assertPosition( { element, target, positions: [ attachLeft ] }, {
104+
top: 160,
105+
left: 260,
106+
name: 'left'
107+
} );
89108
} );
90109
} );
91110
} );
@@ -252,7 +271,7 @@ describe( 'getOptimalPosition', () => {
252271
} );
253272

254273
it( 'should return the very first coordinates if limiter does not fit into the viewport', () => {
255-
stubElementRect( limiter, {
274+
limiter = getElement( {
256275
top: -100,
257276
right: -80,
258277
bottom: -80,
@@ -320,12 +339,41 @@ const attachTop = ( targetRect, elementRect ) => ( {
320339
name: 'bottom'
321340
} );
322341

323-
function stubElementRect( element, rect ) {
324-
if ( element.getBoundingClientRect.restore ) {
325-
element.getBoundingClientRect.restore();
342+
// Returns a synthetic element.
343+
//
344+
// @private
345+
// @param {Object} properties A set of properties for the element.
346+
// @param {Object} styles A set of styles in `window.getComputedStyle()` format.
347+
function getElement( properties = {}, styles = {} ) {
348+
const element = {
349+
tagName: 'div',
350+
scrollLeft: 0,
351+
scrollTop: 0
352+
};
353+
354+
Object.assign( element, properties );
355+
356+
if ( !styles.borderLeftWidth ) {
357+
styles.borderLeftWidth = '0px';
358+
}
359+
360+
if ( !styles.borderTopWidth ) {
361+
styles.borderTopWidth = '0px';
326362
}
327363

328-
testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( rect );
364+
global.window.getComputedStyle.withArgs( element ).returns( styles );
365+
366+
return element;
367+
}
368+
369+
// Stubs the window.
370+
//
371+
// @private
372+
// @param {Object} properties A set of properties the window should have.
373+
function stubWindow( properties ) {
374+
global.window = Object.assign( {
375+
getComputedStyle: sinon.stub()
376+
}, properties );
329377
}
330378

331379
// <-- 100px ->
@@ -343,10 +391,7 @@ function stubElementRect( element, rect ) {
343391
// |
344392
//
345393
function setElementTargetPlayground() {
346-
element = document.createElement( 'div' );
347-
target = document.createElement( 'div' );
348-
349-
stubElementRect( element, {
394+
element = getElement( {
350395
top: 0,
351396
right: 20,
352397
bottom: 20,
@@ -355,7 +400,7 @@ function setElementTargetPlayground() {
355400
height: 20
356401
} );
357402

358-
stubElementRect( target, {
403+
target = getElement( {
359404
top: 100,
360405
right: 110,
361406
bottom: 110,
@@ -387,11 +432,7 @@ function setElementTargetPlayground() {
387432
//
388433
//
389434
function setElementTargetLimiterPlayground() {
390-
element = document.createElement( 'div' );
391-
target = document.createElement( 'div' );
392-
limiter = document.createElement( 'div' );
393-
394-
stubElementRect( element, {
435+
element = getElement( {
395436
top: 0,
396437
right: 20,
397438
bottom: 20,
@@ -400,7 +441,7 @@ function setElementTargetLimiterPlayground() {
400441
height: 20
401442
} );
402443

403-
stubElementRect( limiter, {
444+
limiter = getElement( {
404445
top: 100,
405446
right: 10,
406447
bottom: 120,
@@ -409,7 +450,7 @@ function setElementTargetLimiterPlayground() {
409450
height: 20
410451
} );
411452

412-
stubElementRect( target, {
453+
target = getElement( {
413454
top: 100,
414455
right: 10,
415456
bottom: 110,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<div class="test-box" style="height: 200px; width: 200px">
2+
<div class="test-box" style="height: 400px; width: 400px">
3+
<div class="test-box" style="height: 800px; width: 800px">
4+
<div class="target"></div>
5+
<div class="source source-se"></div>
6+
</div>
7+
<div class="source source-sw"></div>
8+
</div>
9+
<div class="source source-ne"></div>
10+
</div>
11+
<div class="source source-nw"></div>
12+
13+
<style>
14+
body {
15+
padding-top: 2000px;
16+
}
17+
18+
.test-box {
19+
padding: 15px;
20+
border: 25px solid #000;
21+
overflow: scroll;
22+
position: relative;
23+
}
24+
25+
.source {
26+
width: 40px;
27+
height: 40px;
28+
background: red;
29+
position: absolute;
30+
}
31+
32+
.source-nw {
33+
background: cyan;
34+
}
35+
36+
.source-ne {
37+
background: magenta;
38+
}
39+
40+
.source-sw {
41+
background: yellow;
42+
}
43+
44+
.source-se {
45+
background: black;
46+
}
47+
48+
.target {
49+
width: 80px;
50+
height: 80px;
51+
background: blue;
52+
position: absolute;
53+
bottom: 0;
54+
right: 0;
55+
}
56+
</style>

0 commit comments

Comments
 (0)