From 32d95a82fd865d171858cec65e097a3ae929d361 Mon Sep 17 00:00:00 2001 From: Mikkel Laursen Date: Mon, 12 Dec 2016 16:51:54 -0700 Subject: [PATCH] Fixed SelectFieldColumn and EditDialogColumn #170 The SelectFieldColumn was rewritten so that is positions itself absolutely a bit more accurately so that it can correctly overflow in data tables. Also updated the SelectFieldColumn and EditDialogColumn so that when they are open, they will reposition themselves if the user scrolls the table left or right. To get a better rendering experience, I had to remove the `position: relative` from the `.md-table-column`, so tooltips no longer work. I will need to fix that before release. --- .../components/ReactMD/data-tables/index.js | 6 + docs/src/shared/constants/navItems.jsx | 8 +- src/js/DataTables/EditDialogColumn.js | 318 ++++++++++++------ src/js/DataTables/SelectFieldColumn.js | 234 ++++++++++--- src/js/DataTables/TablePagination.js | 23 +- src/js/DataTables/findTable.js | 26 ++ src/scss/components/_data-tables.scss | 40 ++- 7 files changed, 466 insertions(+), 189 deletions(-) create mode 100644 src/js/DataTables/findTable.js diff --git a/docs/src/shared/components/ReactMD/data-tables/index.js b/docs/src/shared/components/ReactMD/data-tables/index.js index 14ecb0d8e0..181e23dfc6 100644 --- a/docs/src/shared/components/ReactMD/data-tables/index.js +++ b/docs/src/shared/components/ReactMD/data-tables/index.js @@ -29,6 +29,12 @@ export default [{ description: ` When the \`plain\` prop is not enabled, the \`DataTable\` will inject a checkbox at the start of each row which will allow the user to select that row. + +This example also introduces another component: \`SelectFieldColumn\`. When using the \`SelectFieldColumn\`, +your \`TableColumn\` in the \`TableHeader\` should apply the \`.md-table-column--select-field\` to position +the header with your select field. In addition, it is recommended to specify an exact width or min-width for +your column on your column's header. When the values for the select field are different widths, the column's +size will keep changing. `, code: DataTableExampleRaw, children: , diff --git a/docs/src/shared/constants/navItems.jsx b/docs/src/shared/constants/navItems.jsx index d586d2a4f5..d73723b93f 100644 --- a/docs/src/shared/constants/navItems.jsx +++ b/docs/src/shared/constants/navItems.jsx @@ -66,6 +66,10 @@ function mapToNavItems(route, parents = []) { resolvedComponent = component; } else if (props.href) { resolvedComponent = 'a'; + if (!props.href.match(/sassdoc/)) { + props.rel = 'noopener noreferrer'; + props.target = '_blank'; + } } else if (!nestedItems) { resolvedComponent = Link; } @@ -92,22 +96,18 @@ const routes = internalRoutes.concat([{ divider: true }, { href: 'https://facebook.github.io/react/', avatarProps: { src: reactLogo, alt: 'React Logo' }, primaryText: 'React', - target: '_blank', }, { href: 'https://www.google.com/design/spec/material-design/introduction.html', avatarProps: { src: googleLogo, alt: 'Google Logo', className: 'google-logo' }, primaryText: 'Material Design', - target: '_blank', }, { href: 'https://design.google.com/icons/', avatarProps: { src: googleLogo, alt: 'Google Logo', className: 'google-logo' }, primaryText: 'Material Icons', - target: '_blank', }, { href: 'http://webaim.org/resources/contrastchecker/', icon: 'accessibility', primaryText: 'Contrast Checker', - target: '_blank', }]).map(route => mapToNavItems(route)); function isNestedActive(nestedItems, pathname) { diff --git a/src/js/DataTables/EditDialogColumn.js b/src/js/DataTables/EditDialogColumn.js index a6e8e9a716..15369453d3 100644 --- a/src/js/DataTables/EditDialogColumn.js +++ b/src/js/DataTables/EditDialogColumn.js @@ -11,6 +11,8 @@ import TableColumn from './TableColumn'; import TextField from '../TextFields/TextField'; import FontIcon from '../FontIcons/FontIcon'; +import findTable from './findTable'; + /** * A Text Edit dialog for tables. This can either be a small * version which only has the text field or a large version @@ -47,6 +49,26 @@ export default class EditDialogColumn extends PureComponent { */ dialogClassName: PropTypes.string, + /** + * An optional style to apply to the text field. + */ + textFieldStyle: PropTypes.object, + + /** + * An optional class name to apply to the text field. + */ + textFieldClassName: PropTypes.string, + + /** + * An optional style to apply to the text field's input. + */ + inputStyle: PropTypes.object, + + /** + * An optional class name to apply to the text field's input. + */ + inputClassName: PropTypes.string, + /** * The transition duration when the dialog is moving from * active to inactive. @@ -68,7 +90,10 @@ export default class EditDialogColumn extends PureComponent { * will make the component controlled so you will need * to provide an `onChange` function. */ - value: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), /** * An optional function to call when the text field's value @@ -79,7 +104,10 @@ export default class EditDialogColumn extends PureComponent { /** * The default value for the column. */ - defaultValue: PropTypes.string, + defaultValue: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), /** * An optional function to call when the input is clicked. @@ -206,6 +234,24 @@ export default class EditDialogColumn extends PureComponent { * This is injected by the `TableRow` component. */ header: PropTypes.bool, + + /** + * The type for the text field. + */ + type: TextField.propTypes.type, + + /** + * Boolean if the min width of the dialog should be set to the `$md-edit-dialog-column-min-width` variable. + * If this is undefined, the min width will be enforced when the `type` prop is `text`. + */ + enforceMinWidth: PropTypes.bool, + + /** + * When the dialog is open and a user scrolls the dialog offscreen, this is the amount + * of the dialog that should be offscreen before hiding the dialog (inverse). The default + * is to have 25% of the dialog offscreen. + */ + scrollThreshold: PropTypes.number.isRequired, }; static contextTypes = { @@ -216,12 +262,14 @@ export default class EditDialogColumn extends PureComponent { }; static defaultProps = { + type: 'text', defaultValue: '', transitionDuration: 300, okOnOutsideClick: true, okLabel: 'Save', cancelLabel: 'Cancel', inlineIconChildren: 'edit', + scrollThreshold: 0.75, }; constructor(props, context) { @@ -230,58 +278,40 @@ export default class EditDialogColumn extends PureComponent { this.state = { value: props.defaultValue, active: false, + absolute: false, + animating: false, }; this._setColumn = this._setColumn.bind(this); + this._setDialog = this._setDialog.bind(this); this._setField = this._setField.bind(this); this._setOkButton = this._setOkButton.bind(this); this._save = this._save.bind(this); this._overrideTab = this._overrideTab.bind(this); - this._handleClick = this._handleClick.bind(this); this._handleKeyUp = this._handleKeyUp.bind(this); this._handleKeyDown = this._handleKeyDown.bind(this); this._handleChange = this._handleChange.bind(this); this._handleTouchStart = this._handleTouchStart.bind(this); - this._handleTouchEnd = this._handleTouchEnd.bind(this); this._handleCancelClick = this._handleCancelClick.bind(this); this._handleClickOutside = this._handleClickOutside.bind(this); this._handleMouseOver = this._handleMouseOver.bind(this); this._handleMouseLeave = this._handleMouseLeave.bind(this); + this._positionCell = this._positionCell.bind(this); + this._repositionCell = this._repositionCell.bind(this); + this._activateDialog = this._activateDialog.bind(this); } - componentWillUpdate(nextProps, nextState) { - if (this.state.active === nextState.active) { + componentDidUpdate(prevProps, prevState) { + const { active } = this.state; + if (active === prevState.active) { return; + } else if (this._table) { + this._table[`${active ? 'add' : 'remove'}EventListener`]('scroll', this._repositionCell); + this._left = active ? this.state.left : null; + this._scrollLeft = active ? this._table.scrollLeft : null; } - if (nextState.active) { - // Wait for a re-render cycle before adding the window click event. - // This will be called immediately after being clicked open and close - // the dialog immediately on open. - this._clickTimeout = setTimeout(() => { - this._clickTimeout = null; - window.addEventListener('click', this._handleClickOutside); - }, TICK); - } else { - if (this._clickTimeout) { - clearTimeout(this._clickTimeout); - this._clickTimeout = null; - } - window.removeEventListener('click', this._handleClickOutside); - } - - if (this._timeout) { - clearTimeout(this._timeout); - } - - this._timeout = setTimeout(() => { - if (!nextState.active && this._field) { - this._field.blur(); - } - this._timeout = null; - this.setState({ animating: false }); - }, nextProps.transitionDuration); - this.setState({ animating: true }); + window[`${active ? 'add' : 'remove'}EventListener`]('click', this._handleClickOutside); } componentWillUnmount() { @@ -290,14 +320,26 @@ export default class EditDialogColumn extends PureComponent { if (this._timeout) { clearTimeout(this._timeout); } + } - if (this._clickTimeout) { - clearTimeout(this._clickTimeout); + _getDialogPosition(dialog) { + let left = null; + let width = null; + if (dialog) { + left = dialog.getBoundingClientRect().left - 1; + width = dialog.offsetWidth; } + + return { width, left }; } _setColumn(column) { this._column = findDOMNode(column); + this._table = findTable(this._column); + } + + _setDialog(dialog) { + this._dialog = dialog; } _setField(field) { @@ -310,6 +352,85 @@ export default class EditDialogColumn extends PureComponent { this._okButton = findDOMNode(okButton); } + /** + * This function will absolutely position a cell either on mouse over, keyboard tab focus, + * or touch start. This allows the table cell to expand outside of the table's scroll view. + */ + _positionCell() { + if (this.props.inline) { + return; + } + + let position; + if (!this.state.absolute) { + position = this._getDialogPosition(this._dialog, this._table); + } + + this.setState({ absolute: true, ...position }); + } + + /** + * When the dialog is open and the user scrolls the data table (for some reason), this will + * keep the cell positioned correctly. + */ + _repositionCell() { + if (!this._ticking) { + requestAnimationFrame(() => { + this._ticking = false; + + let left = this._left; + let scrolledOut = false; + if (this._table) { + const { scrollLeft, offsetWidth } = this._table; + left -= (scrollLeft - this._scrollLeft); + scrolledOut = left < 16 || offsetWidth - left < this.state.width * this.props.scrollThreshold; + } + + let { absolute, active } = this.state; + if (!this._timeout && scrolledOut) { + this._timeout = setTimeout(() => { + this._timeout = null; + this.setState({ absolute: false, left: null, width: null }); + }, this.props.transitionDuration); + active = false; + absolute = true; + } + + this.setState({ left, absolute, active }); + }); + } + + this._ticking = true; + } + + /** + * Activates the dialog after it has already been positioned absolutely. This + * is triggered froma click event, a touchend event, or the callback of the `this.setState` + * when coming from a keyboard focus event. + * + * @param {Object=} e - The click or touchend event. + */ + _activateDialog(e) { + if (e) { + let callback; + if (e.type === 'click') { + callback = 'onClick'; + } else if (e.type === 'touchend') { + callback = 'onTouchEnd'; + } + + if (callback && this.props[callback]) { + this.props[callback](e); + } + } + + if (this.props.inline || this.state.active) { + return; + } + + this.setState({ active: true, cancelValue: getField(this.props, this.state, 'value') }); + } + _handleClickOutside(e) { if (this._column && !this._column.contains(e.target)) { if (this.props.onOutsideClick) { @@ -324,18 +445,30 @@ export default class EditDialogColumn extends PureComponent { } } - _handleClick(e) { - if (this.props.onClick) { - this.props.onClick(e); + _handleMouseOver(e) { + if (this.props.onMouseOver) { + this.props.onMouseOver(e); } - if (this.props.inline || this.state.active) { + this._positionCell(); + } + + _handleMouseLeave(e) { + if (this.props.onMouseLeave) { + this.props.onMouseLeave(e); + } + + if (this.props.inline) { return; } - this.setState({ active: true, cancelValue: getField(this.props, this.state, 'value') }); - } + let position; + if (!this.state.active) { + position = { width: null, left: null }; + } + this.setState({ absolute: false, ...position }); + } _handleKeyUp(e) { if (this.props.onKeyUp) { @@ -351,10 +484,9 @@ export default class EditDialogColumn extends PureComponent { // Basically position the edit field absolutely, wait for a re-render, then activate the dialog. this._timeout = setTimeout(() => { this._timeout = null; - this.setState({ active: true, cancelValue: getField(this.props, this.state, 'value') }); + this._activateDialog(); }, TICK); - - this.setState({ absolute: true }); + this._positionCell(); } _handleTouchStart(e) { @@ -362,19 +494,7 @@ export default class EditDialogColumn extends PureComponent { this.props.onTouchStart(e); } - this.setState({ absolute: true }); - } - - _handleTouchEnd(e) { - if (this.props.onTouchEnd) { - this.props.onTouchEnd(e); - } - - if (this.state.active || this.props.inline) { - return; - } - - this.setState({ active: true, cancelValue: getField(this.props, this.state, 'value') }); + this._positionCell(); } _handleKeyDown(e) { @@ -394,7 +514,7 @@ export default class EditDialogColumn extends PureComponent { } _overrideTab(e) { - const { large, inline } = this.props; + const { large, inline, okOnOutsideClick } = this.props; const key = e.which || e.keyCode; if (key !== TAB) { return; @@ -402,10 +522,10 @@ export default class EditDialogColumn extends PureComponent { this._save(e); return; } else if (!large) { - if (getField(this.props, this.state, 'value')) { - e.preventDefault(); + if (okOnOutsideClick) { + this._save(e); } else { - this.setState({ active: false }); + this._handleCancelClick(e); } return; } @@ -431,7 +551,11 @@ export default class EditDialogColumn extends PureComponent { this.props.onOkClick(getField(this.props, this.state, 'value'), e); } - this.setState({ active: false, absolute: false }); + this._timeout = setTimeout(() => { + this._timeout = null; + this.setState({ absolute: false, left: null, width: null }); + }, this.props.transitionDuration); + this.setState({ active: false, absolute: true }); } _handleCancelClick(e) { @@ -439,7 +563,17 @@ export default class EditDialogColumn extends PureComponent { this.props.onCancelClick(this.state.cancelValue, e); } - this.setState({ absolute: false, active: false, value: this.state.cancelValue }); + const state = { absolute: true, active: false }; + if (typeof this.props.value === 'undefined') { + state.value = this.state.cancelValue; + } + + this._timeout = setTimeout(() => { + this._timeout = null; + this.setState({ absolute: false, left: null, width: null }); + }, this.props.transitionDuration); + + this.setState(state); } _handleChange(value, e) { @@ -452,38 +586,18 @@ export default class EditDialogColumn extends PureComponent { } } - _handleMouseOver(e) { - if (this.props.onMouseOver) { - this.props.onMouseOver(e); - } - - if (this.props.inline) { - return; - } - - this.setState({ absolute: true }); - } - - _handleMouseLeave(e) { - if (this.props.onMouseLeave) { - this.props.onMouseLeave(e); - } - - if (this.props.inline) { - return; - } - - this.setState({ absolute: false }); - } - render() { const { rowId } = this.context; - const { active, absolute, animating } = this.state; + const { active, absolute, animating, left, width } = this.state; const { style, className, dialogStyle, dialogClassName, + textFieldStyle, + textFieldClassName, + inputStyle, + inputClassName, maxLength, title, okLabel, @@ -496,6 +610,7 @@ export default class EditDialogColumn extends PureComponent { inlineIconClassName, noIcon, header, + enforceMinWidth, ...props } = this.props; @@ -511,6 +626,7 @@ export default class EditDialogColumn extends PureComponent { delete props.header; delete props.okOnOutsideClick; delete props.transitionDuration; + delete props.scrollThreshold; const value = getField(this.props, this.state, 'value'); let { id } = this.props; @@ -560,25 +676,29 @@ export default class EditDialogColumn extends PureComponent { return (
@@ -593,8 +713,12 @@ export default class EditDialogColumn extends PureComponent { placeholder={active ? placeholder : placeholder || label} block={!active} paddedBlock={false} - className={pointer} - inputClassName={pointer} + style={textFieldStyle} + className={cn(pointer, textFieldClassName)} + inputStyle={inputStyle} + inputClassName={cn(pointer, { + 'md-text-right': props.type === 'number', + }, inputClassName)} onKeyUp={this._handleKeyUp} onKeyDown={this._handleKeyDown} value={value} diff --git a/src/js/DataTables/SelectFieldColumn.js b/src/js/DataTables/SelectFieldColumn.js index bf3e3981c7..95eb8dcf00 100644 --- a/src/js/DataTables/SelectFieldColumn.js +++ b/src/js/DataTables/SelectFieldColumn.js @@ -1,104 +1,240 @@ import React, { PureComponent, PropTypes } from 'react'; import cn from 'classnames'; -import TableColumn from './TableColumn'; import SelectField from '../SelectFields/SelectField'; - -// This is really half the time of the default menu transition. It seemed to be reasonable -// enough -const ABSOLUTE_DELAY = 100; - -const styles = { - absolute: { position: 'absolute' }, -}; +import TableColumn from './TableColumn'; +import findTable from './findTable'; /** - * The `SelectFieldColumns` is a simple wrapper for the `SelectField` and `TableColumn` - * components. The only purpose of this Component is to allow the select field's menu - * to extend past the data table's bounds. + * The `SelectFieldColumn` component is used to render select fields in `DataTable`s. + * The only reason this component is required is to that the select field will not + * take the overflow of the `DataTable` into consideration when rendering itself. Without + * this component, the menu items would be hidden by the data table's overflow. * - * All props are just passed to the `SelectField` inside. To view all other undocumented - * props here, view [Select Field Documentation](/components/select-fields#prop-types-select-field) + * All props that are on the `SelectField` are also available here (except the naming of style or className). + * See the [SelectField](/components/select-fields?tab=1#select-field-proptypes) for remaining prop descriptions. */ export default class SelectFieldColumn extends PureComponent { static propTypes = { /** - * Boolean if the select field is open by default. + * An optional id to use for the select field in the column. If this is omitted, + * the id will be generated from the `baseId` from the `DataTable`. */ - defaultOpen: PropTypes.bool.isRequired, + id: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), /** - * An optional function to call when the select field's menu open state - * is toggled. + * An optional style to apply to the `TableColumn`. */ - onMenuToggle: PropTypes.func, + style: PropTypes.object, /** - * An optional style to apply to the column. + * An optional className to apply to the `TableColumn`. */ - style: PropTypes.object, + className: PropTypes.string, /** - * An optional className to apply to the column. + * An optional style to apply to the select field's wrapper in the column. */ - className: PropTypes.string, + wrapperStyle: PropTypes.object, + + /** + * An optional className to apply to the select field's wrapper in the column. + */ + wrapperClassName: PropTypes.string, /** - * An optional style to apply to the select field's menu in the table column. + * An optional style to apply to the select field's menu component. */ menuStyle: PropTypes.object, /** - * An optional class name to apply to the select field's menu in the table column. + * An optional className to apply to the select field's menu component. */ menuClassName: PropTypes.string, + + /** + * An optional style to apply to the select field's input. + */ + inputStyle: PropTypes.object, + + /** + * An optional className to apply to the select field's input. + */ + inputClassName: PropTypes.string, + + /** + * Boolean if the `SelectFieldColumn` is in the `TableHeader` component. This is + * injected from the `TableRow` component. Should not be used. + */ + header: PropTypes.bool, + + /** + * An optional function to call when the select field's menu is toggled open. + * See the select field component for the callback information. + */ + onMenuToggle: PropTypes.func, + + /** + * The position of the select field. It is ideal to keep this as the default. + */ + position: SelectField.propTypes.position, + + /** + * Boolean if the select field is open by default. + */ + defaultOpen: PropTypes.bool, + + /** + * When the dialog is open and a user scrolls the dialog offscreen, this is the amount + * of the dialog that should be offscreen before hiding the dialog (inverse). The default + * is to have 25% of the dialog offscreen. + */ + scrollThreshold: PropTypes.number.isRequired, }; static defaultProps = { - defaultOpen: false, + position: SelectField.Positions.BELOW, + scrollThreshold: 0.75, }; - constructor(props) { - super(props); + static contextTypes = { + rowId: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + } + + constructor(props, context) { + super(props, context); + + this.state = { + active: !!props.defaultOpen, + left: null, + width: null, + }; - this.state = { absolute: props.defaultOpen }; + this._wrapper = null; + this._table = null; + this._left = null; + this._scrollLeft = null; + + this._setWrapper = this._setWrapper.bind(this); + this._repositionCell = this._repositionCell.bind(this); this._handleMenuToggle = this._handleMenuToggle.bind(this); } + componentDidUpdate(prevProps, prevState) { + const { active } = this.state; + if (active === prevState.active) { + return; + } else if (this._table) { + this._table[`${active ? 'add' : 'remove'}EventListener`]('scroll', this._repositionCell); + this._left = active ? this.state.left : null; + this._scrollLeft = active ? this._table.scrollLeft : null; + } + } + componentWillUnmount() { - if (this._timeout) { - clearTimeout(this._timeout); + if (this.table && this.state.active) { + this._table.removeEventListener('scroll', this._repositionCell); } } - _handleMenuToggle(open, event) { + _setWrapper(wrapper) { + this._wrapper = wrapper; + this._table = findTable(this._wrapper); + } + + /** + * When the dialog is open and the user scrolls the data table (for some reason), this will + * keep the cell positioned correctly. + */ + _repositionCell() { + if (!this._ticking) { + requestAnimationFrame(() => { + this._ticking = false; + + let left = this._left; + let scrolledOut = false; + if (this._table) { + const { scrollLeft, offsetWidth } = this._table; + left -= (scrollLeft - this._scrollLeft); + scrolledOut = left < 16 || offsetWidth - left < this.state.width * this.props.scrollThreshold; + } + + let { active } = this.state; + if (!this._timeout && scrolledOut) { + active = false; + } + + this.setState({ left, active }); + }); + } + + this._ticking = true; + } + + _handleMenuToggle(active, e) { if (this.props.onMenuToggle) { - this.props.onMenuToggle(open, event); + this.props.onMenuToggle(active, e); } - this._timeout = setTimeout(() => { - this._timeout = null; - this.setState({ absolute: open }); - }, ABSOLUTE_DELAY); + let width = null; + let left = null; + if (this._wrapper && active) { + left = this._wrapper.getBoundingClientRect().left - 1; // 1px for box shadow + width = this._wrapper.offsetWidth; + } + + this.setState({ active, width, left }); } render() { - const { absolute } = this.state; - const { style, className, menuStyle, menuClassName, ...props } = this.props; - delete props.header; + const { rowId } = this.context; + const { active, width, left } = this.state; + const { + style, + className, + menuStyle, + menuClassName, + wrapperStyle, + wrapperClassName, + header, + ...props + } = this.props; + delete props.id; + delete props.scrollThreshold; + + let { id } = this.props; + if (!id) { + id = `${rowId}-select`; + } return ( - +
+ +
); } diff --git a/src/js/DataTables/TablePagination.js b/src/js/DataTables/TablePagination.js index 1230a57446..e85846c3b0 100644 --- a/src/js/DataTables/TablePagination.js +++ b/src/js/DataTables/TablePagination.js @@ -5,6 +5,7 @@ import cn from 'classnames'; import getField from '../utils/getField'; import SelectField from '../SelectFields/SelectField'; import Button from '../Buttons/Button'; +import findTable from './findTable'; /** * The `TablePagination` component is used to generate the table footer that helps @@ -153,28 +154,8 @@ export default class TablePagination extends PureComponent { this._controls = findDOMNode(controls); } - _findTable(el) { - let table; - let node = el; - while (node && node.parentNode) { - if (node.classList && node.classList.contains('md-data-table')) { - // Attempt to check one more element up to see if there is a table-container - // for responsive tables. - table = node; - } else if (node.classList && node.classList.contains('md-data-table--responsive')) { - return node; - } else if (table) { - return table; - } - - node = node.parentNode; - } - - return null; - } - _position() { - const table = this._findTable(findDOMNode(this)); + const table = findTable(findDOMNode(this)); if (table) { this.setState({ controlsMarginLeft: Math.max(0, table.offsetWidth - this._controls.offsetWidth), diff --git a/src/js/DataTables/findTable.js b/src/js/DataTables/findTable.js new file mode 100644 index 0000000000..b92fdc0319 --- /dev/null +++ b/src/js/DataTables/findTable.js @@ -0,0 +1,26 @@ +/** + * Attempts fo find the base table component from an element in the table. + * This will either be the wrapper for responsive data tables, or the table element. + * + * @param {Object} el - The element to traverse from + * @param {Object} the table or null. + */ +export default function findTable(el) { + let table; + let node = el; + while (node && node.parentNode) { + if (node.classList && node.classList.contains('md-data-table')) { + // Attempt to check one more element up to see if there is a table-container + // for responsive tables. + table = node; + } else if (node.classList && node.classList.contains('md-data-table--responsive')) { + return node; + } else if (table) { + return table; + } + + node = node.parentNode; + } + + return null; +} diff --git a/src/scss/components/_data-tables.scss b/src/scss/components/_data-tables.scss index 425bfd5cec..8bf5e0a1b0 100644 --- a/src/scss/components/_data-tables.scss +++ b/src/scss/components/_data-tables.scss @@ -147,6 +147,10 @@ $md-data-table-card-header-font-size: 16px !default; @include react-md-data-table-rows($light-theme); @include react-md-data-table-column; + @if $include-select-fields or $include-edit-dialog { + @include react-md-data-table-fixed-columns; + } + @if $include-checkboxes { @include react-md-data-table-checkboxes; } @@ -277,7 +281,6 @@ $md-data-table-card-header-font-size: 16px !default; @mixin react-md-data-table-column { .md-table-column { padding-right: $md-data-table-padding; - position: relative; white-space: nowrap; &:first-child { @@ -326,25 +329,20 @@ $md-data-table-card-header-font-size: 16px !default; @mixin react-md-data-table-edit-dialogs { .md-edit-dialog-column { padding-right: 0; - - &--animating { - position: absolute; - } - - &--active { - z-index: 1; - } } .md-edit-dialog { left: 0; min-height: $md-data-table-column-height; - min-width: $md-edit-dialog-min-width; padding-right: $md-data-table-padding; top: 0; transition-duration: .3s; transition-property: background, box-shadow, padding; + &--min-width { + min-width: $md-edit-dialog-min-width; + } + &--active { @include md-box-shadow(1); @@ -385,8 +383,10 @@ $md-data-table-card-header-font-size: 16px !default; padding-left: $md-select-field-left-padding; } - .md-data-table .md-select-field--btn { - height: $md-data-table-column-height; + .md-select-field-column { + .md-select-field--btn { + height: $md-data-table-column-height; + } } } @@ -476,6 +476,16 @@ $md-data-table-card-header-font-size: 16px !default; } } +@mixin react-md-data-table-fixed-columns { + .md-table-column--fixed { + position: absolute; + } + + .md-table-column--fixed-active { + z-index: 1; + } +} + /// Creates the media queries and styles for data tables. If the edit dialog column is not included, nothing /// will be created. /// @@ -491,11 +501,5 @@ $md-data-table-card-header-font-size: 16px !default; padding-top: 14.5px; } } - - @media #{$md-mobile-media} and (max-width: #{$md-tablet-min-width - 1}) { - .md-edit-dialog-column--animating { - right: $md-data-table-edit-dialog-mobile-right; - } - } } }