-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POC: use react portals for cell editors #1369
Changes from 9 commits
3e75128
e9c97ba
91e2cc2
6488ef1
61b938d
55df83b
157aff0
111f2c2
0ebd806
81cef8a
89b1143
86b7423
d62f89a
4b93765
e18bf66
367b5a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,11 @@ import React from 'react'; | |
import PropTypes from 'prop-types'; | ||
import joinClasses from 'classnames'; | ||
import SimpleTextEditor from './SimpleTextEditor'; | ||
import {isFunction} from 'common/utils'; | ||
import { isFunction } from 'common/utils'; | ||
import { isKeyPrintable, isCtrlKeyHeldDown } from 'common/utils/keyboardUtils'; | ||
import zIndexes from 'common/constants/zIndexes'; | ||
import EditorPortal from './EditorPortal'; | ||
|
||
require('../../../themes/react-data-grid-core.css'); | ||
|
||
const isFrozen = column => column.frozen === true || column.locked === true; | ||
|
@@ -22,14 +24,13 @@ class EditorContainer extends React.Component { | |
onCommit: PropTypes.func, | ||
onCommitCancel: PropTypes.func, | ||
firstEditorKeyPress: PropTypes.string, | ||
width: PropTypes.number, | ||
top: PropTypes.number, | ||
left: PropTypes.number, | ||
width: PropTypes.number.isRequired, | ||
scrollLeft: PropTypes.number, | ||
scrollTop: PropTypes.number | ||
scrollTop: PropTypes.number, | ||
position: PropTypes.object.isRequired | ||
}; | ||
|
||
state = {isInvalid: false}; | ||
state = { isInvalid: false }; | ||
changeCommitted = false; | ||
changeCanceled = false; | ||
|
||
|
@@ -42,7 +43,6 @@ class EditorContainer extends React.Component { | |
inputNode.style.height = this.props.height - 1 + 'px'; | ||
} | ||
} | ||
window.addEventListener('scroll', this.setContainerPosition); | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
|
@@ -53,24 +53,10 @@ class EditorContainer extends React.Component { | |
|
||
componentWillUnmount() { | ||
if (!this.changeCommitted && !this.changeCanceled) { | ||
this.commit({key: 'Enter'}); | ||
} | ||
window.removeEventListener('scroll', this.setContainerPosition); | ||
} | ||
|
||
setContainerPosition = () => { | ||
if (this.container) { | ||
this.container.style.transform = this.calculateTransform(); | ||
this.commit({ key: 'Enter' }); | ||
} | ||
} | ||
|
||
calculateTransform = () => { | ||
const { column, left, scrollLeft, top, scrollTop } = this.props; | ||
const editorLeft = isFrozen(column) ? left : left - scrollLeft; | ||
const editorTop = top - scrollTop - window.pageYOffset; | ||
return `translate(${editorLeft}px, ${editorTop}px)`; | ||
} | ||
|
||
isKeyExplicitlyHandled = (key) => { | ||
return isFunction(this['onPress' + key]); | ||
}; | ||
|
@@ -132,15 +118,15 @@ class EditorContainer extends React.Component { | |
return <CustomEditor ref={this.setEditorRef} {...editorProps} />; | ||
} | ||
|
||
return <SimpleTextEditor ref={this.setEditorRef} column={this.props.column} value={this.getInitialValue()} onBlur={this.commit} rowMetaData={this.getRowMetaData()} onKeyDown={() => {}} commit={() => {}}/>; | ||
return <SimpleTextEditor ref={this.setEditorRef} column={this.props.column} value={this.getInitialValue()} onBlur={this.commit} rowMetaData={this.getRowMetaData()} onKeyDown={() => { }} commit={() => { }} />; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we lift the |
||
}; | ||
|
||
onPressEnter = () => { | ||
this.commit({key: 'Enter'}); | ||
this.commit({ key: 'Enter' }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can refactor this to use constants instead of magic strings There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a |
||
}; | ||
|
||
onPressTab = () => { | ||
this.commit({key: 'Tab'}); | ||
this.commit({ key: 'Tab' }); | ||
}; | ||
|
||
onPressEscape = (e) => { | ||
|
@@ -239,13 +225,13 @@ class EditorContainer extends React.Component { | |
}; | ||
|
||
commit = (args) => { | ||
const {onCommit} = this.props; | ||
const { onCommit } = this.props; | ||
let opts = args || {}; | ||
let updated = this.getEditor().getValue(); | ||
if (this.isNewValueValid(updated)) { | ||
this.changeCommitted = true; | ||
let cellKey = this.props.column.key; | ||
onCommit({cellKey: cellKey, rowIdx: this.props.rowIdx, updated: updated, key: opts.key}); | ||
onCommit({ cellKey: cellKey, rowIdx: this.props.rowIdx, updated: updated, key: opts.key }); | ||
} | ||
}; | ||
|
||
|
@@ -257,7 +243,7 @@ class EditorContainer extends React.Component { | |
isNewValueValid = (value) => { | ||
if (isFunction(this.getEditor().validate)) { | ||
let isValid = this.getEditor().validate(value); | ||
this.setState({isInvalid: !isValid}); | ||
this.setState({ isInvalid: !isValid }); | ||
return isValid; | ||
} | ||
|
||
|
@@ -306,8 +292,8 @@ class EditorContainer extends React.Component { | |
|
||
getRelatedTarget = (e) => { | ||
return e.relatedTarget || | ||
e.explicitOriginalTarget || | ||
document.activeElement; // IE11 | ||
e.explicitOriginalTarget || | ||
document.activeElement; // IE11 | ||
}; | ||
|
||
handleRightClick = (e) => { | ||
|
@@ -321,7 +307,7 @@ class EditorContainer extends React.Component { | |
} | ||
|
||
if (!this.isBodyClicked(e)) { | ||
// prevent null reference | ||
// prevent null reference | ||
if (this.isViewportClicked(e) || !this.isClickInsideEditor(e)) { | ||
this.commit(e); | ||
} | ||
|
@@ -349,15 +335,22 @@ class EditorContainer extends React.Component { | |
}; | ||
|
||
render() { | ||
const { width, height, column } = this.props; | ||
const zIndex = isFrozen(column) ? zIndexes.FROZEN_EDITOR_CONTAINER : zIndexes.EDITOR_CONTAINER; | ||
const style = { position: 'fixed', height, width, zIndex, transform: this.calculateTransform() }; | ||
const { width, height, column, position } = this.props; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have removed |
||
const zIndex = isFrozen(column) ? zIndexes.FROZEN_EDITOR_CONTAINER : zIndexes.EDITOR_CONTAINER; | ||
const style = { position: 'absolute', height, width, zIndex, ...position }; | ||
return ( | ||
<div ref={this.setContainerRef} style={style} className={this.getContainerClass()} onBlur={this.handleBlur} onKeyDown={this.onKeyDown} onContextMenu={this.handleRightClick}> | ||
<EditorPortal> | ||
<div style={style} | ||
className={this.getContainerClass()} | ||
onBlur={this.handleBlur} | ||
onKeyDown={this.onKeyDown} | ||
onContextMenu={this.handleRightClick} | ||
> | ||
{this.createEditor()} | ||
{this.renderStatusIcon()} | ||
</div> | ||
); | ||
</EditorPortal> | ||
); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import PropTypes from 'prop-types'; | ||
|
||
const editorRoot = document.body; | ||
|
||
export default class EditorPortal extends React.Component { | ||
static propTypes = { | ||
children: PropTypes.node | ||
}; | ||
|
||
el = document.createElement('div'); | ||
|
||
componentDidMount() { | ||
editorRoot.appendChild(this.el); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this makes sense and can fix some of those annoying issues that arise from using fixed positioning |
||
} | ||
|
||
componentWillUnmount() { | ||
editorRoot.removeChild(this.el); | ||
} | ||
|
||
render() { | ||
return ReactDOM.createPortal( | ||
this.props.children, | ||
this.el, | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import Row from './Row'; | ||
import cellMetaDataShape from 'common/prop-shapes/CellActionShape'; | ||
import * as rowUtils from './RowUtils'; | ||
|
@@ -218,9 +218,13 @@ class Canvas extends React.PureComponent { | |
}; | ||
|
||
setScrollLeft = (scrollLeft) => { | ||
if (this.interactionMasks && this.interactionMasks.setScrollLeft) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the same approach to fix mask's position There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since you are touching this file, maybe we can also add some simple doc above the function? like
|
||
this.interactionMasks.setScrollLeft(scrollLeft); | ||
} | ||
|
||
this.rows.forEach((r, idx) => { | ||
if (r) { | ||
let row = this.getRowByRef(idx); | ||
const row = this.getRowByRef(idx); | ||
if (row && row.setScrollLeft) { | ||
row.setScrollLeft(scrollLeft); | ||
} | ||
|
@@ -237,37 +241,23 @@ class Canvas extends React.PureComponent { | |
return this.rows[i]; | ||
}; | ||
|
||
getSelectedRowTop = (rowIdx) => { | ||
const row = this.getRowByRef(rowIdx); | ||
if (row) { | ||
const node = ReactDOM.findDOMNode(row); | ||
return node && node.offsetTop; | ||
} | ||
return this.props.rowHeight * rowIdx; | ||
} | ||
|
||
getSelectedRowHeight = (rowIdx) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this logic was added to support variable height rows. This adds additional complexity and it is currently not used by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by saying variable height, do you mean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is correct. This is a great feature to have but we need to handle all the masks (Copy and Drag). This currently does not work with them and has some edge cases so I removed it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that it adds complexity. When starting the work on the Interaction Mask, I did it under the assumption that RDG only supports fixed row height. However, this is not exactly true as it is possible to have dynamic row height using custom row renderers. I have seen a lot of cases out in the wild where this is happening. Removing this, is going to cause a lot of strange display issues for those cases Supporting dynamic row height is not trivial, and this current implementation was really just a stop gap to support these non documented edge cases. I would prefer to leave this here and think of a better solution that to completely remove it |
||
const row = this.getRowByRef(rowIdx); | ||
if (row) { | ||
const node = ReactDOM.findDOMNode(row); | ||
return node && node.clientHeight > 0 ? node.clientHeight : this.props.rowHeight; | ||
} | ||
return this.props.rowHeight; | ||
} | ||
|
||
getSelectedRowColumns = (rowIdx) => { | ||
const row = this.getRowByRef(rowIdx); | ||
return row && row.props ? row.props.columns : this.props.columns; | ||
} | ||
|
||
setCanvasRef = (canvas) => { | ||
this.canvas = canvas; | ||
setCanvasRef = el => { | ||
this.canvas = el; | ||
}; | ||
|
||
setRowRef = idx => row => { | ||
this.rows[idx] = row; | ||
}; | ||
|
||
setInteractionMasksRef = el => { | ||
this.interactionMasks = el; | ||
}; | ||
|
||
renderCustomRowRenderer(props) { | ||
const { ref, ...otherProps } = props; | ||
const CustomRowRenderer = this.props.rowRenderer; | ||
|
@@ -382,6 +372,7 @@ class Canvas extends React.PureComponent { | |
onScroll={this.onScroll} | ||
className="react-grid-Canvas"> | ||
<InteractionMasks | ||
ref={this.setInteractionMasksRef} | ||
rowGetter={rowGetter} | ||
rowsCount={rowsCount} | ||
width={this.props.totalWidth} | ||
|
@@ -414,10 +405,6 @@ class Canvas extends React.PureComponent { | |
onCellRangeSelectionCompleted={this.props.onCellRangeSelectionCompleted} | ||
scrollLeft={this._scroll.scrollLeft} | ||
scrollTop={this._scroll.scrollTop} | ||
prevScrollLeft={this.props.prevScrollLeft} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This props do not seem to be used anywhere There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think they can be safely removed. |
||
prevScrollTop={this.props.prevScrollTop} | ||
getSelectedRowHeight={this.getSelectedRowHeight} | ||
getSelectedRowTop={this.getSelectedRowTop} | ||
getSelectedRowColumns={this.getSelectedRowColumns} | ||
/> | ||
<RowsContainer id={contextMenu ? contextMenu.props.id : 'rowsContainer'}> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not that familiar with the really old syntax, should we add the shape of the position?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I will do that in a separate PR to keep this simple.