Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- The `datestartswith` relational operator now supports number comparison
- Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type

[#591](https://github.com/plotly/dash-table/issues/591)
- Fixed row and column selection when multiple tables are present

## [4.3.0] - 2019-09-17
### Added
[#566](https://github.com/plotly/dash-table/pull/566)
Expand Down
18 changes: 11 additions & 7 deletions demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ class App extends Component<any, any> {
}} />
</div>);
} else if (mode === AppMode.TaleOfTwoTables) {
const props: any = {};
Object.entries(this.state.tableProps).forEach(([key, value]) => {
props[key] = this.propCache.get(key)(value);
});
if (!this.state.tableProps2) {
this.setState({
tableProps2: R.clone(this.state.tableProps)
});
}

return (<DataTable
{...props}
/>);
const baseId = this.state.tableProps2 && this.state.tableProps2.id;

return (this.state.tableProps2 ? <DataTable
{...this.state.tableProps2}
id={baseId ? 'table2' : baseId}
/> : null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the 2 tables test behavior so that

  • the props are not shared between the tables 🙄
  • the 2nd table will have an id only if the 1st one has one

}
}

Expand Down
4 changes: 3 additions & 1 deletion demo/AppMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export enum AppFlavor {
FixedColumnPlus1 = 'fixed_columns={ "headers": true, "data": 1 }',
FixedRow = 'fixed_rows={ "headers": true }',
FixedRowPlus1 = 'fixed_rows={ "headers": true, "data": 1 }',
Merged = 'merge_duplicate_headers=true'
Merged = 'merge_duplicate_headers=true',
NoId = 'id=null'
}

export const ReadWriteModes = [
Expand Down Expand Up @@ -346,6 +347,7 @@ function getModeState(mode: string | null) {
case AppMode.SingleHeaders:
return getSingleHeaderState();
case AppMode.TaleOfTwoTables:
return getActionableState();
case AppMode.Default:
default:
return getDefaultState();
Expand Down
2 changes: 2 additions & 0 deletions src/dash-table/components/CellFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default class CellFactory {
dropdown,
data,
dropdown_data,
id,
is_focused,
row_deletable,
row_selectable,
Expand Down Expand Up @@ -94,6 +95,7 @@ export default class CellFactory {
);

const operations = this.cellOperations(
id,
data,
virtualized.data,
virtualized.indices,
Expand Down
2 changes: 2 additions & 0 deletions src/dash-table/components/HeaderFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default class HeaderFactory {
columns,
data,
hidden_columns,
id,
map,
merge_duplicate_headers,
page_action,
Expand Down Expand Up @@ -93,6 +94,7 @@ export default class HeaderFactory {
);

const contents = this.headerContent(
id,
visibleColumns,
columns,
hidden_columns,
Expand Down
7 changes: 4 additions & 3 deletions src/dash-table/derived/cell/operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function deleteRow(idx: number, data: Data, selectedRows: number[]) {
return newProps;
}

function rowSelectCell(idx: number, rowSelectable: Selection, selectedRows: number[], setProps: SetProps, data: Data) {
function rowSelectCell(id: string, idx: number, rowSelectable: Selection, selectedRows: number[], setProps: SetProps, data: Data) {
return (<td
key='select'
className='dash-select-cell'
Expand All @@ -42,7 +42,7 @@ function rowSelectCell(idx: number, rowSelectable: Selection, selectedRows: numb
<input
type={rowSelectable === 'single' ? 'radio' : 'checkbox'}
style={{ verticalAlign: 'middle' }}
name='row-select'
name={`row-select-${id}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the table's id to make the group unique

checked={R.includes(idx, selectedRows)}
onChange={() => {
const newSelectedRows = rowSelectable === 'single' ?
Expand Down Expand Up @@ -73,6 +73,7 @@ function rowDeleteCell(doDelete: () => any) {
}

const getter = (
id: string,
data: Data,
viewportData: Data,
viewportIndices: Indices,
Expand All @@ -83,7 +84,7 @@ const getter = (
): JSX.Element[][] => R.addIndex<Datum, JSX.Element[]>(R.map)(
(_, rowIndex) => [
...(rowDeletable ? [rowDeleteCell(() => setProps(deleteRow(viewportIndices[rowIndex], data, selectedRows)))] : []),
...(rowSelectable ? [rowSelectCell(viewportIndices[rowIndex], rowSelectable, selectedRows, setProps, data)] : [])
...(rowSelectable ? [rowSelectCell(id, viewportIndices[rowIndex], rowSelectable, selectedRows, setProps, data)] : [])
],
viewportData
);
Expand Down
3 changes: 2 additions & 1 deletion src/dash-table/derived/header/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function getSortingIcon(columnId: ColumnId, sortBy: SortBy) {
}

function getter(
id: string,
visibleColumns: Columns,
columns: Columns,
hiddenColumns: string[] | undefined,
Expand Down Expand Up @@ -230,7 +231,7 @@ function getter(
column_selectable === 'single',
!allSelected
)}
name='column-header--select'
name={`column-select-${id}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the table's id to make the group unique

type={column_selectable === 'single' ?
'radio' :
'checkbox'
Expand Down
172 changes: 134 additions & 38 deletions tests/cypress/src/DashTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,104 +19,200 @@ const getSelector = (state: State) => {
}
};

export default class DashTable {
static getCell(row: number, column: number, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr td.column-${column}`).eq(row);
export class DashTableHelper {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generalize the helper to work for multiple tables - Leaving the static implementation below for existing tests

constructor(private readonly id) {

}

public getCell(row: number, column: number, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td.column-${column}`).eq(row);
}

static getCellById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr td[data-dash-column="${column}"]`).eq(row);
public getCellById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td[data-dash-column="${column}"]`).eq(row);
}

static getFilter(column: number, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-filter.column-${column}`);
public getFilter(column: number, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-filter.column-${column}`);
}

static getFilterById(column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-filter[data-dash-column="${column}"]`);
public getFilterById(column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-filter[data-dash-column="${column}"]`);
}

static getHeader(row: number, column: number, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header.column-${column}`).eq(row);
public getHeader(row: number, column: number, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header.column-${column}`).eq(row);
}

static getHeaderById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"]`).eq(row);
public getHeaderById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"]`).eq(row);
}

static focusCell(row: number, column: number) {
public focusCell(row: number, column: number) {
// somehow we need to scrollIntoView AFTER click, or it doesn't
// work right. Why?
return this.getCell(row, column).click().scrollIntoView();
}

static focusCellById(row: number, column: string) {
public focusCellById(row: number, column: string) {
return this.getCellById(row, column).click().scrollIntoView();
}

static clearColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--clear`).eq(row).click();
public clearColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--clear`).eq(row).click();
}

static deleteColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--delete`).eq(row).click();
public deleteColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--delete`).eq(row).click();
}

static hideColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--hide`).eq(row).click();
public hideColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--hide`).eq(row).click();
}

static getSelectColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--select input`).eq(row);
public getSelectColumnById(row: number, column: string, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--select input`).eq(row);
}

static selectColumnById(row: number, column: string) {
return DashTable.getSelectColumnById(row, column).click();
public selectColumnById(row: number, column: string) {
return this.getSelectColumnById(row, column).click();
}

static getDelete(row: number, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr td.dash-delete-cell`).eq(row);
public getDelete(row: number, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td.dash-delete-cell`).eq(row);
}

static getSelect(row: number, editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody tr td.dash-select-cell`).eq(row);
public getSelect(row: number, editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td.dash-select-cell`).eq(row);
}

static getActiveCell(editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody td.focused`);
public getActiveCell(editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody td.focused`);
}

static getSelectedCells(editable: State = State.Ready) {
return cy.get(`#table ${getSelector(editable)} tbody td.cell--selected`);
public getSelectedCells(editable: State = State.Ready) {
return cy.get(`#${this.id} ${getSelector(editable)} tbody td.cell--selected`);
}

static scrollToTop() {
public scrollToTop() {
cy.get(`.cell.cell-1-1.dash-fixed-content`).invoke(`outerHeight`).then(height => {
cy.scrollTo(0, -height);
});
}

static scrollToBottom() {
public scrollToBottom() {
cy.get(`.cell.cell-1-1.dash-fixed-content`).invoke(`outerHeight`).then(height => {
cy.scrollTo(0, height);
});
}

static getCellInLastRowOfColumn(column: number) {
public getCellInLastRowOfColumn(column: number) {
const cellInLastRow = cy.get(`td.dash-cell.column-${column}`).last().then(elem => {
const lastRow = elem ? elem.attr(`data-dash-row`) : undefined;
return lastRow ? cy.get(`td.dash-cell.column-${column}[data-dash-row="${lastRow}"`) : undefined;
});
return cellInLastRow;
}

static getCellFromDataDash(row: number, column: number) {
public getCellFromDataDash(row: number, column: number) {
return cy.get(`td.column-${column}[data-dash-row="${row}"]`);
}

static toggleScroll(toggled: boolean) {
public toggleScroll(toggled: boolean) {
cy.get('.row-1').then($el => {
$el[0].style.overflow = toggled ? '' : 'unset';
});
}
}

export default class DashTable {
private static __helper = new DashTableHelper('table');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it not work to just say export default const DashTable = new DashTableHelper('table');?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞 very much so


static getCell(_row: number, _column: number, _editable: State = State.Ready) {
return this.__helper.getCell.apply(this.__helper, Array.from(arguments) as any);
}

static getCellById(_row: number, _column: string, _editable: State = State.Ready) {
return this.__helper.getCellById.apply(this.__helper, Array.from(arguments) as any);
}

static getFilter(_column: number, _editable: State = State.Ready) {
return this.__helper.getFilter.apply(this.__helper, Array.from(arguments) as any);
}

static getFilterById(_column: string, _editable: State = State.Ready) {
return this.__helper.getFilterById.apply(this.__helper, Array.from(arguments) as any);
}

static getHeader(_row: number, _column: number, _editable: State = State.Ready) {
return this.__helper.getHeader.apply(this.__helper, Array.from(arguments) as any);
}

static getHeaderById(_row: number, _column: string, _editable: State = State.Ready) {
return this.__helper.getHeaderById.apply(this.__helper, Array.from(arguments) as any);
}

static focusCell(_row: number, _column: number) {
return this.__helper.focusCell.apply(this.__helper, Array.from(arguments) as any);
}

static focusCellById(_row: number, _column: string) {
return this.__helper.focusCellById.apply(this.__helper, Array.from(arguments) as any);
}

static clearColumnById(_row: number, _column: string, _editable: State = State.Ready) {
return this.__helper.clearColumnById.apply(this.__helper, Array.from(arguments) as any);
}

static deleteColumnById(_row: number, _column: string, _editable: State = State.Ready) {
return this.__helper.deleteColumnById.apply(this.__helper, Array.from(arguments) as any);
}

static hideColumnById(_row: number, _column: string, _editable: State = State.Ready) {
return this.__helper.hideColumnById.apply(this.__helper, Array.from(arguments) as any);
}

static getSelectColumnById(_row: number, _column: string, _editable: State = State.Ready) {
return this.__helper.getSelectColumnById.apply(this.__helper, Array.from(arguments) as any);
}

static selectColumnById(_row: number, _column: string) {
return this.__helper.selectColumnById.apply(this.__helper, Array.from(arguments) as any);
}

static getDelete(_row: number, _editable: State = State.Ready) {
return this.__helper.getDelete.apply(this.__helper, Array.from(arguments) as any);
}

static getSelect(_row: number, _editable: State = State.Ready) {
return this.__helper.getSelect.apply(this.__helper, Array.from(arguments) as any);
}

static getActiveCell(_editable: State = State.Ready) {
return this.__helper.getActiveCell.apply(this.__helper, Array.from(arguments) as any);
}

static getSelectedCells(_editable: State = State.Ready) {
return this.__helper.getSelectedCells.apply(this.__helper, Array.from(arguments) as any);
}

static scrollToTop() {
return this.__helper.scrollToTop.apply(this.__helper, Array.from(arguments) as any);
}

static scrollToBottom() {
return this.__helper.scrollToBottom.apply(this.__helper, Array.from(arguments) as any);
}

static getCellInLastRowOfColumn(_column: number) {
return this.__helper.getCellInLastRowOfColumn.apply(this.__helper, Array.from(arguments) as any);
}

static getCellFromDataDash(_row: number, _column: number) {
return this.__helper.getCellFromDataDash.apply(this.__helper, Array.from(arguments) as any);
}

static toggleScroll(_toggled: boolean) {
return this.__helper.toggleScroll.apply(this.__helper, Array.from(arguments) as any);
}
}
Loading