diff --git a/src/table/src/EditableCell.js b/src/table/src/EditableCell.js index e9ab3420d..bdc37e83d 100644 --- a/src/table/src/EditableCell.js +++ b/src/table/src/EditableCell.js @@ -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,7 +38,7 @@ 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. @@ -30,11 +47,20 @@ class EditableCell extends React.PureComponent { } 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 } @@ -47,13 +73,21 @@ class EditableCell extends React.PureComponent { } handleDoubleClick = () => { + if (this.props.disabled || !this.props.isSelectable) return + 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) { this.setState({ isEditing: true, @@ -67,20 +101,21 @@ class EditableCell extends React.PureComponent { } handleFieldBlur = value => { - const { onChange } = this.props + const { onChange, isSelectable } = this.props const currentValue = this.state.value this.setState({ isEditing: false, - // Make edit instantious. Deal with errors up the tree. value }) - if (this.mainRef) this.mainRef.focus() - if (currentValue !== value && typeof onChange === 'function') { onChange(value) } + + if (this.mainRef && isSelectable) { + this.mainRef.focus() + } } handleFieldCancel = () => { @@ -92,36 +127,50 @@ class EditableCell extends React.PureComponent { } render() { - const { children, theme, size, ...props } = this.props - const { isEditing } = this.state + const { + children, + theme, + size, + disabled, + placeholder, + isSelectable, + ...props + } = this.props + const { isEditing, value } = this.state return ( - {children} + {children ? children : placeholder} {isEditing && ( - this.mainRef} - value={children} - onEscape={this.handleFieldEscape} - onBlur={this.handleFieldBlur} - onCancel={this.handleFieldCancel} - size={size} - /> + + {zIndex => ( + this.mainRef} + value={value} + onEscape={this.handleFieldEscape} + onBlur={this.handleFieldBlur} + onCancel={this.handleFieldCancel} + size={size} + /> + )} + )} diff --git a/src/table/src/EditableCellField.js b/src/table/src/EditableCellField.js index 099125846..b75d88e92 100644 --- a/src/table/src/EditableCellField.js +++ b/src/table/src/EditableCellField.js @@ -9,6 +9,11 @@ export default class EditableCellField extends React.PureComponent { */ value: PropTypes.string.isRequired, + /** + * The z-index placed on the element. + */ + zIndex: PropTypes.number.isRequired, + /** * Function to get the target ref of the parent. * Used to mirror the position. @@ -71,7 +76,11 @@ export default class EditableCellField extends React.PureComponent { if (isTableBody) { return ref } - ref = ref.parentElement + if (ref.parentElement) { + ref = ref.parentElement + } else { + return null + } } this.tableBodyRef = ref @@ -84,15 +93,26 @@ export default class EditableCellField extends React.PureComponent { if (!targetRef) return const tableBodyRef = this.getTableBodyRef(targetRef) - const bounds = tableBodyRef.getBoundingClientRect() - - const { left, top, height, width } = targetRef.getBoundingClientRect() + const { + left, + top: targetTop, + height, + width + } = targetRef.getBoundingClientRect() + + let top + if (tableBodyRef) { + const bounds = tableBodyRef.getBoundingClientRect() + top = Math.min(Math.max(targetTop, bounds.top), bounds.bottom - height) + } else { + top = targetTop + } this.setState( () => { return { left, - top: Math.min(Math.max(top, bounds.top), bounds.bottom - height), + top, height, width } @@ -117,11 +137,12 @@ export default class EditableCellField extends React.PureComponent { const { key } = e if (key === 'Escape' || key === 'Enter') { this.textareaRef.blur() + e.preventDefault() } } render() { - const { size, value, minWidth, minHeight } = this.props + const { size, value, minWidth, minHeight, zIndex } = this.props const { left, top, height, width } = this.state return ( @@ -137,7 +158,8 @@ export default class EditableCellField extends React.PureComponent { height, minHeight: Math.max(height, minHeight), width, - minWidth: Math.max(width, minWidth) + minWidth: Math.max(width, minWidth), + zIndex }} height={null} width={null} diff --git a/src/table/src/SelectMenuCell.js b/src/table/src/SelectMenuCell.js index c688ffe05..a9dc3ed42 100644 --- a/src/table/src/SelectMenuCell.js +++ b/src/table/src/SelectMenuCell.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import debounce from 'lodash.debounce' import { withTheme } from '../../theme' import { SelectMenu } from '../../select-menu' +import { Icon } from '../../icon' import TextTableCell from './TextTableCell' import TableCell from './TableCell' @@ -94,6 +95,7 @@ class SelectMenuCell extends React.PureComponent { } aria-haspopup aria-expanded={isShown} size={size} diff --git a/src/table/src/TableCell.js b/src/table/src/TableCell.js index 84fc8ec15..e4892230d 100644 --- a/src/table/src/TableCell.js +++ b/src/table/src/TableCell.js @@ -25,6 +25,12 @@ class TableCell extends PureComponent { */ appearance: PropTypes.string.isRequired, + /** + * Optional node to be placed on the right side of the table cell. + * Useful for icons and icon buttons. + */ + rightView: PropTypes.node, + /** * Theme provided by ThemeProvider. */ @@ -51,6 +57,7 @@ class TableCell extends PureComponent { flex: 1, display: 'flex', alignItems: 'center', + flexShrink: 0, overflow: 'hidden' } @@ -101,6 +108,7 @@ class TableCell extends PureComponent { isSelectable, tabIndex = -1, className, + rightView, ...props } = this.props @@ -122,6 +130,7 @@ class TableCell extends PureComponent { {...props} > {children} + {rightView ? rightView : null} ) }} diff --git a/src/table/src/TextTableCell.js b/src/table/src/TextTableCell.js index 3dc35552c..7ca2b4c75 100644 --- a/src/table/src/TextTableCell.js +++ b/src/table/src/TextTableCell.js @@ -32,7 +32,7 @@ export default class TextTableCell extends PureComponent { } render() { - const { children, textProps, isNumber, ...props } = this.props + const { children, textProps, isNumber, placeholder, ...props } = this.props return ( diff --git a/src/table/stories/EditableTable.js b/src/table/stories/EditableTable.js index 33cf19836..34815d521 100644 --- a/src/table/stories/EditableTable.js +++ b/src/table/stories/EditableTable.js @@ -1,12 +1,13 @@ import React from 'react' import faker from 'faker' import { Table } from '../' +import { Stack } from '../../stack' import { Pane } from '../../layers' const range = N => Array.from({ length: N }, (v, k) => k + 1) // Generate a bunch of users. -const users = range(1000).map(index => { +const users = range(100).map(index => { const options = range(20).map(i => { const item = faker.commerce.productName() return { @@ -21,6 +22,7 @@ const users = range(1000).map(index => { name: faker.name.findName(), email: faker.internet.email(), options, + notes: '', selected: options[0].value } }) @@ -68,49 +70,99 @@ export default class EditableTable extends React.PureComponent { render() { return ( - - - - - Name - - - Email - - Product - - - {this.state.users.map(user => { - return ( - - + {zIndex => { + // Stack used for testing only. Not neccesary for functionality. + return ( + +
+ + - {user.name} - - - {user.email} - - - {user.selected} - - - ) - })} - -
-
+ Id + + + Name + + + Email + + Product + Notes + + + {this.state.users.map(user => { + return ( + + + {user.id} + + + {user.name} + + + {user.email} + + + {user.selected} + + + {user.notes} + + + ) + })} + + + + ) + }} + ) } } diff --git a/src/table/stories/VirtualTable.js b/src/table/stories/VirtualTable.js index 847895d8f..03d2c22a1 100644 --- a/src/table/stories/VirtualTable.js +++ b/src/table/stories/VirtualTable.js @@ -14,7 +14,8 @@ const randomLengthContent = [ // Generate a bunch of users. const users = range(1000) - .map(() => ({ + .map((user, index) => ({ + id: index, name: faker.name.findName(), email: faker.internet.email(), height: faker.random.arrayElement([32, 40, 56, 'auto']) @@ -44,7 +45,7 @@ export default class VirtualTable extends React.PureComponent { {users.map((user, index) => { return ( - + {index} {user.height} {user.name}