Skip to content

Commit

Permalink
Merge pull request #14210 from ckeditor/ck/14107-rect-getvisible-and-…
Browse files Browse the repository at this point in the history
…position-absolute

Fix (utils): `Rect#getVisible()` should not consider ancestors when the target is an element with `position: absolute`. Closes #14107. Closes cksource/ckeditor5-internal#3264.
  • Loading branch information
Dumluregn committed May 30, 2023
2 parents bb7cd9c + 4799a09 commit 8b1ede7
Show file tree
Hide file tree
Showing 5 changed files with 469 additions and 17 deletions.
77 changes: 60 additions & 17 deletions packages/ckeditor5-utils/src/dom/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,33 +236,62 @@ export default class Rect {
* If there's no such visible rect, which is when the rect is limited by one or many of
* the ancestors, `null` is returned.
*
* **Note**: This method does not consider the boundaries of the viewport (window).
* To get a rect cropped by all ancestors and the viewport, use an intersection such as:
*
* ```ts
* const visibleInViewportRect = new Rect( window ).getIntersection( new Rect( source ).getVisible() );
* ```
*
* @returns A visible rect instance or `null`, if there's none.
*/
public getVisible(): Rect | null {
const source: RectSource & { parentNode?: Node | null; commonAncestorContainer?: Node | null } = this._source;

let visibleRect = this.clone();

// There's no ancestor to crop <body> with the overflow.
if ( !isBody( source ) ) {
let parent = source.parentNode || source.commonAncestorContainer;

// Check the ancestors all the way up to the <body>.
while ( parent && !isBody( parent ) ) {
const parentRect = new Rect( parent as HTMLElement );
const intersectionRect = visibleRect.getIntersection( parentRect );

if ( intersectionRect ) {
if ( intersectionRect.getArea() < visibleRect.getArea() ) {
// Reduce the visible rect to the intersection.
visibleRect = intersectionRect;
}
} else {
// There's no intersection, the rect is completely invisible.
return null;
}
if ( isBody( source ) ) {
return visibleRect;
}

let child: any = source;
let parent = source.parentNode || source.commonAncestorContainer;
let absolutelyPositionedChildElement;

// Check the ancestors all the way up to the <body>.
while ( parent && !isBody( parent ) ) {
if ( child instanceof HTMLElement && getElementPosition( child ) === 'absolute' ) {
absolutelyPositionedChildElement = child;
}

// The child will be cropped only if it has `position: absolute` and the parent has `position: relative` + some overflow.
// Otherwise there's no chance of visual clipping and the parent can be skipped
// https://github.com/ckeditor/ckeditor5/issues/14107.
if (
absolutelyPositionedChildElement &&
( getElementPosition( parent as HTMLElement ) !== 'relative' || getElementOverflow( parent as HTMLElement ) === 'visible' )
) {
child = parent;
parent = parent.parentNode;
continue;
}

const parentRect = new Rect( parent as HTMLElement );
const intersectionRect = visibleRect.getIntersection( parentRect );

if ( intersectionRect ) {
if ( intersectionRect.getArea() < visibleRect.getArea() ) {
// Reduce the visible rect to the intersection.
visibleRect = intersectionRect;
}
} else {
// There's no intersection, the rect is completely invisible.
return null;
}

child = parent;
parent = parent.parentNode;
}

return visibleRect;
Expand Down Expand Up @@ -462,3 +491,17 @@ function isDomElement( value: any ): value is Element {
// it makes complicated checks to make sure that given value is a DOM element.
return value !== null && typeof value === 'object' && value.nodeType === 1 && typeof value.getBoundingClientRect === 'function';
}

/**
* Returns the value of the `position` style of an `HTMLElement`.
*/
function getElementPosition( element: HTMLElement ): string {
return element.ownerDocument.defaultView!.getComputedStyle( element ).position;
}

/**
* Returns the value of the `overflow` style of an `HTMLElement`.
*/
function getElementOverflow( element: HTMLElement ): string {
return element.ownerDocument.defaultView!.getComputedStyle( element ).overflow;
}
188 changes: 188 additions & 0 deletions packages/ckeditor5-utils/tests/dom/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,53 @@ describe( 'Rect', () => {
} );
} );

it( 'should return the visible rect (HTMLElement), partially cropped, ' +
'deep ancestor overflow (ancestor with position: absolute)', () => {
ancestorB.appendChild( ancestorA );
document.body.appendChild( ancestorB );

element.style.position = 'static';
ancestorA.style.position = 'absolute';
ancestorB.style.overflow = 'scroll';
ancestorB.style.position = 'relative';

sinon.stub( element, 'getBoundingClientRect' ).returns( {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );

sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
top: 50,
right: 100,
bottom: 100,
left: 0,
width: 50,
height: 50
} );

sinon.stub( ancestorB, 'getBoundingClientRect' ).returns( {
top: 0,
right: 150,
bottom: 100,
left: 50,
width: 100,
height: 100
} );

assertRect( new Rect( element ).getVisible(), {
top: 50,
right: 100,
bottom: 100,
left: 50,
width: 50,
height: 50
} );
} );

it( 'should return the visible rect (Range), partially cropped', () => {
range.setStart( ancestorA, 0 );
range.setEnd( ancestorA, 1 );
Expand Down Expand Up @@ -684,6 +731,147 @@ describe( 'Rect', () => {

expect( new Rect( element ).getVisible() ).to.equal( null );
} );

it( 'should ignore a parent if target is an element with position: absolute', () => {
sinon.stub( element, 'getBoundingClientRect' ).returns( {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );

element.style.position = 'absolute';

sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
top: 50,
right: 150,
bottom: 150,
left: 50,
width: 100,
height: 100
} );

assertRect( new Rect( element ).getVisible(), {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );
} );

it( 'should ignore all parents if target is an element with position: absolute', () => {
ancestorB.appendChild( ancestorA );
document.body.appendChild( ancestorB );

sinon.stub( element, 'getBoundingClientRect' ).returns( {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );

element.style.position = 'absolute';

sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
top: 50,
right: 150,
bottom: 150,
left: 50,
width: 100,
height: 100
} );

sinon.stub( ancestorB, 'getBoundingClientRect' ).returns( {
top: 200,
right: 300,
bottom: 300,
left: 200,
width: 100,
height: 100
} );

assertRect( new Rect( element ).getVisible(), {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );
} );

it( 'should ignore a parent if target is an element with position: absolute ' +
'but parent has position: relative but no overflow', () => {
sinon.stub( element, 'getBoundingClientRect' ).returns( {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );

element.style.position = 'absolute';
ancestorA.style.position = 'relative';

sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
top: 50,
right: 150,
bottom: 150,
left: 50,
width: 100,
height: 100
} );

assertRect( new Rect( element ).getVisible(), {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );
} );

it( 'should not ignore a parent if target is an element with position: absolute ' +
'but parent has position: relative and overflow', () => {
sinon.stub( element, 'getBoundingClientRect' ).returns( {
top: 0,
right: 100,
bottom: 100,
left: 0,
width: 100,
height: 100
} );

element.style.position = 'absolute';
ancestorA.style.position = 'relative';
ancestorA.style.overflow = 'hidden';

sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
top: 50,
right: 150,
bottom: 150,
left: 50,
width: 100,
height: 100
} );

assertRect( new Rect( element ).getVisible(), {
top: 50,
right: 100,
bottom: 100,
left: 50,
width: 50,
height: 50
} );
} );
} );

describe( 'isEqual()', () => {
Expand Down

0 comments on commit 8b1ede7

Please sign in to comment.