-
Notifications
You must be signed in to change notification settings - Fork 829
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
[V4] EditableCell improvements #278
Changes from 2 commits
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,6 +2,7 @@ import React from 'react' | |
import PropTypes from 'prop-types' | ||
import { withTheme } from '../../theme' | ||
import { Portal } from '../../portal' | ||
import { Stack } from '../../stack' | ||
import TextTableCell from './TextTableCell' | ||
import TableCell from './TableCell' | ||
import EditableCellField from './EditableCellField' | ||
|
@@ -13,6 +14,22 @@ class EditableCell extends React.PureComponent { | |
*/ | ||
...TableCell.propTypes, | ||
|
||
/* | ||
* Makes the TableCell focusable. | ||
* Will add tabIndex={-1 || this.props.tabIndex}. | ||
*/ | ||
isSelectable: PropTypes.bool, | ||
|
||
/** | ||
* When true, the cell can't be edited. | ||
*/ | ||
disabled: PropTypes.bool, | ||
|
||
/** | ||
* Optional placeholder when children is falsy. | ||
*/ | ||
placeholder: PropTypes.node, | ||
|
||
/** | ||
* The size used for the TextTableCell and Textarea. | ||
*/ | ||
|
@@ -21,23 +38,62 @@ class EditableCell extends React.PureComponent { | |
/** | ||
* This is the value of the cell. | ||
*/ | ||
children: PropTypes.string, | ||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||
|
||
/** | ||
* Function called when value changes. (value: string) => void. | ||
*/ | ||
onChange: PropTypes.func | ||
onChange: PropTypes.func, | ||
|
||
/** | ||
* When a value is passed, this component will be controlled. | ||
*/ | ||
isEditing: PropTypes.bool, | ||
|
||
/** | ||
* Only called when component is controlled and isSelectable={true}. | ||
*/ | ||
onEditStart: PropTypes.func, | ||
|
||
/** | ||
* Only called when component is controlled. | ||
*/ | ||
onEditComplete: PropTypes.func | ||
} | ||
|
||
static defaultProps = { | ||
size: 300 | ||
size: 300, | ||
isSelectable: true | ||
} | ||
|
||
static getDerivedStateFromProps(props, state) { | ||
if (props.children !== state.value) { | ||
return { | ||
value: props.children | ||
} | ||
} | ||
return null | ||
} | ||
|
||
state = { | ||
isEditing: false, | ||
value: this.props.children | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this.isControlled() && this.props.isEditing) { | ||
this.props.onEditComplete() | ||
} | ||
} | ||
|
||
/** | ||
* Return true when component is controlled. | ||
*/ | ||
isControlled = () => { | ||
const { isEditing } = this.props | ||
return isEditing !== null && isEditing !== undefined | ||
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. You could probably do |
||
} | ||
|
||
onMainRef = ref => { | ||
this.mainRef = ref | ||
} | ||
|
@@ -47,40 +103,76 @@ class EditableCell extends React.PureComponent { | |
} | ||
|
||
handleDoubleClick = () => { | ||
this.setState({ | ||
isEditing: true | ||
}) | ||
if (this.props.disabled || !this.props.isSelectable) return | ||
if (this.isControlled()) { | ||
this.props.onEditStart() | ||
} else { | ||
this.setState({ | ||
isEditing: true | ||
}) | ||
} | ||
} | ||
|
||
handleKeyDown = e => { | ||
if (this.props.disabled) return | ||
const { key } = e | ||
|
||
/** | ||
* When the user presses a character on the keyboard, use that character | ||
* as the value in the text field. | ||
*/ | ||
if (key.match(/^[a-z]{0,10}$/) && !e.metaKey && !e.ctrlKey && !e.altKey) { | ||
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 seems excessive to me – why not use an actual input element or a 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 see you are you a textarea, but binding to 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 can be a bit confusing! |
||
this.setState({ | ||
isEditing: true, | ||
value: key | ||
}) | ||
if (this.isControlled()) { | ||
this.props.onEditStart() | ||
this.setState({ | ||
value: key | ||
}) | ||
} else { | ||
this.setState({ | ||
isEditing: true, | ||
value: key | ||
}) | ||
} | ||
} else if (key === 'Enter') { | ||
this.setState({ | ||
isEditing: true | ||
}) | ||
if (this.isControlled()) { | ||
this.props.onEditStart() | ||
} else { | ||
this.setState({ | ||
isEditing: true | ||
}) | ||
} | ||
} | ||
} | ||
|
||
handleFieldBlur = value => { | ||
const { onChange } = this.props | ||
const { onChange, onEditComplete, isSelectable } = this.props | ||
const currentValue = this.state.value | ||
|
||
this.setState({ | ||
isEditing: false, | ||
// Make edit instantious. Deal with errors up the tree. | ||
value | ||
}) | ||
if (this.isControlled() && typeof onEditComplete === 'function') { | ||
this.props.onEditComplete(value) | ||
|
||
if (this.mainRef) this.mainRef.focus() | ||
this.setState({ | ||
value | ||
}) | ||
} else { | ||
this.setState({ | ||
isEditing: false, | ||
value | ||
}) | ||
} | ||
|
||
if (currentValue !== value && typeof onChange === 'function') { | ||
onChange(value) | ||
} | ||
|
||
if (this.mainRef) { | ||
if (isSelectable) { | ||
this.mainRef.focus() | ||
} else { | ||
const element = this.mainRef.querySelector('button') | ||
if (element) element.focus() | ||
} | ||
} | ||
} | ||
|
||
handleFieldCancel = () => { | ||
|
@@ -92,36 +184,60 @@ class EditableCell extends React.PureComponent { | |
} | ||
|
||
render() { | ||
const { children, theme, size, ...props } = this.props | ||
const { isEditing } = this.state | ||
const { | ||
children, | ||
theme, | ||
size, | ||
disabled, | ||
placeholder, | ||
isSelectable, | ||
isEditing: propsIsEditing, | ||
onEditComplete, | ||
...props | ||
} = this.props | ||
const { isEditing: stateIsEditing, value } = this.state | ||
|
||
let isEditing | ||
if (this.isControlled()) { | ||
// Controlled usage. | ||
isEditing = propsIsEditing | ||
} else { | ||
isEditing = stateIsEditing | ||
} | ||
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. Anytime you allow a component to be fully controlled OR fully uncontrolled you need to make some careful decisions around what happens when consumers switch a mounted component from one to the other. This could be as simple as a consumer unintentionally passing an How will this component's value be affected when you switch from controlled to uncontrolled or vice versa? 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. Beware of some of these pitfalls (which are commonly used when you have controlled and uncontrolled support side-by-side): https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#anti-pattern-unconditionally-copying-props-to-state 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. One implementation I've seen that helps tease apart concerns is to actually make a |
||
|
||
return ( | ||
<React.Fragment> | ||
<TextTableCell | ||
isSelectable | ||
isSelectable={isSelectable && !disabled} | ||
innerRef={this.onMainRef} | ||
onClick={this.handleClick} | ||
onDoubleClick={this.handleDoubleClick} | ||
onKeyDown={this.handleKeyDown} | ||
size={size} | ||
cursor="default" | ||
cursor={disabled ? 'not-allowed' : 'default'} | ||
textProps={{ | ||
size | ||
size, | ||
opacity: disabled || (!children && placeholder) ? 0.5 : 1 | ||
}} | ||
{...props} | ||
> | ||
{children} | ||
{children ? children : placeholder} | ||
</TextTableCell> | ||
{isEditing && ( | ||
<Portal> | ||
<EditableCellField | ||
getTargetRef={() => this.mainRef} | ||
value={children} | ||
onEscape={this.handleFieldEscape} | ||
onBlur={this.handleFieldBlur} | ||
onCancel={this.handleFieldCancel} | ||
size={size} | ||
/> | ||
<Stack> | ||
{zIndex => ( | ||
<EditableCellField | ||
zIndex={zIndex} | ||
getTargetRef={() => this.mainRef} | ||
value={value} | ||
onEscape={this.handleFieldEscape} | ||
onBlur={this.handleFieldBlur} | ||
onCancel={this.handleFieldCancel} | ||
size={size} | ||
/> | ||
)} | ||
</Stack> | ||
</Portal> | ||
)} | ||
</React.Fragment> | ||
|
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.
You should check that this has been passed in and is a function first, since it's not marked as required nor does it have a default prop value: