More reliable dropdown positioning (Fixes #1113)

This commit is contained in:
Gregory Schier 2018-12-11 17:02:30 -05:00
parent 2b971aed1e
commit 6f355bc7ba
2 changed files with 62 additions and 75 deletions

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en-US">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Insomnia</title> <title>Insomnia</title>
@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<div id="dropdowns-container" style="z-index:1000000;position:relative"></div>
<script src="bundle.js" type="application/javascript"></script> <script src="bundle.js" type="application/javascript"></script>
</body> </body>
</html> </html>

View File

@ -10,6 +10,8 @@ import { fuzzyMatch } from '../../../../common/misc';
import KeydownBinder from '../../keydown-binder'; import KeydownBinder from '../../keydown-binder';
import * as hotkeys from '../../../../common/hotkeys'; import * as hotkeys from '../../../../common/hotkeys';
const dropdownsContainer = document.querySelector('#dropdowns-container');
@autobind @autobind
class Dropdown extends PureComponent { class Dropdown extends PureComponent {
constructor(props) { constructor(props) {
@ -123,13 +125,17 @@ class Dropdown extends PureComponent {
return; return;
} }
// Make the dropdown scroll if it drops off screen. // Get dropdown menu
const dropdownRect = this._node.getBoundingClientRect(); const dropdownList = this._dropdownList;
// Compute the size of all the menus
const dropdownBtnRect = this._node.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect();
const dropdownListRect = dropdownList.getBoundingClientRect();
// Should it drop up? // Should it drop up?
const bodyHeight = bodyRect.height; const bodyHeight = bodyRect.height;
const dropdownTop = dropdownRect.top; const dropdownTop = dropdownBtnRect.top;
const dropUp = dropdownTop > bodyHeight - 200; const dropUp = dropdownTop > bodyHeight - 200;
// Reset all the things so we can start fresh // Reset all the things so we can start fresh
@ -140,45 +146,51 @@ class Dropdown extends PureComponent {
this._dropdownList.style.minWidth = 'initial'; this._dropdownList.style.minWidth = 'initial';
this._dropdownList.style.maxWidth = 'initial'; this._dropdownList.style.maxWidth = 'initial';
// Make dropdown keep it's shape when filtering if (!dropdownList.hasAttribute('data-fixed-shape')) {
const ul = this._dropdownList.querySelector('ul'); this._dropdownList.style.minHeight = `${dropdownListRect.height}px`;
const ulRect = ul.getBoundingClientRect(); this._dropdownList.style.minWidth = `${dropdownListRect.width}px`;
if (!ul.hasAttribute('data-fixed-shape')) { this._dropdownList.style.width = '100%';
ul.style.minHeight = `${ulRect.height}px`; this._dropdownList.setAttribute('data-fixed-shape', 'on');
ul.style.minWidth = `${ulRect.width}px`;
ul.style.width = '100%';
ul.setAttribute('data-fixed-shape', 'on');
} }
const screenMargin = 5;
const { right, wide } = this.props; const { right, wide } = this.props;
if (right || wide) { if (right || wide) {
const { right: originalRight } = dropdownRect; const { right: originalRight } = dropdownBtnRect;
// Prevent dropdown from squishing against left side of screen // Prevent dropdown from squishing against left side of screen
const right = Math.max(220, originalRight); const right = Math.max(dropdownListRect.width + screenMargin, originalRight);
const { beside } = this.props; const { beside } = this.props;
const offset = beside ? dropdownRect.width - dropdownRect.height : 0; const offset = beside ? dropdownBtnRect.width - 40 : 0;
this._dropdownList.style.right = `${bodyRect.width - right + offset}px`; this._dropdownList.style.right = `${bodyRect.width - right + offset}px`;
this._dropdownList.style.maxWidth = `${right + offset}px`; this._dropdownList.style.maxWidth = `${Math.min(dropdownListRect.width, right + offset)}px`;
this._dropdownList.style.minWidth = `${Math.min(right, 200)}px`;
} }
if (!right || wide) { if (!right || wide) {
const { left } = dropdownRect; const { left: originalLeft } = dropdownBtnRect;
const { beside } = this.props; const { beside } = this.props;
const offset = beside ? dropdownRect.width - dropdownRect.height : 0; const offset = beside ? dropdownBtnRect.width - 40 : 0;
// Prevent dropdown from squishing against right side of screen
const left =
Math.min(bodyRect.width - dropdownListRect.width - screenMargin, originalLeft) - offset;
this._dropdownList.style.left = `${left + offset}px`; this._dropdownList.style.left = `${left + offset}px`;
this._dropdownList.style.maxWidth = `${bodyRect.width - left - 5 - offset}px`; this._dropdownList.style.maxWidth = `${Math.min(
this._dropdownList.style.minWidth = `${Math.min(bodyRect.width - left, 200)}px`; dropdownListRect.width,
bodyRect.width - left - offset
)}px`;
} }
if (dropUp) { if (dropUp) {
const { top } = dropdownRect; const { top } = dropdownBtnRect;
this._dropdownList.style.bottom = `${bodyRect.height - top}px`; this._dropdownList.style.bottom = `${bodyRect.height - top}px`;
this._dropdownList.style.maxHeight = `${top - 5}px`; this._dropdownList.style.maxHeight = `${top - 5}px`;
} else { } else {
const { bottom } = dropdownRect; const { bottom } = dropdownBtnRect;
this._dropdownList.style.top = `${bottom}px`; this._dropdownList.style.top = `${bottom}px`;
this._dropdownList.style.maxHeight = `${bodyRect.height - bottom - 5}px`; this._dropdownList.style.maxHeight = `${bodyRect.height - bottom - 5}px`;
} }
@ -188,7 +200,7 @@ class Dropdown extends PureComponent {
this.toggle(); this.toggle();
} }
_handleMouseDown(e) { static _handleMouseDown(e) {
// Intercept mouse down so that clicks don't trigger things like drag and drop. // Intercept mouse down so that clicks don't trigger things like drag and drop.
e.preventDefault(); e.preventDefault();
} }
@ -233,35 +245,6 @@ class Dropdown extends PureComponent {
this._checkSizeAndPosition(); this._checkSizeAndPosition();
} }
_getContainer() {
let container = document.querySelector('#dropdowns-container');
if (!container) {
container = document.createElement('div');
container.id = 'dropdowns-container';
container.style.zIndex = '1000000';
container.style.position = 'relative';
document.body.appendChild(container);
}
return container;
}
componentDidMount() {
// Move the element to the body so we can position absolutely
if (this._dropdownMenu) {
const el = ReactDOM.findDOMNode(this._dropdownMenu);
this._getContainer().appendChild(el);
}
}
componentWillUnmount() {
// Remove the element from the body
if (this._dropdownMenu) {
const el = ReactDOM.findDOMNode(this._dropdownMenu);
this._getContainer().removeChild(el);
}
}
hide() { hide() {
// Focus the dropdown button after hiding // Focus the dropdown button after hiding
if (this._node) { if (this._node) {
@ -373,6 +356,7 @@ class Dropdown extends PureComponent {
const noResults = filter && filterItems && filterItems.length === 0; const noResults = filter && filterItems && filterItems.length === 0;
finalChildren = [ finalChildren = [
dropdownButtons[0], dropdownButtons[0],
ReactDOM.createPortal(
<div key="item" className={menuClasses} ref={this._addDropdownMenuRef}> <div key="item" className={menuClasses} ref={this._addDropdownMenuRef}>
<div className="dropdown__backdrop theme--transparent-overlay" /> <div className="dropdown__backdrop theme--transparent-overlay" />
<div <div
@ -394,7 +378,9 @@ class Dropdown extends PureComponent {
{noResults && <div className="text-center pad warning">No match :(</div>} {noResults && <div className="text-center pad warning">No match :(</div>}
<ul className={classnames({ hide: noResults })}>{dropdownItems}</ul> <ul className={classnames({ hide: noResults })}>{dropdownItems}</ul>
</div> </div>
</div> </div>,
dropdownsContainer
)
]; ];
} }
@ -406,7 +392,7 @@ class Dropdown extends PureComponent {
ref={this._setRef} ref={this._setRef}
onClick={this._handleClick} onClick={this._handleClick}
tabIndex="-1" tabIndex="-1"
onMouseDown={this._handleMouseDown}> onMouseDown={Dropdown._handleMouseDown}>
{finalChildren} {finalChildren}
</div> </div>
</KeydownBinder> </KeydownBinder>