diff --git a/packages/docs-app/src/examples/table-examples/cellLoadingExample.tsx b/packages/docs-app/src/examples/table-examples/cellLoadingExample.tsx index 6bf1216259..07fbf1512a 100644 --- a/packages/docs-app/src/examples/table-examples/cellLoadingExample.tsx +++ b/packages/docs-app/src/examples/table-examples/cellLoadingExample.tsx @@ -17,7 +17,7 @@ import * as React from "react"; import { RadioGroup } from "@blueprintjs/core"; import { Example, handleStringChange, IExampleProps } from "@blueprintjs/docs-theme"; -import { Cell, Column, ColumnHeaderCell2, RowHeaderCell, Table2 } from "@blueprintjs/table"; +import { Cell, Column, ColumnHeaderCell2, RowHeaderCell2, Table2 } from "@blueprintjs/table"; interface IBigSpaceRock { [key: string]: number | string; @@ -126,7 +126,7 @@ export class CellLoadingExample extends React.PureComponent { - return ; + return ; }; private isLoading = (rowIndex: number, columnIndex: number) => { diff --git a/packages/eslint-plugin/src/rules/no-deprecated-components/no-deprecated-table-components.ts b/packages/eslint-plugin/src/rules/no-deprecated-components/no-deprecated-table-components.ts index 45afb1a6bb..05da4ced7c 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated-components/no-deprecated-table-components.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated-components/no-deprecated-table-components.ts @@ -8,6 +8,7 @@ export const tableComponentsMigrationMapping = { ColumnHeaderCell: "ColumnHeaderCell2", EditableCell: "EditableCell2", JSONFormat: "JSONFormat2", + RowHeaderCell: "RowHeaderCell2", Table: "Table2", TruncatedFormat: "TruncatedFormat2", }; diff --git a/packages/popover2/src/contextMenu2.tsx b/packages/popover2/src/contextMenu2.tsx index 94faf77b3b..cf1fe6a760 100644 --- a/packages/popover2/src/contextMenu2.tsx +++ b/packages/popover2/src/contextMenu2.tsx @@ -217,10 +217,12 @@ export const ContextMenu2: React.FC = React.forwardRef { - return ; + return ; }, }, ), @@ -547,7 +547,7 @@ ReactDOM.render( }, { renderRowHeaderCell: (rowIndex: number) => { - return ; + return ; }, }, ), diff --git a/packages/table-dev-app/src/mutableTable.tsx b/packages/table-dev-app/src/mutableTable.tsx index f428c27afd..d9940df3d4 100644 --- a/packages/table-dev-app/src/mutableTable.tsx +++ b/packages/table-dev-app/src/mutableTable.tsx @@ -47,7 +47,7 @@ import { RegionCardinality, Regions, RenderMode, - RowHeaderCell, + RowHeaderCell2, StyledRegionGroup, Table2, TableLoadingOption, @@ -520,7 +520,7 @@ export class MutableTable extends React.Component<{}, IMutableTableState> { }; private renderRowHeader = (rowIndex: number) => { - return ; + return ; }; private renderRowMenu = (rowIndex: number) => { diff --git a/packages/table/src/common/contextMenuTargetWrapper.tsx b/packages/table/src/common/contextMenuTargetWrapper.tsx index 0c14a266f9..2c9aa37d6b 100644 --- a/packages/table/src/common/contextMenuTargetWrapper.tsx +++ b/packages/table/src/common/contextMenuTargetWrapper.tsx @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * @fileoverview This component is DEPRECATED, and the code is frozen. + * Table components should use ContextMenu2 instead. + */ + /* eslint-disable deprecation/deprecation */ import * as React from "react"; @@ -31,6 +36,8 @@ export interface IContextMenuTargetWrapper extends IProps { * `element.addEventListener`, the prop can be lost. This wrapper helps us * maintain context menu fuctionality when doing fancy React.cloneElement * chains. + * + * @deprecated use ContextMenu2 instead */ @ContextMenuTarget export class ContextMenuTargetWrapper extends React.PureComponent { diff --git a/packages/table/src/docs/table-features.md b/packages/table/src/docs/table-features.md index 20c6734cf8..bd87d442b9 100644 --- a/packages/table/src/docs/table-features.md +++ b/packages/table/src/docs/table-features.md @@ -144,7 +144,7 @@ individual column's header and body cells. Try selecting a different column in t @### Cells -`Cell`, `EditableCell`, `ColumnHeaderCell2`, and `RowHeaderCell` expose a `loading` prop for granular +`Cell`, `EditableCell2`, `ColumnHeaderCell2`, and `RowHeaderCell2` expose a `loading` prop for granular control of which cells should show a loading state. Try selecting a different preset loading configuration. diff --git a/packages/table/src/headers/columnHeaderCell2.tsx b/packages/table/src/headers/columnHeaderCell2.tsx index 2f72b89bea..7b9c82e1b5 100644 --- a/packages/table/src/headers/columnHeaderCell2.tsx +++ b/packages/table/src/headers/columnHeaderCell2.tsx @@ -25,7 +25,7 @@ import { columnInteractionBarContextTypes, ColumnInteractionBarContextTypes } fr import { LoadableContent } from "../common/loadableContent"; import { CLASSNAME_EXCLUDED_FROM_TEXT_MEASUREMENT } from "../common/utils"; import { HorizontalCellDivider, IColumnHeaderCellProps, IColumnHeaderCellState } from "./columnHeaderCell"; -import { HeaderCell } from "./headerCell"; +import { HeaderCell2 } from "./headerCell2"; // eslint-disable-next-line deprecation/deprecation export type ColumnHeaderCellProps = IColumnHeaderCellProps; @@ -86,7 +86,7 @@ export class ColumnHeaderCell2 extends AbstractPureComponent2 + ); } diff --git a/packages/table/src/headers/headerCell.tsx b/packages/table/src/headers/headerCell.tsx index 081a5b33fb..f38f3dd041 100644 --- a/packages/table/src/headers/headerCell.tsx +++ b/packages/table/src/headers/headerCell.tsx @@ -14,6 +14,13 @@ * limitations under the License. */ +/** + * @fileoverview This component is DEPRECATED, and the code is frozen. + * All changes & bugfixes should be made to HeaderCell2 instead. + */ + +/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components */ + import classNames from "classnames"; import * as React from "react"; @@ -92,7 +99,7 @@ export interface IHeaderCellState { isActive: boolean; } -// eslint-disable-next-line deprecation/deprecation +/** @deprecated use HeaderCell2 */ @ContextMenuTarget export class HeaderCell extends React.Component { public state: IHeaderCellState = { diff --git a/packages/table/src/headers/headerCell2.tsx b/packages/table/src/headers/headerCell2.tsx new file mode 100644 index 0000000000..d716581214 --- /dev/null +++ b/packages/table/src/headers/headerCell2.tsx @@ -0,0 +1,131 @@ +/* + * Copyright 2022 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from "classnames"; +import * as React from "react"; + +import { Classes as CoreClasses, Utils as CoreUtils, Props } from "@blueprintjs/core"; +import { ContextMenu2 } from "@blueprintjs/popover2"; + +import * as Classes from "../common/classes"; +import type { ResizeHandle } from "../interactions/resizeHandle"; + +export interface HeaderCell2Props extends Props { + children?: React.ReactNode; + + /** + * The index of the cell in the header. If provided, this will be passed as an argument to any + * callbacks when they are invoked. + */ + index?: number; + + /** + * If `true`, will apply the active class to the header to indicate it is + * part of an external operation. + */ + isActive?: boolean; + + /** + * Specifies if the cell is reorderable. + * + * @internal users should pass `isReorderable` to `ColumnHeader` or `RowHeader` instead + */ + isReorderable?: boolean; + + /** + * Specifies if the cell is selected. + * + * @internal + */ + isSelected?: boolean; + + /** + * If `true`, the row/column `name` will be replaced with a fixed-height skeleton, and the + * `resizeHandle` will not be rendered. If passing in additional children to this component, you + * will also want to conditionally apply `Classes.SKELETON` where appropriate. + * + * @default false + */ + loading?: boolean; + + /** + * The name displayed in the header of the row/column. + */ + name?: string; + + /** + * A callback that returns an element, like a ``, which is displayed by right-clicking + * anywhere in the header. The callback will receive the cell index if it was provided via + * props. + */ + menuRenderer?: (index?: number) => JSX.Element; + + /** + * A `ReorderHandle` React component that allows users to drag-reorder the column header. + */ + reorderHandle?: JSX.Element; + + /** + * A `ResizeHandle` React component that allows users to drag-resize the header. + */ + resizeHandle?: ResizeHandle; + + /** + * CSS styles for the top level element. + */ + style?: React.CSSProperties; +} + +export interface HeaderCell2State { + isActive: boolean; +} + +export class HeaderCell2 extends React.Component { + public state: HeaderCell2State = { + isActive: false, + }; + + public shouldComponentUpdate(nextProps: HeaderCell2Props) { + return ( + !CoreUtils.shallowCompareKeys(this.props, nextProps, { exclude: ["style"] }) || + !CoreUtils.deepCompareKeys(this.props, nextProps, ["style"]) + ); + } + + public render() { + const classes = classNames( + Classes.TABLE_HEADER, + { + [Classes.TABLE_HEADER_ACTIVE]: this.props.isActive || this.state.isActive, + [Classes.TABLE_HEADER_SELECTED]: this.props.isSelected, + [CoreClasses.LOADING]: this.props.loading, + }, + this.props.className, + ); + const hasMenu = this.props.menuRenderer !== undefined; + + return ( + + {this.props.children} + + ); + } +} diff --git a/packages/table/src/headers/rowHeader.tsx b/packages/table/src/headers/rowHeader.tsx index c9bc739817..502604769f 100644 --- a/packages/table/src/headers/rowHeader.tsx +++ b/packages/table/src/headers/rowHeader.tsx @@ -24,10 +24,11 @@ import { IIndexedResizeCallback } from "../interactions/resizable"; import { Orientation } from "../interactions/resizeHandle"; import { RegionCardinality, Regions } from "../regions"; import { Header, IHeaderProps } from "./header"; -import { IRowHeaderCellProps, RowHeaderCell } from "./rowHeaderCell"; +import type { RowHeaderCellProps } from "./rowHeaderCell"; +import { RowHeaderCell2 } from "./rowHeaderCell2"; /** @deprecated use RowHeaderRenderer */ -export type IRowHeaderRenderer = (rowIndex: number) => React.ReactElement; +export type IRowHeaderRenderer = (rowIndex: number) => React.ReactElement; // eslint-disable-next-line deprecation/deprecation export type RowHeaderRenderer = IRowHeaderRenderer; @@ -179,7 +180,7 @@ export class RowHeader extends React.Component { private renderGhostCell = (index: number, extremaClasses: string[]) => { const rect = this.props.grid.getGhostCellRect(index, 0); return ( - { } /** - * A default implementation of `IRowHeaderRenderer` that displays 1-indexed + * A default implementation of `RowHeaderRenderer` that displays 1-indexed * numbers for each row. */ export function renderDefaultRowHeader(rowIndex: number) { - return ; + return ; } diff --git a/packages/table/src/headers/rowHeaderCell.tsx b/packages/table/src/headers/rowHeaderCell.tsx index 4c92f795a3..4beec51e73 100644 --- a/packages/table/src/headers/rowHeaderCell.tsx +++ b/packages/table/src/headers/rowHeaderCell.tsx @@ -14,6 +14,13 @@ * limitations under the License. */ +/** + * @fileoverview This component is DEPRECATED, and the code is frozen. + * All changes & bugfixes should be made to RowHeaderCell2 instead. + */ + +/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components */ + import * as React from "react"; import { AbstractPureComponent2, Props } from "@blueprintjs/core"; @@ -47,6 +54,7 @@ export interface IRowHeaderCellProps extends IHeaderCellProps, Props { nameRenderer?: (name: string, index?: number) => React.ReactElement; } +/** @deprecated use RowHeaderCell2 */ export class RowHeaderCell extends AbstractPureComponent2 { public render() { const { diff --git a/packages/table/src/headers/rowHeaderCell2.tsx b/packages/table/src/headers/rowHeaderCell2.tsx new file mode 100644 index 0000000000..8442a8298e --- /dev/null +++ b/packages/table/src/headers/rowHeaderCell2.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; + +import { AbstractPureComponent2 } from "@blueprintjs/core"; + +import * as Classes from "../common/classes"; +import { LoadableContent } from "../common/loadableContent"; +import { HeaderCell2 } from "./headerCell2"; +import type { RowHeaderCellProps } from "./rowHeaderCell"; + +export class RowHeaderCell2 extends AbstractPureComponent2 { + public render() { + const { + // from IRowHeaderCellProps + enableRowReordering, + isRowSelected, + name, + nameRenderer, + + // from IHeaderProps + ...spreadableProps + } = this.props; + const defaultName =
{name}
; + + const nameComponent = ( + + {nameRenderer?.(name!, spreadableProps.index) ?? defaultName} + + ); + + return ( + +
{nameComponent}
+ {this.props.children} + {spreadableProps.loading ? undefined : spreadableProps.resizeHandle} +
+ ); + } +} diff --git a/packages/table/src/index.ts b/packages/table/src/index.ts index c946a5c129..e463fa7506 100644 --- a/packages/table/src/index.ts +++ b/packages/table/src/index.ts @@ -73,7 +73,9 @@ export { ColumnHeaderCell, IColumnHeaderCellProps, HorizontalCellDivider } from export { ColumnHeaderCell2, ColumnHeaderCellProps } from "./headers/columnHeaderCell2"; -export { IRowHeaderCellProps, RowHeaderCell } from "./headers/rowHeaderCell"; +export { IRowHeaderCellProps, RowHeaderCellProps, RowHeaderCell } from "./headers/rowHeaderCell"; + +export { RowHeaderCell2 } from "./headers/rowHeaderCell2"; export { IEditableNameProps, EditableNameProps, EditableName } from "./headers/editableName"; diff --git a/packages/table/src/table2.tsx b/packages/table/src/table2.tsx index 7dbfaf121f..d7b69f9c90 100644 --- a/packages/table/src/table2.tsx +++ b/packages/table/src/table2.tsx @@ -55,7 +55,7 @@ import { resizeRowsByTallestCell, } from "./resizeRows"; import { compareChildren, getHotkeysFromProps, isSelectionModeEnabled } from "./table2Utils"; -import { TableBody } from "./tableBody"; +import { TableBody2 } from "./tableBody2"; import { TableHotkeys } from "./tableHotkeys"; import type { TableProps, TablePropsDefaults, TablePropsWithDefaults } from "./tableProps"; import type { TableSnapshot, TableState } from "./tableState"; @@ -1029,7 +1029,7 @@ export class Table2 extends AbstractComponent2 - = ["selectedRegions"]; +const DEEP_COMPARE_KEYS: Array = ["selectedRegions"]; -export class TableBody extends AbstractComponent2 { +/** @deprecated use TableBody2 instead */ +export class TableBody extends AbstractComponent2 { public static defaultProps = { loading: false, renderMode: RenderMode.BATCH, diff --git a/packages/table/src/tableBody2.tsx b/packages/table/src/tableBody2.tsx new file mode 100644 index 0000000000..d845291552 --- /dev/null +++ b/packages/table/src/tableBody2.tsx @@ -0,0 +1,170 @@ +/* + * Copyright 2022 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from "classnames"; +import * as React from "react"; + +import { AbstractComponent2, Utils as CoreUtils } from "@blueprintjs/core"; +import { ContextMenu2, ContextMenu2ContentProps } from "@blueprintjs/popover2"; + +import type { CellCoordinates } from "./common/cellTypes"; +import * as Classes from "./common/classes"; +import { RenderMode } from "./common/renderMode"; +import type { CoordinateData } from "./interactions/dragTypes"; +import { MenuContext } from "./interactions/menus"; +import { DragSelectable } from "./interactions/selectable"; +import { Region, Regions } from "./regions"; +import type { TableBodyProps } from "./tableBody"; +import { cellClassNames, TableBodyCells } from "./tableBodyCells"; + +const DEEP_COMPARE_KEYS: Array = ["selectedRegions"]; + +export class TableBody2 extends AbstractComponent2 { + public static defaultProps = { + loading: false, + renderMode: RenderMode.BATCH, + }; + + /** + * @deprecated, will be removed from public API in the next major version + */ + public static cellClassNames(rowIndex: number, columnIndex: number) { + return cellClassNames(rowIndex, columnIndex); + } + + private activationCell: CellCoordinates | null = null; + + public shouldComponentUpdate(nextProps: TableBodyProps) { + return ( + !CoreUtils.shallowCompareKeys(this.props, nextProps, { exclude: DEEP_COMPARE_KEYS }) || + !CoreUtils.deepCompareKeys(this.props, nextProps, DEEP_COMPARE_KEYS) + ); + } + + public render() { + const { grid, numFrozenColumns, numFrozenRows } = this.props; + + const defaultStyle = grid.getRect().sizeStyle(); + const style = { + height: numFrozenRows != null ? grid.getCumulativeHeightAt(numFrozenRows - 1) : defaultStyle.height, + width: numFrozenColumns != null ? grid.getCumulativeWidthAt(numFrozenColumns - 1) : defaultStyle.width, + }; + + return ( + + + + + + ); + } + + public renderContextMenu = ({ mouseEvent }: ContextMenu2ContentProps) => { + const { grid, bodyContextMenuRenderer, selectedRegions = [] } = this.props; + const { numRows, numCols } = grid; + + if (bodyContextMenuRenderer === undefined || mouseEvent === undefined) { + return undefined; + } + + const targetRegion = this.locateClick(mouseEvent.nativeEvent as MouseEvent); + let nextSelectedRegions: Region[] = selectedRegions; + + // if the event did not happen within a selected region, clear all + // selections and select the right-clicked cell. + const foundIndex = Regions.findContainingRegion(selectedRegions, targetRegion); + if (foundIndex < 0) { + nextSelectedRegions = [targetRegion]; + } + + const menuContext = new MenuContext(targetRegion, nextSelectedRegions, numRows, numCols); + const contextMenu = bodyContextMenuRenderer(menuContext); + + return contextMenu == null ? undefined : contextMenu; + }; + + // Callbacks + // ========= + + // state updates cannot happen in renderContextMenu() during the render phase, so we must handle them separately + private handleContextMenu = (e: React.MouseEvent) => { + const { onFocusedCell, onSelection, selectedRegions = [] } = this.props; + + const targetRegion = this.locateClick(e.nativeEvent as MouseEvent); + let nextSelectedRegions: Region[] = selectedRegions; + + // if the event did not happen within a selected region, clear all + // selections and select the right-clicked cell. + const foundIndex = Regions.findContainingRegion(selectedRegions, targetRegion); + if (foundIndex < 0) { + nextSelectedRegions = [targetRegion]; + onSelection(nextSelectedRegions); + + // move the focused cell to the new region. + const nextFocusedCell = { + ...Regions.getFocusCellCoordinatesFromRegion(targetRegion), + focusSelectionIndex: 0, + }; + onFocusedCell(nextFocusedCell); + } + }; + + private handleSelectionEnd = () => { + this.activationCell = null; // not strictly required, but good practice + }; + + private locateClick = (event: MouseEvent) => { + this.activationCell = this.props.locator.convertPointToCell(event.clientX, event.clientY); + return Regions.cell(this.activationCell.row, this.activationCell.col); + }; + + private locateDrag = (_event: MouseEvent, coords: CoordinateData, returnEndOnly = false) => { + if (this.activationCell === null) { + return undefined; + } + const start = this.activationCell; + const end = this.props.locator.convertPointToCell(coords.current[0], coords.current[1]); + return returnEndOnly ? Regions.cell(end.row, end.col) : Regions.cell(start.row, start.col, end.row, end.col); + }; +} diff --git a/packages/table/test/index.ts b/packages/table/test/index.ts index 323ab0231c..d51b78ca1a 100644 --- a/packages/table/test/index.ts +++ b/packages/table/test/index.ts @@ -23,12 +23,12 @@ import "./columnHeaderCellTests.tsx"; import "./columnHeaderCell2Tests.tsx"; import "./columnTests.tsx"; import "./common/internal/"; -import "./editableCellTests.tsx"; import "./editableCell2Tests.tsx"; +import "./editableCellTests.tsx"; import "./editableNameTests.tsx"; -import "./formats/truncatedFormatTests.tsx"; -import "./formats/truncatedFormat2Tests.tsx"; import "./formats/jsonFormatTests.tsx"; +import "./formats/truncatedFormat2Tests.tsx"; +import "./formats/truncatedFormatTests.tsx"; import "./gridTests.ts"; import "./guidesTests.tsx"; import "./loadableContentTests.tsx"; @@ -41,10 +41,12 @@ import "./rectTests.ts"; import "./regionsTests.ts"; import "./reorderableTests.tsx"; import "./resizableTests.tsx"; -import "./rowHeaderTests.tsx"; +import "./rowHeaderCell2Tests.tsx"; +import "./rowHeaderCellTests.tsx"; import "./selectableTests.tsx"; import "./selectionTests.tsx"; -import "./tableBodyTests.tsx"; import "./table2Tests.tsx"; +import "./tableBody2Tests.tsx"; +import "./tableBodyTests.tsx"; import "./tableTests.tsx"; import "./utilsTests.ts"; diff --git a/packages/table/test/loadingOptionsTests.tsx b/packages/table/test/loadingOptionsTests.tsx index 1a215eb4f3..5f165ace2e 100644 --- a/packages/table/test/loadingOptionsTests.tsx +++ b/packages/table/test/loadingOptionsTests.tsx @@ -16,7 +16,15 @@ import * as React from "react"; -import { Cell, Column, ColumnHeaderCell, ColumnLoadingOption, RowHeaderCell, Table2, TableLoadingOption } from "../src"; +import { + Cell, + Column, + ColumnHeaderCell2, + ColumnLoadingOption, + RowHeaderCell2, + Table2, + TableLoadingOption, +} from "../src"; import * as Classes from "../src/common/classes"; import { CellType, expectCellLoading } from "./cellTestUtils"; import { ReactHarness } from "./harness"; @@ -42,11 +50,13 @@ class TableLoadingOptionsTester extends React.Component { - return ; + return ( + + ); }; private static rowHeaderCellRenderer = (rowIndex: number) => { - return ; + return ; }; public render() { diff --git a/packages/table/test/rowHeaderCell2Tests.tsx b/packages/table/test/rowHeaderCell2Tests.tsx new file mode 100644 index 0000000000..07e55d8e16 --- /dev/null +++ b/packages/table/test/rowHeaderCell2Tests.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License.``` + */ + +import { expect } from "chai"; +import { shallow } from "enzyme"; +import * as React from "react"; +import * as sinon from "sinon"; + +import { H4 } from "@blueprintjs/core"; + +import { RowHeaderCell2, RowHeaderCellProps } from "../src"; +import * as Classes from "../src/common/classes"; +import { ReactHarness } from "./harness"; +import { createTableOfSize } from "./mocks/table"; + +describe("", () => { + const harness = new ReactHarness(); + + afterEach(() => { + harness.unmount(); + }); + + after(() => { + harness.destroy(); + }); + + it("Default renderer", () => { + const table = harness.mount(createTableOfSize(3, 2)); + const text = table.find(`.${Classes.TABLE_ROW_NAME_TEXT}`, 1)!.text(); + expect(text).to.equal("2"); + }); + + it("renders with custom className if provided", () => { + const CLASS_NAME = "my-custom-class-name"; + const table = harness.mount(); + const hasCustomClass = table.find(`.${Classes.TABLE_HEADER}`, 0)!.hasClass(CLASS_NAME); + expect(hasCustomClass).to.be.true; + }); + + it("passes index prop to nameRenderer callback if index was provided", () => { + const renderNameStub = sinon.stub(); + renderNameStub.returns("string"); + const NAME = "my-name"; + const INDEX = 17; + shallow(); + expect(renderNameStub.firstCall.args).to.deep.equal([NAME, INDEX]); + }); + + describe("Custom renderer", () => { + it("renders custom name", () => { + const rowHeaderCellRenderer = (rowIndex: number) => { + return ; + }; + const table = harness.mount(createTableOfSize(3, 2, null, { rowHeaderCellRenderer })); + const text = table.find(`.${Classes.TABLE_ROW_NAME_TEXT}`, 1)!.text(); + expect(text).to.equal("ROW-1"); + }); + + it("renders custom content", () => { + const rowHeaderCellRenderer = (rowIndex: number) => { + return ( + +

Header of {rowIndex}

+
+ ); + }; + const table = harness.mount(createTableOfSize(3, 2, null, { rowHeaderCellRenderer })); + const text = table.find(`.${Classes.TABLE_ROW_HEADERS} h4`, 1)!.text(); + expect(text).to.equal("Header of 1"); + }); + + it("renders loading state properly", () => { + const rowHeaderCellRenderer = (rowIndex: number) => { + return ; + }; + const table = harness.mount(createTableOfSize(2, 2, null, { rowHeaderCellRenderer })); + expect(table.find(`.${Classes.TABLE_ROW_HEADERS} .${Classes.TABLE_HEADER}`, 0)!.text()).to.equal(""); + expect(table.find(`.${Classes.TABLE_ROW_HEADERS} .${Classes.TABLE_HEADER}`, 1)!.text()).to.equal( + "Row Header", + ); + }); + }); + + // TODO: re-enable these tests when we switch to enzyme's testing harness instead of our own, + // so that we can supply a react context with enableColumnInteractionBar: true + // see https://github.com/palantir/blueprint/issues/2076 + describe.skip("Reorder handle", () => { + const REORDER_HANDLE_CLASS = Classes.TABLE_REORDER_HANDLE_TARGET; + + it("shows reorder handle in interaction bar if reordering and interaction bar are enabled", () => { + const element = mount({ enableRowReordering: true }); + expect(element.find(`.${Classes.TABLE_INTERACTION_BAR} .${REORDER_HANDLE_CLASS}`)!.exists()).to.be.true; + }); + + it("shows reorder handle next to row name if reordering enabled but interaction bar disabled", () => { + const element = mount({ enableRowReordering: true }); + expect(element.find(`.${Classes.TABLE_ROW_NAME} .${REORDER_HANDLE_CLASS}`)!.exists()).to.be.true; + }); + + function mount(props: Partial) { + const element = harness.mount( + } + />, + ); + return element; + } + }); +}); diff --git a/packages/table/test/rowHeaderTests.tsx b/packages/table/test/rowHeaderCellTests.tsx similarity index 95% rename from packages/table/test/rowHeaderTests.tsx rename to packages/table/test/rowHeaderCellTests.tsx index e2bd547fc2..a5fd7365be 100644 --- a/packages/table/test/rowHeaderTests.tsx +++ b/packages/table/test/rowHeaderCellTests.tsx @@ -14,6 +14,13 @@ * limitations under the License.``` */ +/** + * @fileoverview This component is DEPRECATED, and the code is frozen. + * All changes & bugfixes should be made to RowHeaderCell2 instead. + */ + +/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components */ + import { expect } from "chai"; import { shallow } from "enzyme"; import * as React from "react"; diff --git a/packages/table/test/tableBody2Tests.tsx b/packages/table/test/tableBody2Tests.tsx new file mode 100644 index 0000000000..ee0ea59bc2 --- /dev/null +++ b/packages/table/test/tableBody2Tests.tsx @@ -0,0 +1,266 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from "chai"; +import { mount, ReactWrapper } from "enzyme"; +import * as React from "react"; +import * as sinon from "sinon"; + +import { Cell } from "../src/cell/cell"; +import { Batcher } from "../src/common/batcher"; +import * as Classes from "../src/common/classes"; +import { Grid } from "../src/common/grid"; +import { Rect } from "../src/common/rect"; +import { RenderMode } from "../src/common/renderMode"; +import { MenuContext } from "../src/interactions/menus/menuContext"; +import { Region, Regions } from "../src/regions"; +import { TableBodyProps } from "../src/tableBody"; +import { TableBody2 } from "../src/tableBody2"; +import { cellClassNames } from "../src/tableBodyCells"; + +describe("TableBody2", () => { + // use enough rows that batching won't render all of them in one pass. + // and careful: if this value is too big (~100), the batcher's reliance + // on `requestIdleCallback` may cause the tests to run multiple times. + const LARGE_NUM_ROWS = Batcher.DEFAULT_ADD_LIMIT * 2; + const NUM_COLUMNS = 1; + + const COLUMN_WIDTH = 100; + const ROW_HEIGHT = 20; + + let containerElement: HTMLElement | undefined; + + beforeEach(() => { + containerElement = document.createElement("div"); + document.body.appendChild(containerElement); + }); + + afterEach(() => { + containerElement?.remove(); + }); + + it("cellClassNames", () => { + expect(cellClassNames(0, 0)).to.deep.equal([Classes.rowCellIndexClass(0), Classes.columnCellIndexClass(0)]); + expect(cellClassNames(4096, 1024)).to.deep.equal([ + Classes.rowCellIndexClass(4096), + Classes.columnCellIndexClass(1024), + ]); + }); + + describe("onCompleteRender", () => { + it("triggers onCompleteRender immediately when renderMode={RenderMode.NONE}", () => { + const onCompleteRenderSpy = sinon.spy(); + + mountTableBody({ + columnIndexEnd: 10, + onCompleteRender: onCompleteRenderSpy, + renderMode: RenderMode.NONE, + rowIndexEnd: 50, + }); + + expect(onCompleteRenderSpy.calledOnce).to.be.true; + }); + + it("doesn't triggers onCompleteRender immediately when renderMode={RenderMode.BATCH}", () => { + const onCompleteRenderSpy = sinon.spy(); + + mountTableBody({ + columnIndexEnd: 10, + onCompleteRender: onCompleteRenderSpy, + renderMode: RenderMode.BATCH, + rowIndexEnd: 500, + }); + + expect(onCompleteRenderSpy.called).to.be.false; + }); + }); + + describe("renderMode", () => { + it("renders all cells immediately if renderMode === RenderMode.NONE", () => { + const tableBody = mountTableBodyForRenderModeTest(RenderMode.NONE); + + // expect all cells to have rendered in one pass + expect(tableBody.find(Cell).length).to.equal(LARGE_NUM_ROWS); + }); + + it("uses batch rendering if renderMode === RenderMode.BATCH", () => { + const tableBody = mountTableBodyForRenderModeTest(RenderMode.BATCH); + + // run this assertion immediately, expecting that the batching hasn't finished yet. + expect(tableBody.find(Cell).length).to.equal(Batcher.DEFAULT_ADD_LIMIT); + }); + + function mountTableBodyForRenderModeTest(renderMode: RenderMode.BATCH | RenderMode.NONE) { + const rowHeights = Array(LARGE_NUM_ROWS).fill(ROW_HEIGHT); + const columnWidths = Array(NUM_COLUMNS).fill(COLUMN_WIDTH); + + const grid = new Grid(rowHeights, columnWidths); + const viewportRect = new Rect(0, 0, NUM_COLUMNS * COLUMN_WIDTH, LARGE_NUM_ROWS * ROW_HEIGHT); + + return mountTableBody({ + columnIndexEnd: NUM_COLUMNS - 1, + grid, + renderMode, + rowIndexEnd: LARGE_NUM_ROWS - 1, + viewportRect, + }); + } + }); + + describe("bodyContextMenuRenderer", () => { + // 0-indexed coordinates + const TARGET_ROW = 1; + const TARGET_COLUMN = 1; + const TARGET_CELL_COORDS = { row: TARGET_ROW, col: TARGET_COLUMN }; + const TARGET_REGION = Regions.cell(TARGET_ROW, TARGET_COLUMN); + + const onFocusedCell = sinon.spy(); + const onSelection = sinon.spy(); + const bodyContextMenuRenderer = sinon.stub().returns(
); + + afterEach(() => { + onFocusedCell.resetHistory(); + onSelection.resetHistory(); + bodyContextMenuRenderer.resetHistory(); + }); + + describe("on right-click", () => { + const simulateAction = (tableBody: ReactWrapper) => { + tableBody.simulate("contextmenu"); + }; + runTestSuite(simulateAction); + }); + + // triggering onContextMenu via ctrl+click doesn't work in HeadlessChrome + describe.skip("on ctrl+click", () => { + // ctrl+click should also triggers the context menu and should behave in the exact same way + const simulateAction = (tableBody: ReactWrapper) => { + tableBody.simulate("mousedown", { ctrlKey: true }); + }; + runTestSuite(simulateAction); + }); + + function runTestSuite(simulateAction: (tableBody: ReactWrapper) => void) { + it("selects a right-clicked cell if there is no active selection", () => { + const tableBody = mountTableBodyForContextMenuTests(TARGET_CELL_COORDS, []); + simulateAction(tableBody); + checkOnSelectionCallback([TARGET_REGION]); + }); + + it("doesn't change the selected regions if the right-clicked cell is contained in one", () => { + const selectedRegions = [ + Regions.row(TARGET_ROW + 1), // some other row + Regions.cell(0, 0, TARGET_ROW + 1, TARGET_COLUMN + 1), // includes the target cell + ]; + const tableBody = mountTableBodyForContextMenuTests(TARGET_CELL_COORDS, selectedRegions); + simulateAction(tableBody); + expect(onSelection.called).to.be.false; + }); + + it("clears selections and select the right-clicked cell if it isn't within any existing selection", () => { + const selectedRegions = [ + Regions.row(TARGET_ROW + 1), // some other row + Regions.cell(TARGET_ROW + 1, TARGET_COLUMN + 1), // includes the target cell + ]; + const tableBody = mountTableBodyForContextMenuTests(TARGET_CELL_COORDS, selectedRegions); + simulateAction(tableBody); + checkOnSelectionCallback([TARGET_REGION]); + }); + + it("renders context menu using new selection if selection changed on right-click", () => { + const tableBody = mountTableBodyForContextMenuTests(TARGET_CELL_COORDS, []); + simulateAction(tableBody); + const menuContext = bodyContextMenuRenderer.firstCall.args[0] as MenuContext; + expect(menuContext.getSelectedRegions()).to.deep.equal([TARGET_REGION]); + }); + + it("moves focused cell to right-clicked cell if selection changed on right-click", () => { + const tableBody = mountTableBodyForContextMenuTests(TARGET_CELL_COORDS, []); + simulateAction(tableBody); + expect(onFocusedCell.calledOnce).to.be.true; + expect(onFocusedCell.firstCall.args[0]).to.deep.equal({ + ...TARGET_CELL_COORDS, + focusSelectionIndex: 0, + }); + }); + } + + function mountTableBodyForContextMenuTests( + targetCellCoords: { row: number; col: number }, + selectedRegions: Region[], + ) { + return mountTableBody({ + bodyContextMenuRenderer, + locator: { + convertPointToCell: sinon.stub().returns(targetCellCoords), + } as any, + onFocusedCell, + onSelection, + selectedRegions, + }); + } + + function checkOnSelectionCallback(expectedSelectedRegions: Region[]) { + expect(onSelection.calledOnce).to.be.true; + expect(onSelection.firstCall.args[0]).to.deep.equal(expectedSelectedRegions); + } + }); + + function mountTableBody(props: Partial = {}) { + const { rowIndexEnd, columnIndexEnd, renderMode, ...spreadableProps } = props; + + const numRows = rowIndexEnd != null ? rowIndexEnd : LARGE_NUM_ROWS; + const numCols = columnIndexEnd != null ? columnIndexEnd : NUM_COLUMNS; + + const rowHeights = Array(numRows).fill(ROW_HEIGHT); + const columnWidths = Array(numCols).fill(COLUMN_WIDTH); + + const grid = new Grid(rowHeights, columnWidths); + const viewportRect = new Rect(0, 0, NUM_COLUMNS * COLUMN_WIDTH, LARGE_NUM_ROWS * ROW_HEIGHT); + + return mount( + , + { attachTo: containerElement }, + ); + } + + function cellRenderer() { + return gg; + } + + function noop() { + return; + } +}); diff --git a/packages/table/test/tableBodyTests.tsx b/packages/table/test/tableBodyTests.tsx index d3b0cddee1..30fbe095b8 100644 --- a/packages/table/test/tableBodyTests.tsx +++ b/packages/table/test/tableBodyTests.tsx @@ -14,6 +14,13 @@ * limitations under the License. */ +/** + * @fileoverview This component is DEPRECATED, and the code is frozen. + * All changes & bugfixes should be made to TableBody2 instead. + */ + +/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components */ + import { expect } from "chai"; import { mount, ReactWrapper } from "enzyme"; import * as React from "react";