Allow to use Portal for rendering Picker, rework animation

master
Vitaliy Filippov 2021-09-16 01:14:56 +03:00
parent c3cc77f244
commit f1f7941bf2
4 changed files with 72 additions and 55 deletions

View File

@ -4,7 +4,7 @@
// ...Or maybe a button with a popup menu // ...Or maybe a button with a popup menu
// License: LGPLv3.0+ // License: LGPLv3.0+
// (c) Vitaliy Filippov 2019+ // (c) Vitaliy Filippov 2019+
// Version 2021-09-09 // Version 2021-09-14
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -22,6 +22,7 @@ export default class Picker extends React.Component
popupX: PropTypes.number, popupX: PropTypes.number,
popupY: PropTypes.number, popupY: PropTypes.number,
onHide: PropTypes.func, onHide: PropTypes.func,
usePortal: PropTypes.bool,
} }
state = { state = {
@ -98,21 +99,67 @@ export default class Picker extends React.Component
}); });
} }
animatePicker = (e) =>
{
if (e)
{
e = ReactDOM.findDOMNode(e);
if (!this.props.renderInput)
{
e.focus();
}
e.style.visibility = 'hidden';
e.style.overflowY = 'hidden';
const anim = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
anim(() =>
{
e.style.visibility = '';
e.style.maxHeight = '1px';
anim(() =>
{
e.style.transitionTimingFunction = 'cubic-bezier(0.4, 0, 0.2, 1)';
e.style.transitionDuration = '0.2s';
e.style.transitionProperty = 'max-height';
e.style.maxHeight = '100%';
const end = () =>
{
e.style.transitionProperty = '';
e.style.maxHeight = '';
e.style.overflowY = '';
e.removeEventListener('transitionend', end);
};
e.addEventListener('transitionend', end);
});
});
}
}
render() render()
{ {
return (<React.Fragment> return (<React.Fragment>
{this.props.renderInput && this.props.renderInput(this.getInputProps())} {this.props.renderInput && this.props.renderInput(this.getInputProps())}
{!this.props.renderInput || this.state.focused {!this.props.renderInput || this.state.focused
? <div style={{ ? (this.props.usePortal
position: 'fixed', ? ReactDOM.createPortal(<div style={{
height: this.state.height ? this.state.height+'px' : 'auto', position: 'absolute',
top: this.state.top+'px', height: this.state.height ? this.state.height+'px' : 'auto',
width: this.state.width ? this.state.width+'px' : 'auto', top: this.state.top+'px',
left: this.state.left+'px', width: this.state.width ? this.state.width+'px' : 'auto',
zIndex: 100, left: this.state.left+'px',
}} ref={this.setPicker}> zIndex: 100,
{this.renderPicker()} }} ref={this.setPicker}>
</div> {this.renderPicker()}
</div>, document.body)
: <div style={{
position: 'fixed',
height: this.state.height ? this.state.height+'px' : 'auto',
top: this.state.top+'px',
width: this.state.width ? this.state.width+'px' : 'auto',
left: this.state.left+'px',
zIndex: 100,
}} ref={this.setPicker}>
{this.renderPicker()}
</div>)
: null} : null}
</React.Fragment>); </React.Fragment>);
} }
@ -169,7 +216,7 @@ export default class Picker extends React.Component
} }
} }
static calculatePopupPosition(clientRect, popup, props) static calculatePopupPosition(input_pos, popup, props)
{ {
const popup_size = ReactDOM.findDOMNode(popup).getBoundingClientRect(); const popup_size = ReactDOM.findDOMNode(popup).getBoundingClientRect();
const screen_width = window.innerWidth || document.documentElement.offsetWidth; const screen_width = window.innerWidth || document.documentElement.offsetWidth;
@ -177,22 +224,24 @@ export default class Picker extends React.Component
let direction = props && props.direction; let direction = props && props.direction;
if (!direction || direction === 'auto') if (!direction || direction === 'auto')
{ {
const down = clientRect.top + popup_size.height < screen_height || const down = input_pos.top + popup_size.height < screen_height ||
clientRect.top < screen_height/2; input_pos.top < screen_height/2;
direction = down ? 'down' : 'up'; direction = down ? 'down' : 'up';
} }
let top = clientRect.top let top = input_pos.top
+ (props.usePortal ? (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop) : 0)
- (document.documentElement.clientTop || document.body.clientTop || 0); - (document.documentElement.clientTop || document.body.clientTop || 0);
const max_height = (direction == 'down' ? screen_height-top-(clientRect.height||0)-32 : top-32); const max_height = (direction == 'down' ? screen_height-top-(input_pos.height||0)-32 : top-32);
const height = Math.ceil(popup_size.height < max_height ? popup_size.height : max_height); const height = Math.ceil(popup_size.height < max_height ? popup_size.height : max_height);
top = direction == 'down' ? (top + (clientRect.height||0)) : (top - height); top = direction == 'down' ? (top + (input_pos.height||0)) : (top - height);
let left = (clientRect.left let left = (input_pos.left
+ (props.usePortal ? (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft) : 0)
- (document.documentElement.clientLeft || document.body.clientLeft || 0)); - (document.documentElement.clientLeft || document.body.clientLeft || 0));
if (left + popup_size.width > screen_width) if (left + popup_size.width > screen_width)
{ {
left = screen_width - popup_size.width; left = screen_width - popup_size.width;
} }
let width = (clientRect.width||0) > popup_size.width ? clientRect.width : popup_size.width; let width = (input_pos.width||0) > popup_size.width ? input_pos.width : popup_size.width;
width = Math.ceil(props && props.minWidth && width < props.minWidth ? props.minWidth : width); width = Math.ceil(props && props.minWidth && width < props.minWidth ? props.minWidth : width);
return { top, left, width, height }; return { top, left, width, height };
} }

View File

@ -117,36 +117,6 @@ export default class PickerMenu extends Picker
} }
} }
animatePicker = (e) =>
{
if (e)
{
if (!this.props.renderInput)
{
e.focus();
}
e.style.visibility = 'hidden';
e.style.overflowY = 'hidden';
const anim = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
anim(() =>
{
e.style.visibility = '';
e.style.maxHeight = '1px';
anim(() =>
{
e.style.maxHeight = '100%';
const end = () =>
{
e.style.maxHeight = 'none';
e.style.overflowY = 'auto';
e.removeEventListener('transitionend', end);
};
e.addEventListener('transitionend', end);
});
});
}
}
renderPicker = () => renderPicker = () =>
{ {
const theme = this.props.theme || autocomplete_css; const theme = this.props.theme || autocomplete_css;

View File

@ -1,5 +1,5 @@
// Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules // Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules
// Version 2021-08-30 // Version 2021-09-14
// License: LGPLv3.0+ // License: LGPLv3.0+
// (c) Vitaliy Filippov 2019+ // (c) Vitaliy Filippov 2019+
@ -289,6 +289,7 @@ export default class Selectbox extends React.PureComponent
disabledKey={this.props.disabledKey} disabledKey={this.props.disabledKey}
afterItems={this.props.suggestionMsg} afterItems={this.props.suggestionMsg}
onSelectItem={this.onSelectItem} onSelectItem={this.onSelectItem}
usePortal={this.props.usePortal}
/>); />);
} }

View File

@ -43,7 +43,6 @@
overflow: hidden; overflow: hidden;
cursor: text; cursor: text;
height: auto; height: auto;
padding: 1px 0;
} }
.input.readonly, .input.readonly .inputElement .input.readonly, .input.readonly .inputElement
@ -60,15 +59,12 @@
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding: 0; padding: 0;
transition-duration: 0.2s;
transition-property: max-height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
width: 100%; width: 100%;
max-height: 100%;
} }
.suggestion .suggestion
{ {
font-family: helvetica, arial, verdana, sans-serif;
font-size: 13px; font-size: 13px;
padding: 7px; padding: 7px;
user-select: none; user-select: none;
@ -90,6 +86,7 @@
{ {
background: transparent; background: transparent;
font-size: inherit; font-size: inherit;
font-weight: inherit;
line-height: 200%; line-height: 200%;
border: 0; border: 0;
padding: 0 2em 0 .5em; padding: 0 2em 0 .5em;