diff --git a/packages/grid/_modules_/grid/GridComponent.tsx b/packages/grid/_modules_/grid/GridComponent.tsx index 531b1c356f269..fb2abbf6fbda8 100644 --- a/packages/grid/_modules_/grid/GridComponent.tsx +++ b/packages/grid/_modules_/grid/GridComponent.tsx @@ -9,7 +9,7 @@ import { useSelection, useSorting, } from './hooks/features'; -import { DEFAULT_GRID_OPTIONS, ElementSize, RootContainerRef } from './models'; +import { ElementSize, RootContainerRef } from './models'; import { COMPONENT_ERROR, DATA_CONTAINER_CSS_CLASS } from './constants'; import { GridRoot } from './components/styled-wrappers/GridRoot'; import { GridDataContainer } from './components/styled-wrappers/GridDataContainer'; @@ -17,7 +17,7 @@ import { GridColumnsContainer } from './components/styled-wrappers/GridColumnsCo import { useVirtualRows } from './hooks/virtualization'; import { ApiContext, - AutoSizerWrapper, + AutoSizer, ColumnsHeader, DefaultFooter, OptionsContext, @@ -43,11 +43,8 @@ export const GridComponent = React.forwardRef(null); const handleRef = useForkRef(rootContainerRef, ref); @@ -66,7 +63,7 @@ export const GridComponent = React.forwardRef { // We are handling error here, to set up the handler as early as possible and be able to catch error thrown at init time. @@ -119,6 +116,19 @@ export const GridComponent = React.forwardRef { + if (size.height === 0) { + gridLogger.warn( + [ + 'The parent of the grid has an empty height.', + 'You need to make sure the container has an intrinsic height.', + 'The grid displays with a height of 0px.', + '', + 'You can find a solution in the docs:', + 'https://material-ui.com/components/data-grid/rendering/#layout', + ].join('\n'), + ); + } + gridLogger.info('resized...', size); apiRef!.current.resize(); }, @@ -161,7 +171,7 @@ export const GridComponent = React.forwardRef + {(size: any) => ( )} - + ); }); diff --git a/packages/grid/_modules_/grid/components/AutoSizer.tsx b/packages/grid/_modules_/grid/components/AutoSizer.tsx new file mode 100644 index 0000000000000..aa69768153d3f --- /dev/null +++ b/packages/grid/_modules_/grid/components/AutoSizer.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { useForkRef, ownerWindow, useEventCallback } from '@material-ui/core/utils'; +import createDetectElementResize from '../lib/createDetectElementResize'; +// TODO replace with https://caniuse.com/resizeobserver. + +export interface AutoSizerSize { + height: number; + width: number; +} + +// Credit to https://github.com/bvaughn/react-virtualized/blob/master/source/AutoSizer/AutoSizer.js +// for the sources. + +export interface AutoSizerProps extends Omit, 'children'> { + /** + * Function responsible for rendering children. + */ + children: (size: AutoSizerSize) => React.ReactNode; + /** + * Default height to use for initial render; useful for SSR. + * @default 0 + */ + defaultHeight?: number; + /** + * Default width to use for initial render; useful for SSR. + * @default 0 + */ + defaultWidth?: number; + /** + * If `true`, disable dynamic :height property. + * @default false + */ + disableHeight?: boolean; + /** + * If `true`, disable dynamic :width property. + * @default false + */ + disableWidth?: boolean; + /** + * Nonce of the inlined stylesheet for Content Security Policy. + */ + nonce?: string; + /** + * Callback to be invoked on-resize. + */ + onResize?: (size: AutoSizerSize) => void; +} + +// TODO v5: replace with @material-ui/core/utils/useEnhancedEffect. +const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; + +export const AutoSizer = React.forwardRef(function AutoSizer( + props, + ref, +) { + const { + children, + defaultHeight = 0, + defaultWidth = 0, + disableHeight = false, + disableWidth = false, + nonce, + onResize, + style, + ...other + } = props; + + const [state, setState] = React.useState({ + height: defaultHeight, + width: defaultWidth, + }); + + const rootRef = React.useRef(null); + const parentElement = React.useRef(null) as React.MutableRefObject; + + const handleResize = useEventCallback(() => { + // Guard against AutoSizer component being removed from the DOM immediately after being added. + // This can result in invalid style values which can result in NaN values if we don't handle them. + // See issue #150 for more context. + if (parentElement.current) { + const height = parentElement.current.offsetHeight || 0; + const width = parentElement.current.offsetWidth || 0; + + const win = ownerWindow(parentElement.current); + const computedStyle = win.getComputedStyle(parentElement.current); + const paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0; + const paddingRight = parseInt(computedStyle.paddingRight, 10) || 0; + const paddingTop = parseInt(computedStyle.paddingTop, 10) || 0; + const paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0; + + const newHeight = height - paddingTop - paddingBottom; + const newWidth = width - paddingLeft - paddingRight; + + if ( + (!disableHeight && state.height !== newHeight) || + (!disableWidth && state.width !== newWidth) + ) { + setState({ + height: height - paddingTop - paddingBottom, + width: width - paddingLeft - paddingRight, + }); + + if (onResize) { + onResize({ height, width }); + } + } + } + }); + + useEnhancedEffect(() => { + parentElement.current = rootRef.current!.parentElement; + + if (!parentElement) { + return undefined; + } + + const win = ownerWindow(parentElement.current ?? undefined); + + const detectElementResize = createDetectElementResize(nonce, win); + detectElementResize.addResizeListener(parentElement.current, handleResize); + // @ts-expect-error fixed in v5 + handleResize(); + + return () => { + detectElementResize.removeResizeListener(parentElement.current, handleResize); + }; + }, [nonce, handleResize]); + + // Outer div should not force width/height since that may prevent containers from shrinking. + // Inner component should overflow and use calculated width/height. + // See issue #68 for more information. + const outerStyle: any = { overflow: 'visible' }; + const childParams: any = {}; + + if (!disableHeight) { + outerStyle.height = 0; + childParams.height = state.height; + } + + if (!disableWidth) { + outerStyle.width = 0; + childParams.width = state.width; + } + + const handleRef = useForkRef(rootRef, ref); + + return ( +
+ {state.height === 0 && state.width === 0 ? null : children(childParams)} +
+ ); +}); diff --git a/packages/grid/_modules_/grid/components/autosizer.tsx b/packages/grid/_modules_/grid/components/autosizer.tsx deleted file mode 100644 index 7391028e35981..0000000000000 --- a/packages/grid/_modules_/grid/components/autosizer.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import AutoSizer from '../lib/autosizer'; - -export const AutoSizerWrapper = AutoSizer; diff --git a/packages/grid/_modules_/grid/components/default-footer.tsx b/packages/grid/_modules_/grid/components/default-footer.tsx index a8e722286a505..635a1c88734ea 100644 --- a/packages/grid/_modules_/grid/components/default-footer.tsx +++ b/packages/grid/_modules_/grid/components/default-footer.tsx @@ -12,17 +12,16 @@ export interface DefaultFooterProps { } export const DefaultFooter = React.forwardRef( - function DefaultFooter({ options, rowCount, paginationComponent }, ref) { - const api = React.useContext(ApiContext); + function DefaultFooter(props, ref) { + const { options, rowCount, paginationComponent } = props; + const apiRef = React.useContext(ApiContext); const [selectedRowCount, setSelectedCount] = React.useState(0); React.useEffect(() => { - if (api && api.current) { - return api.current!.onSelectionChange(({ rows }) => setSelectedCount(rows.length)); - } - - return undefined; - }, [api]); + return apiRef!.current.onSelectionChange(({ rows }) => { + setSelectedCount(rows.length); + }); + }, [apiRef]); if (options.hideFooter) { return null; diff --git a/packages/grid/_modules_/grid/components/index.ts b/packages/grid/_modules_/grid/components/index.ts index 56dc2c6d7fde3..02b606caa6b34 100644 --- a/packages/grid/_modules_/grid/components/index.ts +++ b/packages/grid/_modules_/grid/components/index.ts @@ -1,5 +1,5 @@ export * from './api-context'; -export * from './autosizer'; +export * from './AutoSizer'; export * from './cell'; export * from './checkbox-renderer'; export * from './column-header-item'; diff --git a/packages/grid/_modules_/grid/hooks/root/useApi.ts b/packages/grid/_modules_/grid/hooks/root/useApi.ts index 5bfdf64c711a3..5db2239404b34 100644 --- a/packages/grid/_modules_/grid/hooks/root/useApi.ts +++ b/packages/grid/_modules_/grid/hooks/root/useApi.ts @@ -1,14 +1,10 @@ import * as React from 'react'; import { useLogger } from '../utils/useLogger'; import { COMPONENT_ERROR, UNMOUNT } from '../../constants/eventsConstants'; -import { GridOptions, ApiRef } from '../../models'; +import { ApiRef } from '../../models'; import { useApiMethod } from './useApiMethod'; -export function useApi( - gridRootRef: React.RefObject, - options: GridOptions, - apiRef: ApiRef, -): boolean { +export function useApi(gridRootRef: React.RefObject, apiRef: ApiRef): boolean { const [initialised, setInit] = React.useState(false); const logger = useLogger('useApi'); diff --git a/packages/grid/_modules_/grid/hooks/utils/useLogger.ts b/packages/grid/_modules_/grid/hooks/utils/useLogger.ts index 616794cf6ec70..89881c685f16f 100644 --- a/packages/grid/_modules_/grid/hooks/utils/useLogger.ts +++ b/packages/grid/_modules_/grid/hooks/utils/useLogger.ts @@ -46,7 +46,8 @@ function getAppender(name: string, logLevel: string, appender: Logger = console) if (idx >= minLogLevelIdx) { loggerObj[method] = (...args: any[]) => { const [message, ...rest] = args; - (appender as any)[method](`[${name}] - ${message}`, ...rest); + + (appender as any)[method](`${name}: ${message}`, ...rest); }; } else { loggerObj[method] = noop; @@ -68,7 +69,7 @@ let factory: LoggerFactoryFn | null; export function useLoggerFactory( customLogger?: Logger | LoggerFactoryFn, - logLevel: string | boolean = 'debug', + logLevel: string | boolean = process.env.NODE_ENV === 'production' ? 'error' : 'warn', ) { if (forceDebug) { factory = defaultFactory('debug'); diff --git a/packages/grid/_modules_/grid/hooks/virtualization/useVirtualRows.ts b/packages/grid/_modules_/grid/hooks/virtualization/useVirtualRows.ts index fdf469601cf52..3b2e6d0d3deec 100644 --- a/packages/grid/_modules_/grid/hooks/virtualization/useVirtualRows.ts +++ b/packages/grid/_modules_/grid/hooks/virtualization/useVirtualRows.ts @@ -28,10 +28,7 @@ import { useApiEventHandler } from '../root/useApiEventHandler'; type UseVirtualRowsReturnType = Partial | null; // TODO v5: replace with @material-ui/core/utils/useEnhancedEffect. -const useEnhancedEffect = - typeof window !== 'undefined' && process.env.NODE_ENV !== 'test' - ? React.useLayoutEffect - : React.useEffect; +const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; export const useVirtualRows = ( colRef: React.MutableRefObject, diff --git a/packages/grid/_modules_/grid/lib/autosizer/types.d.ts b/packages/grid/_modules_/grid/lib/autosizer/types.d.ts deleted file mode 100644 index faed519745eb9..0000000000000 --- a/packages/grid/_modules_/grid/lib/autosizer/types.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Type definitions for react-virtualized-auto-sizer 1.0 -// Project: https://github.com/bvaughn/react-virtualized-auto-sizer/ -// Definitions by: Hidemi Yukita -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 -import * as React from 'react'; - -export interface Size { - height: number; - width: number; -} - -export interface AutoSizerProps { - /** - * Function responsible for rendering children. - */ - children: (size: Size) => React.ReactNode; - /** - * Optional custom CSS class name to attach to root AutoSizer element. - */ - className?: string; - /** - * Default height to use for initial render; useful for SSR. - */ - defaultHeight?: number; - /** - * Default width to use for initial render; useful for SSR. - */ - defaultWidth?: number; - /** - * Disable dynamic :height property. - */ - disableHeight?: boolean; - /** - * Disable dynamic :width property. - */ - disableWidth?: boolean; - /** - * Nonce of the inlined stylesheet for Content Security Policy. - */ - nonce?: string; - /** - * Callback to be invoked on-resize. - */ - onResize?: (size: Size) => void; - /** - * Optional inline style. - */ - style?: React.CSSProperties; -} - -export default class extends React.Component {} diff --git a/packages/grid/_modules_/grid/lib/autosizer/index.js b/packages/grid/_modules_/grid/lib/createDetectElementResize/index.js similarity index 50% rename from packages/grid/_modules_/grid/lib/autosizer/index.js rename to packages/grid/_modules_/grid/lib/createDetectElementResize/index.js index 6f14ea8014521..93d6b7d70828f 100644 --- a/packages/grid/_modules_/grid/lib/autosizer/index.js +++ b/packages/grid/_modules_/grid/lib/createDetectElementResize/index.js @@ -11,17 +11,9 @@ import * as React from 'react'; * 3) Avoid unnecessary reflows by not measuring size for scroll events bubbling from children. * 4) Add nonce for style element. **/ - -function createDetectElementResize(nonce) { +export default function createDetectElementResize(nonce, hostWindow) { // Check `document` and `window` in case of server-side rendering - var _window; - if (typeof window !== 'undefined') { - _window = window; - } else if (typeof self !== 'undefined') { - _window = self; - } else { - _window = global; - } + var _window = hostWindow; var attachEvent = typeof document !== 'undefined' && document.attachEvent; @@ -194,208 +186,3 @@ function createDetectElementResize(nonce) { removeResizeListener: removeResizeListener }; } - -var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -}; - -var createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; -}(); - -var _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; -}; - -var inherits = function (subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - enumerable: false, - writable: true, - configurable: true - } - }); - if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; -}; - -var possibleConstructorReturn = function (self, call) { - if (!self) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return call && (typeof call === "object" || typeof call === "function") ? call : self; -}; - -var AutoSizer = function (_React$PureComponent) { - inherits(AutoSizer, _React$PureComponent); - - function AutoSizer() { - var _ref; - - var _temp, _this, _ret; - - classCallCheck(this, AutoSizer); - - for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - return _ret = (_temp = (_this = possibleConstructorReturn(this, (_ref = AutoSizer.__proto__ || Object.getPrototypeOf(AutoSizer)).call.apply(_ref, [this].concat(args))), _this), _this.state = { - height: _this.props.defaultHeight || 0, - width: _this.props.defaultWidth || 0 - }, _this._onResize = function () { - var _this$props = _this.props, - disableHeight = _this$props.disableHeight, - disableWidth = _this$props.disableWidth, - onResize = _this$props.onResize; - - - if (_this._parentNode) { - // Guard against AutoSizer component being removed from the DOM immediately after being added. - // This can result in invalid style values which can result in NaN values if we don't handle them. - // See issue #150 for more context. - - var _height = _this._parentNode.offsetHeight || 0; - var _width = _this._parentNode.offsetWidth || 0; - - var _style = window.getComputedStyle(_this._parentNode) || {}; - var paddingLeft = parseInt(_style.paddingLeft, 10) || 0; - var paddingRight = parseInt(_style.paddingRight, 10) || 0; - var paddingTop = parseInt(_style.paddingTop, 10) || 0; - var paddingBottom = parseInt(_style.paddingBottom, 10) || 0; - - var newHeight = _height - paddingTop - paddingBottom; - var newWidth = _width - paddingLeft - paddingRight; - - if (!disableHeight && _this.state.height !== newHeight || !disableWidth && _this.state.width !== newWidth) { - _this.setState({ - height: _height - paddingTop - paddingBottom, - width: _width - paddingLeft - paddingRight - }); - - onResize({ height: _height, width: _width }); - } - } - }, _this._setRef = function (autoSizer) { - _this._autoSizer = autoSizer; - }, _temp), possibleConstructorReturn(_this, _ret); - } - - createClass(AutoSizer, [{ - key: 'componentDidMount', - value: function componentDidMount() { - var nonce = this.props.nonce; - - if (this._autoSizer && this._autoSizer.parentNode && this._autoSizer.parentNode.ownerDocument && this._autoSizer.parentNode.ownerDocument.defaultView && this._autoSizer.parentNode instanceof this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement) { - // Delay access of parentNode until mount. - // This handles edge-cases where the component has already been unmounted before its ref has been set, - // As well as libraries like react-lite which have a slightly different lifecycle. - this._parentNode = this._autoSizer.parentNode; - - // Defer requiring resize handler in order to support server-side rendering. - // See issue #41 - this._detectElementResize = createDetectElementResize(nonce); - this._detectElementResize.addResizeListener(this._parentNode, this._onResize); - - this._onResize(); - } - } - }, { - key: 'componentWillUnmount', - value: function componentWillUnmount() { - if (this._detectElementResize && this._parentNode) { - this._detectElementResize.removeResizeListener(this._parentNode, this._onResize); - } - } - }, { - key: 'render', - value: function render() { - var _props = this.props, - children = _props.children, - className = _props.className, - disableHeight = _props.disableHeight, - disableWidth = _props.disableWidth, - style = _props.style; - var _state = this.state, - height = _state.height, - width = _state.width; - - // Outer div should not force width/height since that may prevent containers from shrinking. - // Inner component should overflow and use calculated width/height. - // See issue #68 for more information. - - var outerStyle = { overflow: 'visible' }; - var childParams = {}; - - // Avoid rendering children before the initial measurements have been collected. - // At best this would just be wasting cycles. - var bailoutOnChildren = false; - - if (!disableHeight) { - if (height === 0) { - bailoutOnChildren = true; - } - outerStyle.height = 0; - childParams.height = height; - } - - if (!disableWidth) { - if (width === 0) { - bailoutOnChildren = true; - } - outerStyle.width = 0; - childParams.width = width; - } - - return React.createElement( - 'div', - { - className: className, - ref: this._setRef, - style: _extends({}, outerStyle, style) }, - !bailoutOnChildren && children(childParams) - ); - } - }]); - return AutoSizer; -}(React.PureComponent); - -AutoSizer.defaultProps = { - onResize: function onResize() {}, - disableHeight: false, - disableWidth: false, - style: {} -}; - -export default AutoSizer; diff --git a/packages/grid/_modules_/grid/lib/createDetectElementResize/types.d.ts b/packages/grid/_modules_/grid/lib/createDetectElementResize/types.d.ts new file mode 100644 index 0000000000000..8c49c7e2babd7 --- /dev/null +++ b/packages/grid/_modules_/grid/lib/createDetectElementResize/types.d.ts @@ -0,0 +1,9 @@ +interface DetectElementResize { + addResizeListener: any; + removeResizeListener: any; +} + +export default function createDetectElementResize( + nonce: string, + hostWindow: Window +): DetectElementResize; diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx index 21494e9150844..c207ecf1b7d03 100644 --- a/packages/grid/_modules_/grid/models/gridOptions.tsx +++ b/packages/grid/_modules_/grid/models/gridOptions.tsx @@ -257,7 +257,6 @@ export const DEFAULT_GRID_OPTIONS: GridOptions = { paginationMode: FeatureModeConstant.client, sortingMode: FeatureModeConstant.client, sortingOrder: ['asc', 'desc', null], - logLevel: 'warn', columnTypes: DEFAULT_COLUMN_TYPES, icons: { columnSortedAscending: ArrowUpwardIcon, diff --git a/packages/grid/data-grid/src/DataGrid.test.tsx b/packages/grid/data-grid/src/DataGrid.test.tsx index 2844d6bc85776..43426d949334b 100644 --- a/packages/grid/data-grid/src/DataGrid.test.tsx +++ b/packages/grid/data-grid/src/DataGrid.test.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; // @ts-ignore import { createClientRender, ErrorBoundary } from 'test/utils'; +import { useFakeTimers } from 'sinon'; import { expect } from 'chai'; import { DataGrid } from '@material-ui/data-grid'; @@ -81,6 +82,29 @@ describe('', () => { expect(cell).to.have.text('Addidas'); }); }); + + describe('warnings', () => { + let clock; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should warn if the container has no intrinsic height', () => { + expect(() => { + render( +
+ +
, + ); + clock.tick(100); + }).toWarnDev('Material-UI Data Grid: The parent of the grid has an empty height.'); + }); + }); }); describe('warnings', () => { diff --git a/tsconfig.json b/tsconfig.json index edd18c494f9f6..e412ad9f78535 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,7 @@ "@material-ui/x-grid": ["./packages/grid/x-grid/src"], "@material-ui/x-grid/*": ["./packages/grid/x-grid/src/*"], "@material-ui/x-license": ["./packages/x-license/src"], - "@material-ui/x-license/*": ["./packages/x-license/src/ยจ"] + "@material-ui/x-license/*": ["./packages/x-license/src/*"] } } }