Skip to content

Commit 7893d1a

Browse files
authored
Merge pull request #54 from metalabdesign/container-detection
Improve parent container detection
2 parents e83eba5 + 5cb7a51 commit 7893d1a

File tree

5 files changed

+118
-43
lines changed

5 files changed

+118
-43
lines changed

packages/flowtip-react-dom/src/FlowTip.js

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import flowtip, {
1414
import type {RectLike, Region, Align, Dimensions, Result} from 'flowtip-core';
1515

1616
import getContainingBlock from './util/getContainingBlock';
17+
import getClippingBlock from './util/getClippingBlock';
18+
import getContentRect from './util/getContentRect';
1719
import findDOMNode from './util/findDOMNode';
1820

1921
// Static `flowtip` layout calculation result mock for use during initial client
@@ -30,8 +32,8 @@ const STATIC_RESULT: Result = {
3032
};
3133

3234
export type State = {
33-
containingBlock: RectLike,
34-
bounds: RectLike | null,
35+
containingBlock: Rect,
36+
bounds: Rect | null,
3537
content: Dimensions | null,
3638
tail: Dimensions | null,
3739
result: Result | null,
@@ -140,11 +142,12 @@ class FlowTip extends React.Component<Props, State> {
140142

141143
_nextContent: Dimensions | null = null;
142144
_nextTail: Dimensions | null = null;
143-
_nextContainingBlock: RectLike = Rect.zero;
144-
_nextBounds: RectLike | null = Rect.zero;
145+
_nextContainingBlock: Rect = Rect.zero;
146+
_nextBounds: Rect | null = null;
145147
_lastRegion: Region | void;
146148
_isMounted: boolean = false;
147149
_containingBlockNode: HTMLElement | null = null;
150+
_clippingBlockNode: HTMLElement | null = null;
148151
_node: HTMLElement | null = null;
149152
state = this._getState(this.props);
150153

@@ -153,7 +156,7 @@ class FlowTip extends React.Component<Props, State> {
153156
_handleScroll = this._handleScroll.bind(this);
154157

155158
// ===========================================================================
156-
// Lifecycle Methods.
159+
// Lifecycle Methods
157160
// ===========================================================================
158161
componentDidMount(): void {
159162
this._isMounted = true;
@@ -169,6 +172,7 @@ class FlowTip extends React.Component<Props, State> {
169172
}
170173

171174
componentWillReceiveProps(nextProps: Props): void {
175+
this._nextContainingBlock = this._getContainingBlockRect();
172176
this._nextBounds = this._getBoundsRect(nextProps);
173177

174178
this._updateState(nextProps);
@@ -182,14 +186,15 @@ class FlowTip extends React.Component<Props, State> {
182186
this._isMounted = false;
183187

184188
this._containingBlockNode = null;
189+
this._clippingBlockNode = null;
185190
this._node = null;
186191

187192
window.removeEventListener('scroll', this._handleScroll);
188193
window.removeEventListener('resize', this._handleScroll);
189194
}
190195

191196
// ===========================================================================
192-
// State Management.
197+
// State Management
193198
// ===========================================================================
194199

195200
_getLastRegion(nextProps: Props): Region | void {
@@ -402,59 +407,89 @@ class FlowTip extends React.Component<Props, State> {
402407
}
403408

404409
// ===========================================================================
405-
// DOM Measurement Methods.
410+
// DOM Measurement Methods
406411
// ===========================================================================
407412

408-
_getBoundsRect(nextProps: Props): RectLike | null {
409-
const viewport = new Rect(
410-
0,
411-
0,
412-
window.document.documentElement.clientWidth,
413-
window.document.documentElement.clientHeight,
414-
);
413+
_getBoundsRect(nextProps: Props): Rect | null {
414+
const viewportRect = new Rect(0, 0, window.innerWidth, window.innerHeight);
415415

416-
const bounds = Rect.grow(
417-
nextProps.bounds ? Rect.intersect(viewport, nextProps.bounds) : viewport,
418-
-nextProps.edgeOffset,
419-
);
416+
const processBounds = (boundsRect: RectLike) => {
417+
const visibleBounds = Rect.grow(
418+
Rect.intersect(viewportRect, boundsRect),
419+
-nextProps.edgeOffset,
420+
);
421+
422+
// A rect with negative dimensions doesn't make sense here.
423+
// Returning null will disable rendering content.
424+
if (visibleBounds.width < 0 || visibleBounds.height < 0) {
425+
return Rect.zero;
426+
}
427+
428+
return visibleBounds;
429+
};
430+
431+
if (nextProps.bounds) {
432+
return processBounds(nextProps.bounds);
433+
}
434+
435+
if (document.body && this._clippingBlockNode === document.documentElement) {
436+
return processBounds(
437+
new Rect(
438+
-document.body.scrollLeft,
439+
-document.body.scrollTop,
440+
document.body.scrollWidth,
441+
document.body.scrollHeight,
442+
),
443+
);
444+
}
420445

421-
// A rect with neagitve dimensions doesn't make sense here.
422-
// Returning null disable rendering of any content.
423-
if (bounds.width >= 0 && bounds.height >= 0) {
424-
return bounds;
446+
if (this._clippingBlockNode) {
447+
return processBounds(getContentRect(this._clippingBlockNode));
425448
}
426449

427450
return null;
428451
}
429452

430-
_getContainingBlockRect(): RectLike {
431-
if (!this._containingBlockNode) return Rect.zero;
432-
return Rect.from(this._containingBlockNode.getBoundingClientRect());
453+
_getContainingBlockRect(): Rect {
454+
if (!this._containingBlockNode) {
455+
return Rect.zero;
456+
}
457+
458+
if (
459+
document.body &&
460+
this._containingBlockNode === document.documentElement
461+
) {
462+
return new Rect(
463+
-document.body.scrollLeft,
464+
-document.body.scrollTop,
465+
document.body.scrollWidth,
466+
document.body.scrollHeight,
467+
);
468+
}
469+
470+
return getContentRect(this._containingBlockNode);
433471
}
434472

435473
// ===========================================================================
436-
// DOM Element Accessors.
474+
// DOM Element Accessors
437475
// ===========================================================================
438476

439477
_updateDOMNodes(): void {
440478
const node = findDOMNode(this);
441-
this._node = node instanceof HTMLElement ? node : null;
442-
443-
const block = this._node && getContainingBlock(this._node.parentNode);
444-
if (block) {
445-
this._containingBlockNode = block;
446-
} else {
447-
// Refine nullable `document.body`.
448-
// see: https://stackoverflow.com/questions/42377663
449-
if (document.body === null) {
450-
throw new Error('document.body is null');
451-
}
452-
this._containingBlockNode = document.body;
479+
480+
if (node instanceof HTMLElement) {
481+
this._node = node;
482+
483+
this._containingBlockNode =
484+
getContainingBlock(node.parentNode) || document.documentElement;
485+
486+
this._clippingBlockNode =
487+
getClippingBlock(node.parentNode) || document.documentElement;
453488
}
454489
}
455490

456491
// ===========================================================================
457-
// Event Handlers.
492+
// Event Handlers
458493
// ===========================================================================
459494

460495
/**
@@ -501,7 +536,7 @@ class FlowTip extends React.Component<Props, State> {
501536
}
502537

503538
// ===========================================================================
504-
// Render Methods.
539+
// Render Methods
505540
// ===========================================================================
506541

507542
/**

packages/flowtip-react-dom/src/util/findAncestor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22

33
const findAncestor = (
4-
callback: (node: Node) => boolean,
4+
callback: (node: HTMLElement) => boolean,
55
node: ?Node,
66
): HTMLElement | null => {
77
let current = node;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @flow
2+
3+
import findAncestor from './findAncestor';
4+
5+
const getClippingBlock = (node: ?Node): HTMLElement | null => {
6+
const result = findAncestor((node) => {
7+
if (node === document.documentElement) return true;
8+
9+
const style = getComputedStyle(node);
10+
11+
return style.overflow && style.overflow !== 'visible';
12+
}, node);
13+
14+
return result;
15+
};
16+
17+
export default getClippingBlock;

packages/flowtip-react-dom/src/util/getContainingBlock.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// @flow
2+
23
import findAncestor from './findAncestor';
34

45
const getContainingBlock = (node: ?Node): HTMLElement | null => {
56
const result = findAncestor((node) => {
6-
if (node.tagName === 'BODY') return true;
7+
if (node === document.documentElement) return true;
78

8-
const style = window.getComputedStyle(node);
9+
const style = getComputedStyle(node);
910

1011
return style.position && style.position !== 'static';
1112
}, node);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// @flow
2+
3+
import {Rect} from 'flowtip-core';
4+
5+
const getContentRect = (node: HTMLElement): Rect => {
6+
const rect = node.getBoundingClientRect();
7+
const style = getComputedStyle(node);
8+
9+
const topBorder = parseInt(style.borderTopWidth, 10);
10+
const rightBorder = parseInt(style.borderRightWidth, 10);
11+
const bottomBorder = parseInt(style.borderBottomWidth, 10);
12+
const leftBorder = parseInt(style.borderLeftWidth, 10);
13+
14+
return new Rect(
15+
rect.left + leftBorder || 0,
16+
rect.top + topBorder || 0,
17+
Math.min(rect.width - leftBorder - rightBorder, node.clientWidth) || 0,
18+
Math.min(rect.height - topBorder - bottomBorder, node.clientHeight) || 0,
19+
);
20+
};
21+
22+
export default getContentRect;

0 commit comments

Comments
 (0)