Skip to content

Commit 427b2c8

Browse files
committed
feat: dropdown will open on top if needed
Calculate if viewport, clientHeight and offset are enough for showing the dropdown on bottom. If not, then show on top.
1 parent 0202eb3 commit 427b2c8

File tree

6 files changed

+109
-19
lines changed

6 files changed

+109
-19
lines changed

__tests__/unit/iconHelpers.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,29 @@ describe('InvalidSourceException', () => {
122122
);
123123
});
124124
});
125+
126+
describe('debounce', () => {
127+
let mutate = false;
128+
const mutator = () => {
129+
mutate = true;
130+
};
131+
132+
const watcher = () =>
133+
new Promise(resolve => {
134+
setTimeout(() => {
135+
resolve();
136+
}, 200);
137+
});
138+
139+
test('works properly', () => {
140+
iconHelpers.debounce(() => mutator(), 100)();
141+
iconHelpers.debounce(() => mutator(), 100)();
142+
iconHelpers.debounce(() => mutator(), 100)();
143+
144+
expect(mutate).toBe(false);
145+
146+
return watcher().then(() => {
147+
expect(mutate).toBe(true);
148+
});
149+
});
150+
});

src/docs/scss/layout/_app.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
.app-main {
1414
padding-top: 16px;
1515
overflow: hidden;
16+
padding-bottom: 64px;
1617
@media screen and (min-width: 720px) {
17-
min-height: calc(100vh - 114px);
18+
min-height: calc(100vh - 124px);
1819
width: calc(100% - 225px);
1920
}
2021
}

src/docs/scss/layout/_sidebar.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
}
6565

6666
@media screen and (min-width: 720px) {
67-
min-height: calc(100vh - 114px);
67+
min-height: calc(100vh - 124px);
6868
width: 220px;
6969
overflow-y: auto;
7070
.hamburger {

src/js/components/FipDropDownPortal.jsx

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React from 'react';
77
import { createPortal } from 'react-dom';
88
import PropTypes from 'prop-types';
99
import className from 'classnames';
10-
import { getOffset } from '../helpers/iconHelpers';
10+
import { getOffset, debounce } from '../helpers/iconHelpers';
1111

1212
class FipDropDownPortal extends React.PureComponent {
1313
static propTypes = {
@@ -66,10 +66,17 @@ class FipDropDownPortal extends React.PureComponent {
6666
// because it will be rendered by the
6767
// getDerivedStateFromProps lifecycle method
6868
this.state = {};
69+
70+
// A debounced function for resize and scroll
71+
this.debouncedSyncPortalPosition = debounce(
72+
this.syncPortalPosition,
73+
250,
74+
);
6975
}
7076

7177
componentDidMount() {
72-
window.addEventListener('resize', this.syncPortalPosition);
78+
window.addEventListener('resize', this.debouncedSyncPortalPosition);
79+
window.addEventListener('scroll', this.debouncedSyncPortalPosition);
7380
this.syncPortalPosition();
7481
}
7582

@@ -78,15 +85,13 @@ class FipDropDownPortal extends React.PureComponent {
7885
}
7986
/* istanbul ignore next */
8087
componentWillUnmount() {
81-
window.removeEventListener('resize', this.syncPortalPosition);
88+
window.removeEventListener('resize', this.debouncedSyncPortalPosition);
89+
window.removeEventListener('scroll', this.debouncedSyncPortalPosition);
8290
}
8391

8492
syncPortalPosition = () => {
85-
// if mounting not to self, then position the portal
86-
if (this.state.appendRoot !== 'self') {
87-
// setTimeout(() => this.positionPortal(), 10);
88-
this.positionPortal();
89-
}
93+
// reset the portal
94+
this.resetPortalPosition();
9095

9196
// Fix window overflow
9297
this.fixWindowOverflow();
@@ -113,17 +118,40 @@ class FipDropDownPortal extends React.PureComponent {
113118
this.props.domRef.current.style.display = display;
114119
}
115120

121+
resetPortalPosition() {
122+
const { current: dropDown } = this.props.domRef;
123+
if (this.state.appendRoot === 'self') {
124+
// The top would be none
125+
dropDown.style.top = '';
126+
} else {
127+
this.positionPortal();
128+
}
129+
}
130+
116131
fixWindowOverflow = /* istanbul ignore next */ () => {
117132
const popupWidth = this.props.domRef.current.offsetWidth;
118-
const windowWidth = window.innerWidth;
119-
const { left: popupOffsetLeft } = getOffset(this.props.domRef.current);
133+
const popupHeight = this.props.domRef.current.offsetHeight;
134+
const { innerWidth: windowWidth, pageYOffset } = window;
135+
const { clientHeight } = document.documentElement;
136+
137+
const { left: popupOffsetLeft, top: popupOffsetTop } = getOffset(
138+
this.props.domRef.current,
139+
);
140+
const rootElm =
141+
this.state.appendRoot === 'self'
142+
? this.props.domRef.current
143+
: this.state.appendRoot;
144+
const rootOffset = getOffset(rootElm);
145+
const { current: btn } = this.props.btnRef;
146+
const { current: dropDown } = this.props.domRef;
147+
const btnOffset = getOffset(btn);
148+
const btnStyles = getComputedStyle(btn);
149+
const btnBorder =
150+
(parseInt(btnStyles.borderTop, 10) || 0) +
151+
(parseInt(btnStyles.borderBottom, 10) || 0);
152+
120153
// We need to calculate if the popup is going to overflow the window
121154
if (popupOffsetLeft + popupWidth > windowWidth - 20) {
122-
const btnOffset = getOffset(this.props.btnRef.current);
123-
const rootOffset =
124-
this.state.appendRoot === 'self'
125-
? getOffset(this.props.domRef.current)
126-
: getOffset(this.state.appendRoot);
127155
let preferredLeft =
128156
btnOffset.left +
129157
this.props.btnRef.current.offsetWidth -
@@ -134,7 +162,26 @@ class FipDropDownPortal extends React.PureComponent {
134162
}
135163

136164
// Now set the goddamn left value
137-
this.props.domRef.current.style.left = `${preferredLeft}px`;
165+
dropDown.style.left = `${preferredLeft}px`;
166+
}
167+
// We need to calculate if opened popup is too low
168+
if (
169+
// the height of popup + popoffset top > view port height
170+
popupHeight + popupOffsetTop - pageYOffset > clientHeight &&
171+
// If we are to position on top of button, then make sure page view can handle
172+
// so button offset top - popup height > 0
173+
btnOffset.top - popupHeight > 0
174+
) {
175+
// Now we position the popup on top of the button
176+
if (this.state.appendRoot === 'self') {
177+
// When appending to self, position should be relative to the
178+
// button height and popup height
179+
dropDown.style.top = `-${popupHeight - btnBorder}px`;
180+
} else {
181+
dropDown.style.top = `${btnOffset.top +
182+
btnBorder -
183+
popupHeight}px`; // 2px for border
184+
}
138185
}
139186
};
140187

src/js/helpers/iconHelpers.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,23 @@ export function InvalidSourceException(givenType, requiredType) {
133133
};
134134
}
135135

136+
/**
137+
* Implementation of debounce function
138+
*
139+
* {@link https://medium.com/a-developers-perspective/throttling-and-debouncing-in-javascript-b01cad5c8edf}
140+
* @param {Function} func callback function
141+
* @param {int} delay delay in milliseconds
142+
*/
143+
export const debounce = (func, delay) => {
144+
let inDebounce;
145+
return function debounceFunc() {
146+
const context = this;
147+
const args = arguments; // eslint-disable-line
148+
clearTimeout(inDebounce);
149+
inDebounce = setTimeout(() => func.apply(context, args), delay);
150+
};
151+
};
152+
136153
/**
137154
* FuzzySearch Implementation
138155
*

src/scss/components/_rfipdropdown.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
position: absolute;
55
left: 0;
66
margin-top: -1px;
7-
margin-bottom: 50px; // For proper viewing of shadow
87
z-index: 100000001;
98
border-radius: 0 1px 4px 4px;
109

0 commit comments

Comments
 (0)