From edc3bf180d8d65184210141b3e81e54c0b6c33b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 6 Apr 2020 15:39:12 -0400 Subject: [PATCH 01/26] Support arrays for conditional styling `if`: row_index, header_index, column_id --- src/dash-table/conditional/index.ts | 28 ++++--- src/dash-table/dash/DataTable.js | 12 +-- tests/visual/percy-storybook/Style.percy.tsx | 78 +++++++++++++++++++- 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index 92f2f5691..86478a8bf 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -13,11 +13,11 @@ export interface IIndexedHeaderElement { } export interface IIndexedRowElement { - row_index?: number | 'odd' | 'even'; + row_index?: number[] | number | 'odd' | 'even'; } export interface INamedElement { - column_id?: ColumnId; + column_id?: ColumnId[] | ColumnId; } export interface ITypedElement { @@ -38,8 +38,12 @@ function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) { } export function ifColumnId(condition: INamedElement | undefined, columnId: ColumnId) { - return !condition || - condition.column_id === undefined || + if (!condition || condition.column_id === undefined) { + return true; + } + + return Array.isArray(condition.column_id) ? + R.contains(columnId, condition.column_id) : condition.column_id === columnId; } @@ -56,9 +60,11 @@ export function ifRowIndex(condition: IIndexedRowElement | undefined, rowIndex: } const rowCondition = condition.row_index; - return typeof rowCondition === 'number' ? - rowIndex === rowCondition : - rowCondition === 'odd' ? rowIndex % 2 === 1 : rowIndex % 2 === 0; + return typeof rowCondition === 'string' ? + rowCondition === 'odd' ? rowIndex % 2 === 1 : rowIndex % 2 === 0 : + Array.isArray(rowCondition) ? + R.contains(rowIndex, rowCondition) : + rowIndex === rowCondition; } export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, headerIndex: number) { @@ -68,9 +74,11 @@ export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, head } const headerCondition = condition.header_index; - return typeof headerCondition === 'number' ? - headerIndex === headerCondition : - headerCondition === 'odd' ? headerIndex % 2 === 1 : headerIndex % 2 === 0; + return typeof headerCondition === 'string' ? + headerCondition === 'odd' ? headerIndex % 2 === 1 : headerIndex % 2 === 0 : + Array.isArray(headerCondition) ? + R.contains(headerIndex, headerCondition) : + headerIndex === headerCondition; } export function ifFilter(condition: IConditionalElement | undefined, datum: Datum) { diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 6c53e4685..11a8bdee6 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -1062,7 +1062,7 @@ export const propTypes = { */ style_cell_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.string, + column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) }) })), @@ -1073,12 +1073,13 @@ export const propTypes = { */ style_data_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.string, + column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), filter_query: PropTypes.string, row_index: PropTypes.oneOfType([ PropTypes.number, - PropTypes.oneOf(['odd', 'even']) + PropTypes.oneOf(['odd', 'even']), + PropTypes.arrayOf(PropTypes.number) ]), column_editable: PropTypes.bool }) @@ -1090,7 +1091,7 @@ export const propTypes = { */ style_filter_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.string, + column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), column_editable: PropTypes.bool }) @@ -1102,10 +1103,11 @@ export const propTypes = { */ style_header_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.string, + column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), header_index: PropTypes.oneOfType([ PropTypes.number, + PropTypes.arrayOf(PropTypes.number), PropTypes.oneOf(['odd', 'even']) ]), column_editable: PropTypes.bool diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 6499c775c..28ea7276e 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -24,6 +24,20 @@ for (let i = 0; i < 6; ++i) { }); } +const DEFAULT_TABLE = { + data: [ + { a: 1, b: 2, c: '3', d: '4' }, + { a: 11, b: 22, c: '33', d: '44' }, + { a: 111, b: 222, c: '333', d: '444' } + ], + columns: [ + { id: 'a', name: 'A', type: ColumnType.Any }, + { id: 'b', name: 'B', type: ColumnType.Text }, + { id: 'c', name: 'C', type: ColumnType.Numeric }, + { id: 'd', name: 'D' } + ] +}; + storiesOf('DashTable/Style type condition', module) .add('with 1 column', () => ()); + />)) + .add('data column_id array', () => ()) + .add('data row_index array', () => ()) + .add('cell column_id array', () => ()) + .add('filter column_id array', () => ()) + .add('header column_id array', () => ()) + .add('header row_index array', () => ( ({ ...c, name: [c.name, c.name, c.name, c.name] }))} + id='data-column-id-array' + style_header_conditional={[{ + if: { + row_index: [1, 3] + }, + backgroundColor: 'MediumPurple' + }]} + />)); \ No newline at end of file From a02c8d34d8b0c0e985b76a0423593f00f1b9e89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 6 Apr 2020 17:47:09 -0400 Subject: [PATCH 02/26] - Add support for `is_active` and `is_selected` conditional styling - Partial style generation for edges (for memoization optimization) - Additional visual tests - changelog entry --- CHANGELOG.md | 9 ++ src/dash-table/components/CellFactory.tsx | 4 + src/dash-table/components/EdgeFactory.tsx | 19 +++- src/dash-table/conditional/index.ts | 27 +++++- src/dash-table/dash/DataTable.js | 2 + src/dash-table/derived/cell/wrapperStyles.ts | 31 +++++-- src/dash-table/derived/edges/data.ts | 70 ++++++++++++--- src/dash-table/derived/edges/index.ts | 13 ++- src/dash-table/derived/style/index.ts | 16 +++- tests/visual/percy-storybook/Style.percy.tsx | 94 +++++++++++++++++++- 10 files changed, 251 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b311679b7..acc663dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### Added +- []() Improve conditional styling + - `style_data_conditional`: Add support for `row_index` and `column_id` array of values + - `style_header_conditional`: Add support for `header_index` and `column_id` array of values + - `style_filter_conditional`: Add support for `column_id` array of values + - `style_cell_conditional`: Add support for `column_id` array of values + - `style_data_conditional`: Add new conditions `is_active: bool` and `is_selected: bool` to customize selected and active cell styles + ## [4.6.2] - 2020-04-01 ### Changed - [#713](https://github.com/plotly/dash-table/pull/713) Update from React 16.8.6 to 16.13.0 diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index e35fc19e9..a5d9ddb52 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -73,7 +73,11 @@ export default class CellFactory { const cellStyles = this.dataStyles( partialCellStyles, + visibleColumns, + relevantStyles, + virtualized.data, virtualized.offset, + active_cell, selected_cells ); diff --git a/src/dash-table/components/EdgeFactory.tsx b/src/dash-table/components/EdgeFactory.tsx index 6cf20fd5a..904604362 100644 --- a/src/dash-table/components/EdgeFactory.tsx +++ b/src/dash-table/components/EdgeFactory.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import { memoizeOne } from 'core/memoizer'; -import derivedDataEdges from 'dash-table/derived/edges/data'; +import { derivedPartialDataEdges, derivedDataEdges } from 'dash-table/derived/edges/data'; import derivedDataOpEdges from 'dash-table/derived/edges/operationOfData'; import derivedFilterEdges from 'dash-table/derived/edges/filter'; import derivedFilterOpEdges from 'dash-table/derived/edges/operationOfFilters'; @@ -15,7 +15,7 @@ import getHeaderRows from 'dash-table/derived/header/headerRows'; import { derivedRelevantCellStyles, derivedRelevantFilterStyles, derivedRelevantHeaderStyles } from 'dash-table/derived/style'; import { Style, Cells, DataCells, BasicFilters, Headers } from 'dash-table/derived/style/props'; -import { ControlledTableProps, Columns, IViewportOffset, Data, ICellCoordinates, TableAction } from './Table/props'; +import { ControlledTableProps, Columns, IViewportOffset, Data, ICellCoordinates, TableAction, SelectedCells } from './Table/props'; import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; type EdgesMatricesOp = EdgesMatrices | undefined; @@ -25,6 +25,7 @@ export default class EdgeFactory { private readonly filterStyles = derivedRelevantFilterStyles(); private readonly headerStyles = derivedRelevantHeaderStyles(); + private readonly getPartialDataEdges = derivedPartialDataEdges(); private readonly getDataEdges = derivedDataEdges(); private readonly getDataOpEdges = derivedDataOpEdges(); private readonly getFilterEdges = derivedFilterEdges(); @@ -146,6 +147,7 @@ export default class EdgeFactory { fixed_rows, row_deletable, row_selectable, + selected_cells, style_as_list_view, style_cell, style_cell_conditional, @@ -168,6 +170,7 @@ export default class EdgeFactory { workFilter.map, fixed_columns, fixed_rows, + selected_cells, style_as_list_view, style_cell, style_cell_conditional, @@ -191,6 +194,7 @@ export default class EdgeFactory { filterMap: Map, fixed_columns: number, fixed_rows: number, + selected_cells: SelectedCells, style_as_list_view: boolean, style_cell: Style, style_cell_conditional: Cells, @@ -226,13 +230,22 @@ export default class EdgeFactory { const headerRows = getHeaderRows(columns); + const partialDataEdges = this.getPartialDataEdges( + visibleColumns, + dataStyles, + data, + offset, + style_as_list_view + ) + let dataEdges = this.getDataEdges( + partialDataEdges, visibleColumns, dataStyles, data, offset, active_cell, - style_as_list_view + selected_cells ); let dataOpEdges = this.getDataOpEdges( diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index 86478a8bf..27002983d 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -4,6 +4,11 @@ import { ColumnId, Datum, ColumnType, IColumn } from 'dash-table/components/Tabl import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { IConvertedStyle } from 'dash-table/derived/style'; +export interface IActiveElement { + is_active?: boolean; + is_selected?: boolean; +} + export interface IConditionalElement { filter_query?: string; } @@ -29,7 +34,7 @@ export interface IEditableElement { } export type ConditionalBasicFilter = INamedElement & ITypedElement; -export type ConditionalDataCell = IConditionalElement & IIndexedRowElement & INamedElement & ITypedElement & IEditableElement; +export type ConditionalDataCell = IActiveElement & IConditionalElement & IIndexedRowElement & INamedElement & ITypedElement & IEditableElement; export type ConditionalCell = INamedElement & ITypedElement; export type ConditionalHeader = IIndexedHeaderElement & INamedElement & ITypedElement; @@ -37,6 +42,16 @@ function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) { return ast.isValid && ast.evaluate(datum); } +export function ifColumnActive(condition: IActiveElement | undefined, active: boolean, selected: boolean) { + if (!condition) { + return true; + } + + // Apply active if active, apply selected if selected AND not active (active and selected styling don't overlap) + return (condition.is_active === undefined || active === condition.is_active) && + (condition.is_selected === undefined || (selected === condition.is_selected && !active)); +} + export function ifColumnId(condition: INamedElement | undefined, columnId: ColumnId) { if (!condition || condition.column_id === undefined) { return true; @@ -97,13 +112,20 @@ export function ifEditable(condition: IEditableElement | undefined, isEditable: export type Filter = (s: T[]) => T[]; -export const matchesDataCell = (datum: Datum, i: number, column: IColumn): Filter => R.filter((style => +export const matchesDataCell = ( + datum: Datum, + i: number, column: IColumn, + active: boolean, + selected: boolean +): Filter => R.filter((style => + style.matchesActive(active, selected) && style.matchesRow(i) && style.matchesColumn(column) && style.matchesFilter(datum) )); export const matchesFilterCell = (column: IColumn): Filter => R.filter((style => + !style.checksActive() && style.matchesColumn(column) )); @@ -113,6 +135,7 @@ export const matchesHeaderCell = (i: number, column: IColumn): Filter => R.filter((style => + !style.checksActive() && !style.checksColumn() && style.matchesRow(i) && style.matchesFilter(datum) diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 11a8bdee6..14deef1b4 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -1076,6 +1076,8 @@ export const propTypes = { column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), filter_query: PropTypes.string, + is_active: PropTypes.bool, + is_selected: PropTypes.bool, row_index: PropTypes.oneOfType([ PropTypes.number, PropTypes.oneOf(['odd', 'even']), diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 7ebc7ff22..79d8a5cf7 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -2,10 +2,12 @@ import * as R from 'ramda'; import { CSSProperties } from 'react'; import { memoizeOneFactory } from 'core/memoizer'; -import { Data, Columns, IViewportOffset, SelectedCells } from 'dash-table/components/Table/props'; +import { Data, Columns, IViewportOffset, SelectedCells, ICellCoordinates } from 'dash-table/components/Table/props'; import { IConvertedStyle, getDataCellStyle, getDataOpCellStyle } from '../style'; import { traverseMap2, shallowClone } from 'core/math/matrixZipMap'; +import isActiveCell from 'dash-table/derived/cell/isActive'; + const SELECTED_CELL_STYLE = { backgroundColor: 'var(--selected-background)' }; const partialGetter = ( @@ -16,28 +18,39 @@ const partialGetter = ( ) => traverseMap2( data, columns, - (datum, column, i) => getDataCellStyle(datum, i + offset.rows, column)(styles) + (datum, column, i) => getDataCellStyle(datum, i + offset.rows, column, false, false)(styles) ); const getter = ( - styles: CSSProperties[][], + baseline: CSSProperties[][], + columns: Columns, + styles: IConvertedStyle[], + data: Data, offset: IViewportOffset, + activeCell: ICellCoordinates, selectedCells: SelectedCells ) => { - styles = shallowClone(styles); + baseline = shallowClone(baseline); R.forEach(({ row: i, column: j }) => { - i -= offset.rows; - j -= offset.columns; + const iNoOffset = i - offset.rows; + const jNoOffset = j - offset.columns; - if (i < 0 || j < 0 || styles.length <= i || styles[i].length <= j) { + if (iNoOffset < 0 || jNoOffset < 0 || baseline.length <= iNoOffset || baseline[iNoOffset].length <= jNoOffset) { return; } - styles[i][j] = R.merge(styles[i][j], SELECTED_CELL_STYLE); + const active = isActiveCell(activeCell, i, j); + + const style = { + ...SELECTED_CELL_STYLE, + ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(styles) + }; + + baseline[iNoOffset][jNoOffset] = style; }, selectedCells); - return styles; + return baseline; }; const opGetter = ( diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index d97169f74..8b86956dc 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import Environment from 'core/environment'; import { memoizeOneFactory } from 'core/memoizer'; @@ -5,20 +7,22 @@ import { IViewportOffset, Columns, Data, - ICellCoordinates + ICellCoordinates, + SelectedCells } from 'dash-table/components/Table/props'; +import isActiveCell from 'dash-table/derived/cell/isActive'; + import { IConvertedStyle } from '../style'; -import { EdgesMatrices } from './type'; +import { EdgesMatrices, BorderStyle } from './type'; import { getDataCellEdges } from '.'; import { traverse2 } from 'core/math/matrixZipMap'; -export default memoizeOneFactory(( +const partialGetter = ( columns: Columns, styles: IConvertedStyle[], data: Data, offset: IViewportOffset, - active_cell: ICellCoordinates | undefined, listViewStyle: boolean ) => { if (data.length === 0 || columns.length === 0) { @@ -30,17 +34,57 @@ export default memoizeOneFactory(( traverse2( data, columns, - (datum, column, i, j) => edges.setEdges(i, j, getDataCellEdges(datum, i + offset.rows, column)(styles)) + (datum, column, i, j) => edges.setEdges(i, j, getDataCellEdges( + datum, + i + offset.rows, + column, + false, + false + )(styles)) ); - if (active_cell) { - edges.setEdges(active_cell.row - offset.rows, active_cell.column - offset.columns, { - borderBottom: [Environment.activeEdge, Infinity], - borderLeft: [Environment.activeEdge, Infinity], - borderRight: [Environment.activeEdge, Infinity], - borderTop: [Environment.activeEdge, Infinity] - }); + return edges; +} + +const getter = ( + baseline: EdgesMatrices | undefined, + columns: Columns, + styles: IConvertedStyle[], + data: Data, + offset: IViewportOffset, + activeCell: ICellCoordinates | undefined, + selectedCells: SelectedCells +) => { + if (!baseline) { + return baseline; } + const edges = baseline.clone(); + + R.forEach(({ row: i, column: j }) => { + const active = isActiveCell(activeCell, i, j); + const priority = active ? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER - 1; + + const style: BorderStyle = { + borderBottom: [Environment.activeEdge, priority], + borderLeft: [Environment.activeEdge, priority], + borderRight: [Environment.activeEdge, priority], + borderTop: [Environment.activeEdge, priority], + ...getDataCellEdges( + data[i][j], + i + offset.rows, + columns[j], + active, + true, + priority + )(styles) + }; + + edges.setEdges(i, j, style); + }, selectedCells); + return edges; -}); \ No newline at end of file +} + +export const derivedPartialDataEdges = memoizeOneFactory(partialGetter); +export const derivedDataEdges = memoizeOneFactory(getter); \ No newline at end of file diff --git a/src/dash-table/derived/edges/index.ts b/src/dash-table/derived/edges/index.ts index c323c4d15..079c7d730 100644 --- a/src/dash-table/derived/edges/index.ts +++ b/src/dash-table/derived/edges/index.ts @@ -4,7 +4,7 @@ import { Datum, IColumn } from 'dash-table/components/Table/props'; import { matchesDataCell, matchesDataOpCell, matchesFilterCell, getFilterOpStyles, matchesHeaderCell, getHeaderOpStyles } from 'dash-table/conditional'; import { traverse2 } from 'core/math/matrixZipMap'; -function resolveEdges(styles: IConvertedStyle[]): BorderStyle { +function resolveEdges(styles: IConvertedStyle[], priority?: number): BorderStyle { let res: BorderStyle = {}; traverse2( @@ -14,7 +14,7 @@ function resolveEdges(styles: IConvertedStyle[]): BorderStyle { const border = s.style[p] || s.style.border; if (border) { - res[p] = [border, i]; + res[p] = [border, priority ?? i]; } } ); @@ -22,7 +22,14 @@ function resolveEdges(styles: IConvertedStyle[]): BorderStyle { return res; } -export const getDataCellEdges = (datum: Datum, i: number, column: IColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesDataCell(datum, i, column)(styles)); +export const getDataCellEdges = ( + datum: Datum, + i: number, + column: IColumn, + active: boolean, + selected: boolean, + priority?: number +) => (styles: IConvertedStyle[]) => resolveEdges(matchesDataCell(datum, i, column, active, selected)(styles), priority); export const getDataOpCellEdges = (datum: Datum, i: number) => (styles: IConvertedStyle[]) => resolveEdges(matchesDataOpCell(datum, i)(styles)); export const getFilterCellEdges = (column: IColumn) => (styles: IConvertedStyle[]) => resolveEdges(matchesFilterCell(column)(styles)); export const getFilterOpCellEdges = () => (styles: IConvertedStyle[]) => resolveEdges(getFilterOpStyles(styles)); diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index b885234d9..1e86b7656 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -24,7 +24,9 @@ import { IEditableElement, ifColumnId, ifColumnType, - ifEditable + ifEditable, + ifColumnActive, + IActiveElement } from 'dash-table/conditional'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; @@ -32,15 +34,17 @@ import { matchesDataCell, matchesDataOpCell, matchesFilterCell, getFilterOpStyle export interface IConvertedStyle { style: CSSProperties; + checksActive: () => boolean; checksColumn: () => boolean; checksRow: () => boolean; checksFilter: () => boolean; + matchesActive: (active: boolean, selected: boolean) => boolean; matchesColumn: (column: IColumn | undefined) => boolean; matchesRow: (index: number | undefined) => boolean; matchesFilter: (datum: Datum) => boolean; } -type GenericIf = Partial; +type GenericIf = Partial; type GenericStyle = Style & Partial<{ if: GenericIf }>; function convertElement(style: GenericStyle): IConvertedStyle { @@ -48,6 +52,10 @@ function convertElement(style: GenericStyle): IConvertedStyle { let ast: QuerySyntaxTree; return { + checksActive: () => !R.isNil(style.if) && ( + !R.isNil(style.if.is_active) || + !R.isNil(style.if.is_selected) + ), checksColumn: () => !R.isNil(style.if) && ( !R.isNil(style.if.column_id) || !R.isNil(style.if.column_type) || @@ -56,6 +64,8 @@ function convertElement(style: GenericStyle): IConvertedStyle { checksRow: () => !R.isNil(indexFilter), checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter_query), + matchesActive: (active: boolean, selected: boolean) => + ifColumnActive(style.if, active, selected), matchesColumn: (column: IColumn | undefined) => !style.if || ( !R.isNil(column) && @@ -139,7 +149,7 @@ export function resolveStyle(styles: IConvertedStyle[]): CSSProperties { return R.omit(BORDER_PROPERTIES_AND_FRAGMENTS, res); } -export const getDataCellStyle = (datum: Datum, i: number, column: IColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesDataCell(datum, i, column)(styles)); +export const getDataCellStyle = (datum: Datum, i: number, column: IColumn, active: boolean, selected: boolean) => (styles: IConvertedStyle[]) => resolveStyle(matchesDataCell(datum, i, column, active, selected)(styles)); export const getDataOpCellStyle = (datum: Datum, i: number) => (styles: IConvertedStyle[]) => resolveStyle(matchesDataOpCell(datum, i)(styles)); export const getFilterCellStyle = (column: IColumn) => (styles: IConvertedStyle[]) => resolveStyle(matchesFilterCell(column)(styles)); export const getFilterOpCellStyle = () => (styles: IConvertedStyle[]) => resolveStyle(getFilterOpStyles(styles)); diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 28ea7276e..4ce9ec30a 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -450,4 +450,96 @@ storiesOf('DashTable/Style type condition', module) }, backgroundColor: 'MediumPurple' }]} - />)); \ No newline at end of file + />)) + .add('active styling', () => ()) + .add('selected styling (not applied to active)', () => ()) + .add('selected styling + active styling', () => ()) + .add('inactive style applied to all but active', () => ()) + .add('unselected style applied to all but active', () => ()); + + + From 09d7f53a72e8b38df64cc90c69033bf1f976588f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 6 Apr 2020 17:50:49 -0400 Subject: [PATCH 03/26] remove useless check (cell & filter don't support active conditions) --- src/dash-table/conditional/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index 27002983d..2819f86a7 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -125,7 +125,6 @@ export const matchesDataCell = ( )); export const matchesFilterCell = (column: IColumn): Filter => R.filter((style => - !style.checksActive() && style.matchesColumn(column) )); From 6ce6ef6a0faa4946142bb5cabe2004a22235bc48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Mon, 6 Apr 2020 17:57:37 -0400 Subject: [PATCH 04/26] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acc663dc5..0eee7741b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- []() Improve conditional styling +- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling - `style_data_conditional`: Add support for `row_index` and `column_id` array of values - `style_header_conditional`: Add support for `header_index` and `column_id` array of values - `style_filter_conditional`: Add support for `column_id` array of values From 4fa6feb730f50d968c1cec941a6d835097ff3884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 6 Apr 2020 18:19:16 -0400 Subject: [PATCH 05/26] update unit tests --- tests/cypress/tests/unit/edges_test.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/tests/cypress/tests/unit/edges_test.ts b/tests/cypress/tests/unit/edges_test.ts index e8e926d86..0e047e0b7 100644 --- a/tests/cypress/tests/unit/edges_test.ts +++ b/tests/cypress/tests/unit/edges_test.ts @@ -1,17 +1,19 @@ -import dataEdges from 'dash-table/derived/edges/data'; +import { derivedPartialDataEdges } from 'dash-table/derived/edges/data'; import Environment from 'core/environment'; const converter = { + checksActive: () => true, checksColumn: () => true, checksFilter: () => true, checksRow: () => true, + matchesActive: () => true, matchesColumn: () => true, matchesFilter: () => true, matchesRow: () => true }; describe('data edges', () => { - const edgesFn = dataEdges(); + const edgesFn = derivedPartialDataEdges(); it('without data has no edges', () => { const res = edgesFn( @@ -19,7 +21,6 @@ describe('data edges', () => { [], [], { columns: 0, rows: 0 }, - undefined, false ); @@ -32,7 +33,6 @@ describe('data edges', () => { [], [{ id: 1 }], { columns: 0, rows: 0 }, - undefined, false ); @@ -45,7 +45,6 @@ describe('data edges', () => { [], [{ id: 1 }], { columns: 0, rows: 0 }, - undefined, false ); @@ -72,7 +71,6 @@ describe('data edges', () => { [], [{ id: 1 }], { columns: 0, rows: 0 }, - undefined, false ); @@ -107,7 +105,6 @@ describe('data edges', () => { { id: 2, name: 'b' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -152,7 +149,6 @@ describe('data edges', () => { { id: 2, name: 'b' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -197,7 +193,6 @@ describe('data edges', () => { { id: 2, name: 'b' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -248,7 +243,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -281,7 +275,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -314,7 +307,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -359,7 +351,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); From c843e7ef536788901969b270ad6faa8cc4601320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 7 Apr 2020 11:44:29 -0400 Subject: [PATCH 06/26] consider offset when calculating edges --- src/dash-table/components/EdgeFactory.tsx | 8 ++++---- src/dash-table/derived/edges/data.ts | 15 +++++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/dash-table/components/EdgeFactory.tsx b/src/dash-table/components/EdgeFactory.tsx index 904604362..266975b37 100644 --- a/src/dash-table/components/EdgeFactory.tsx +++ b/src/dash-table/components/EdgeFactory.tsx @@ -204,7 +204,7 @@ export default class EdgeFactory { style_filter_conditional: BasicFilters, style_header: Style, style_header_conditional: Headers, - data: Data, + virtualizedData: Data, offset: IViewportOffset ) => { const dataStyles = this.dataStyles( @@ -233,7 +233,7 @@ export default class EdgeFactory { const partialDataEdges = this.getPartialDataEdges( visibleColumns, dataStyles, - data, + virtualizedData, offset, style_as_list_view ) @@ -242,7 +242,7 @@ export default class EdgeFactory { partialDataEdges, visibleColumns, dataStyles, - data, + virtualizedData, offset, active_cell, selected_cells @@ -251,7 +251,7 @@ export default class EdgeFactory { let dataOpEdges = this.getDataOpEdges( operations, dataStyles, - data, + virtualizedData, offset, style_as_list_view ); diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index 8b86956dc..e2f0d95f0 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -61,7 +61,14 @@ const getter = ( const edges = baseline.clone(); - R.forEach(({ row: i, column: j }) => { + R.forEach(({ row: i, column: j, column_id }) => { + const iWithOffset = i - offset.rows; + const jWithOffset = j - offset.columns; + + if (iWithOffset < 0 || jWithOffset < 0 || data.length <= iWithOffset || data[iWithOffset].length <= jWithOffset) { + return; + } + const active = isActiveCell(activeCell, i, j); const priority = active ? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER - 1; @@ -71,8 +78,8 @@ const getter = ( borderRight: [Environment.activeEdge, priority], borderTop: [Environment.activeEdge, priority], ...getDataCellEdges( - data[i][j], - i + offset.rows, + data[iWithOffset][column_id], + iWithOffset, columns[j], active, true, @@ -80,7 +87,7 @@ const getter = ( )(styles) }; - edges.setEdges(i, j, style); + edges.setEdges(iWithOffset, j, style); }, selectedCells); return edges; From 6e007b368499bbf0d3af5400ca552887b1ffc13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 7 Apr 2020 15:27:22 -0400 Subject: [PATCH 07/26] selectedCells || [activeCell] --- src/dash-table/derived/cell/wrapperStyles.ts | 6 +++++- src/dash-table/derived/cell/wrappers.tsx | 6 +++++- src/dash-table/derived/edges/data.ts | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 79d8a5cf7..eebfb006e 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -32,6 +32,10 @@ const getter = ( ) => { baseline = shallowClone(baseline); + const cells = selectedCells.length ? + selectedCells : + activeCell ? [activeCell] : []; + R.forEach(({ row: i, column: j }) => { const iNoOffset = i - offset.rows; const jNoOffset = j - offset.columns; @@ -48,7 +52,7 @@ const getter = ( }; baseline[iNoOffset][jNoOffset] = style; - }, selectedCells); + }, cells); return baseline; }; diff --git a/src/dash-table/derived/cell/wrappers.tsx b/src/dash-table/derived/cell/wrappers.tsx index 21aea8388..3b8a7b744 100644 --- a/src/dash-table/derived/cell/wrappers.tsx +++ b/src/dash-table/derived/cell/wrappers.tsx @@ -44,6 +44,10 @@ class Wrappers { ) => { wrappers = shallowClone(wrappers); + const cells = selectedCells.length ? + selectedCells : + activeCell ? [activeCell] : []; + R.forEach(({ row: i, column: j }) => { i -= offset.rows; j -= offset.columns; @@ -59,7 +63,7 @@ class Wrappers { classes: w.props.classes + ' cell--selected' + (active ? ' focused' : '') }); - }, selectedCells); + }, cells); return wrappers; }); diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index e2f0d95f0..129984263 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -61,6 +61,10 @@ const getter = ( const edges = baseline.clone(); + const cells = selectedCells.length ? + selectedCells : + activeCell ? [activeCell] : []; + R.forEach(({ row: i, column: j, column_id }) => { const iWithOffset = i - offset.rows; const jWithOffset = j - offset.columns; @@ -88,7 +92,7 @@ const getter = ( }; edges.setEdges(iWithOffset, j, style); - }, selectedCells); + }, cells); return edges; } From e8257d4f6b134788faf7ffd36d0713d7a08d2f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 7 Apr 2020 15:30:09 -0400 Subject: [PATCH 08/26] clean up --- src/dash-table/derived/edges/data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index 129984263..f2f908c2f 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -65,7 +65,7 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; - R.forEach(({ row: i, column: j, column_id }) => { + R.forEach(({ row: i, column: j }) => { const iWithOffset = i - offset.rows; const jWithOffset = j - offset.columns; @@ -74,6 +74,7 @@ const getter = ( } const active = isActiveCell(activeCell, i, j); + const column_id = columns[jWithOffset].id; const priority = active ? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER - 1; const style: BorderStyle = { From de553518ba6f6b90f00c225cd75296ed858af979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 7 Apr 2020 15:31:04 -0400 Subject: [PATCH 09/26] clean up tests --- tests/visual/percy-storybook/Style.percy.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 4ce9ec30a..f8eac4c69 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -462,9 +462,6 @@ storiesOf('DashTable/Style type condition', module) border: '1px solid blue' }]} active_cell={{ row: 1, column: 1 }} - selected_cells={[ - { row: 1, column: 1, column_id: 'b' } - ]} />)) .add('selected styling (not applied to active)', () => ()) .add('unselected style applied to all but active', () => ( Date: Tue, 7 Apr 2020 16:14:49 -0400 Subject: [PATCH 10/26] base style for active vs. selected --- src/dash-table/derived/edges/data.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index f2f908c2f..905770af9 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -77,11 +77,15 @@ const getter = ( const column_id = columns[jWithOffset].id; const priority = active ? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER - 1; - const style: BorderStyle = { + const baseStyle: BorderStyle = active ? { borderBottom: [Environment.activeEdge, priority], borderLeft: [Environment.activeEdge, priority], borderRight: [Environment.activeEdge, priority], - borderTop: [Environment.activeEdge, priority], + borderTop: [Environment.activeEdge, priority] + } : {}; + + const style: BorderStyle = { + ...baseStyle, ...getDataCellEdges( data[iWithOffset][column_id], iWithOffset, From 6f93020fbd3045a6ab54059530a25f5d82d436c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 7 Apr 2020 16:26:19 -0400 Subject: [PATCH 11/26] only apply styles for active/selected cells when updating active/selected cells styling --- src/dash-table/derived/cell/wrapperStyles.ts | 2 ++ src/dash-table/derived/edges/data.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index eebfb006e..4b5a98b90 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -36,6 +36,8 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; + styles = styles.filter(style => style.checksActive()); + R.forEach(({ row: i, column: j }) => { const iNoOffset = i - offset.rows; const jNoOffset = j - offset.columns; diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index 905770af9..55565fefc 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -65,6 +65,8 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; + styles = styles.filter(style => style.checksActive()); + R.forEach(({ row: i, column: j }) => { const iWithOffset = i - offset.rows; const jWithOffset = j - offset.columns; From 6a0bc64ffb57c80eaee60cdb4c067f24eb594b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 7 Apr 2020 17:16:05 -0400 Subject: [PATCH 12/26] inactive -> baseline -> active style --- src/dash-table/derived/cell/wrapperStyles.ts | 8 +++++--- src/dash-table/derived/edges/data.ts | 13 +++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 4b5a98b90..1aed6cfd6 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -36,7 +36,8 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; - styles = styles.filter(style => style.checksActive()); + const activeStyles = styles.filter(style => style.checksActive()); + const inactiveStyles = styles.filter(style => !style.checksActive()); R.forEach(({ row: i, column: j }) => { const iNoOffset = i - offset.rows; @@ -49,9 +50,10 @@ const getter = ( const active = isActiveCell(activeCell, i, j); const style = { + ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(inactiveStyles), ...SELECTED_CELL_STYLE, - ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(styles) - }; + ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(activeStyles) + } baseline[iNoOffset][jNoOffset] = style; }, cells); diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index 55565fefc..b2bcd3a45 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -65,7 +65,8 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; - styles = styles.filter(style => style.checksActive()); + const activeStyles = styles.filter(style => style.checksActive()); + const inactiveStyles = styles.filter(style => !style.checksActive()); R.forEach(({ row: i, column: j }) => { const iWithOffset = i - offset.rows; @@ -87,6 +88,14 @@ const getter = ( } : {}; const style: BorderStyle = { + ...getDataCellEdges( + data[iWithOffset][column_id], + iWithOffset, + columns[j], + active, + true, + priority + )(inactiveStyles), ...baseStyle, ...getDataCellEdges( data[iWithOffset][column_id], @@ -95,7 +104,7 @@ const getter = ( active, true, priority - )(styles) + )(activeStyles) }; edges.setEdges(iWithOffset, j, style); From 2af76ce8767edb59be885b108ee0dfab513adf54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 14 Apr 2020 14:46:11 -0400 Subject: [PATCH 13/26] Change deprecated R.contains to R.includes --- src/dash-table/conditional/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index 2819f86a7..a02338d7c 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -58,7 +58,7 @@ export function ifColumnId(condition: INamedElement | undefined, columnId: Colum } return Array.isArray(condition.column_id) ? - R.contains(columnId, condition.column_id) : + R.includes(columnId, condition.column_id) : condition.column_id === columnId; } @@ -78,7 +78,7 @@ export function ifRowIndex(condition: IIndexedRowElement | undefined, rowIndex: return typeof rowCondition === 'string' ? rowCondition === 'odd' ? rowIndex % 2 === 1 : rowIndex % 2 === 0 : Array.isArray(rowCondition) ? - R.contains(rowIndex, rowCondition) : + R.includes(rowIndex, rowCondition) : rowIndex === rowCondition; } @@ -92,7 +92,7 @@ export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, head return typeof headerCondition === 'string' ? headerCondition === 'odd' ? headerIndex % 2 === 1 : headerIndex % 2 === 0 : Array.isArray(headerCondition) ? - R.contains(headerIndex, headerCondition) : + R.includes(headerIndex, headerCondition) : headerIndex === headerCondition; } From 43c00fbe4bd71a45c3d24db8fde7cb2881968d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 14 Apr 2020 17:11:59 -0400 Subject: [PATCH 14/26] refactor `is_selected` and `is_active` into `state: 'active'|'selected'` --- CHANGELOG.md | 2 +- src/dash-table/conditional/index.ts | 32 ++++++++-------- src/dash-table/dash/DataTable.js | 3 +- src/dash-table/derived/cell/wrapperStyles.ts | 6 ++- src/dash-table/derived/edges/data.ts | 32 +++++++++++----- src/dash-table/derived/style/index.ts | 38 ++++++++++--------- tests/visual/percy-storybook/Style.percy.tsx | 39 +++----------------- 7 files changed, 71 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eee7741b..5d15478e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - `style_header_conditional`: Add support for `header_index` and `column_id` array of values - `style_filter_conditional`: Add support for `column_id` array of values - `style_cell_conditional`: Add support for `column_id` array of values - - `style_data_conditional`: Add new conditions `is_active: bool` and `is_selected: bool` to customize selected and active cell styles + - `style_data_conditional`: Add new conditions `state: 'active'|'selected'` to customize selected and active cell styles ## [4.6.2] - 2020-04-01 ### Changed diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index a02338d7c..a00eda259 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -4,11 +4,6 @@ import { ColumnId, Datum, ColumnType, IColumn } from 'dash-table/components/Tabl import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { IConvertedStyle } from 'dash-table/derived/style'; -export interface IActiveElement { - is_active?: boolean; - is_selected?: boolean; -} - export interface IConditionalElement { filter_query?: string; } @@ -25,6 +20,10 @@ export interface INamedElement { column_id?: ColumnId[] | ColumnId; } +export interface IStateElement { + state?: 'active' | 'selected'; +} + export interface ITypedElement { column_type?: ColumnType; } @@ -34,7 +33,7 @@ export interface IEditableElement { } export type ConditionalBasicFilter = INamedElement & ITypedElement; -export type ConditionalDataCell = IActiveElement & IConditionalElement & IIndexedRowElement & INamedElement & ITypedElement & IEditableElement; +export type ConditionalDataCell = IConditionalElement & IIndexedRowElement & INamedElement & IStateElement & ITypedElement & IEditableElement; export type ConditionalCell = INamedElement & ITypedElement; export type ConditionalHeader = IIndexedHeaderElement & INamedElement & ITypedElement; @@ -42,14 +41,12 @@ function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) { return ast.isValid && ast.evaluate(datum); } -export function ifColumnActive(condition: IActiveElement | undefined, active: boolean, selected: boolean) { - if (!condition) { - return true; - } +export function ifColumnStateActive(condition: IStateElement | undefined, active: boolean) { + return condition?.state !== 'active' || active; +} - // Apply active if active, apply selected if selected AND not active (active and selected styling don't overlap) - return (condition.is_active === undefined || active === condition.is_active) && - (condition.is_selected === undefined || (selected === condition.is_selected && !active)); +export function ifColumnStateSelected(condition: IStateElement | undefined, selected: boolean) { + return condition?.state !== 'selected' || selected; } export function ifColumnId(condition: INamedElement | undefined, columnId: ColumnId) { @@ -118,33 +115,38 @@ export const matchesDataCell = ( active: boolean, selected: boolean ): Filter => R.filter((style => - style.matchesActive(active, selected) && + style.matchesActive(active) && + style.matchesSelected(selected) && style.matchesRow(i) && style.matchesColumn(column) && style.matchesFilter(datum) )); export const matchesFilterCell = (column: IColumn): Filter => R.filter((style => + !style.checksState() && style.matchesColumn(column) )); export const matchesHeaderCell = (i: number, column: IColumn): Filter => R.filter((style => + !style.checksState() && style.matchesRow(i) && style.matchesColumn(column) )); export const matchesDataOpCell = (datum: Datum, i: number): Filter => R.filter((style => - !style.checksActive() && + !style.checksState() && !style.checksColumn() && style.matchesRow(i) && style.matchesFilter(datum) )); export const getFilterOpStyles: Filter = R.filter((style => + !style.checksState() && !style.checksColumn() )); export const getHeaderOpStyles = (i: number): Filter => R.filter((style => style.matchesRow(i) && + !style.checksState() && !style.checksColumn() )); \ No newline at end of file diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 14deef1b4..fe387da58 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -1076,8 +1076,7 @@ export const propTypes = { column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), filter_query: PropTypes.string, - is_active: PropTypes.bool, - is_selected: PropTypes.bool, + state: PropTypes.oneOf(['active', 'selected']), row_index: PropTypes.oneOfType([ PropTypes.number, PropTypes.oneOf(['odd', 'even']), diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 1aed6cfd6..045313c2e 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -36,8 +36,9 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; - const activeStyles = styles.filter(style => style.checksActive()); - const inactiveStyles = styles.filter(style => !style.checksActive()); + const inactiveStyles = styles.filter(style => !style.checksState()); + const selectedStyles = styles.filter(style => style.checksStateSelected()); + const activeStyles = styles.filter(style => style.checksStateActive()); R.forEach(({ row: i, column: j }) => { const iNoOffset = i - offset.rows; @@ -52,6 +53,7 @@ const getter = ( const style = { ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(inactiveStyles), ...SELECTED_CELL_STYLE, + ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(selectedStyles), ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(activeStyles) } diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index b2bcd3a45..a1508fe7a 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -18,6 +18,9 @@ import { EdgesMatrices, BorderStyle } from './type'; import { getDataCellEdges } from '.'; import { traverse2 } from 'core/math/matrixZipMap'; +const ACTIVE_PRIORITY = Number.MAX_SAFE_INTEGER; +const SELECTED_PRIORITY = Number.MAX_SAFE_INTEGER - 1; + const partialGetter = ( columns: Columns, styles: IConvertedStyle[], @@ -65,8 +68,9 @@ const getter = ( selectedCells : activeCell ? [activeCell] : []; - const activeStyles = styles.filter(style => style.checksActive()); - const inactiveStyles = styles.filter(style => !style.checksActive()); + const inactiveStyles = styles.filter(style => !style.checksState()); + const selectedStyles = styles.filter(style => style.checksStateSelected()); + const activeStyles = styles.filter(style => style.checksStateActive()); R.forEach(({ row: i, column: j }) => { const iWithOffset = i - offset.rows; @@ -78,14 +82,9 @@ const getter = ( const active = isActiveCell(activeCell, i, j); const column_id = columns[jWithOffset].id; - const priority = active ? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER - 1; - const baseStyle: BorderStyle = active ? { - borderBottom: [Environment.activeEdge, priority], - borderLeft: [Environment.activeEdge, priority], - borderRight: [Environment.activeEdge, priority], - borderTop: [Environment.activeEdge, priority] - } : {}; + const priority = active ? ACTIVE_PRIORITY : SELECTED_PRIORITY; + const defaultEdge = active ? Environment.activeEdge : Environment.defaultEdge; const style: BorderStyle = { ...getDataCellEdges( @@ -96,7 +95,20 @@ const getter = ( true, priority )(inactiveStyles), - ...baseStyle, + + borderBottom: [defaultEdge, priority], + borderLeft: [defaultEdge, priority], + borderRight: [defaultEdge, priority], + borderTop: [defaultEdge, priority], + + ...getDataCellEdges( + data[iWithOffset][column_id], + iWithOffset, + columns[j], + active, + true, + priority + )(selectedStyles), ...getDataCellEdges( data[iWithOffset][column_id], iWithOffset, diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index 1e86b7656..ed597608a 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -25,8 +25,9 @@ import { ifColumnId, ifColumnType, ifEditable, - ifColumnActive, - IActiveElement + ifColumnStateActive, + IStateElement, + ifColumnStateSelected } from 'dash-table/conditional'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; @@ -34,17 +35,20 @@ import { matchesDataCell, matchesDataOpCell, matchesFilterCell, getFilterOpStyle export interface IConvertedStyle { style: CSSProperties; - checksActive: () => boolean; checksColumn: () => boolean; checksRow: () => boolean; checksFilter: () => boolean; - matchesActive: (active: boolean, selected: boolean) => boolean; + checksState: () => boolean; + checksStateActive: () => boolean; + checksStateSelected: () => boolean; + matchesActive: (active: boolean) => boolean; matchesColumn: (column: IColumn | undefined) => boolean; - matchesRow: (index: number | undefined) => boolean; matchesFilter: (datum: Datum) => boolean; + matchesRow: (index: number | undefined) => boolean; + matchesSelected: (selected: boolean) => boolean; } -type GenericIf = Partial; +type GenericIf = Partial; type GenericStyle = Style & Partial<{ if: GenericIf }>; function convertElement(style: GenericStyle): IConvertedStyle { @@ -52,20 +56,17 @@ function convertElement(style: GenericStyle): IConvertedStyle { let ast: QuerySyntaxTree; return { - checksActive: () => !R.isNil(style.if) && ( - !R.isNil(style.if.is_active) || - !R.isNil(style.if.is_selected) - ), checksColumn: () => !R.isNil(style.if) && ( !R.isNil(style.if.column_id) || !R.isNil(style.if.column_type) || !R.isNil(style.if.column_editable) ), - checksRow: () => !R.isNil(indexFilter), checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter_query), - - matchesActive: (active: boolean, selected: boolean) => - ifColumnActive(style.if, active, selected), + checksRow: () => !R.isNil(indexFilter), + checksState: () => !R.isNil(style.if?.state), + checksStateActive: () => style.if?.state === 'active', + checksStateSelected: () => style.if?.state === 'selected', + matchesActive: (active: boolean) => ifColumnStateActive(style.if, active), matchesColumn: (column: IColumn | undefined) => !style.if || ( !R.isNil(column) && @@ -73,16 +74,17 @@ function convertElement(style: GenericStyle): IConvertedStyle { ifColumnType(style.if, column && column.type) && ifEditable (style.if, column && column.editable) ), + matchesFilter: (datum: Datum) => + !style.if || + style.if.filter_query === undefined || + (ast = ast || new QuerySyntaxTree(style.if.filter_query)).evaluate(datum), matchesRow: (index: number | undefined) => indexFilter === undefined ? true : typeof indexFilter === 'number' ? index === indexFilter : !R.isNil(index) && (indexFilter === 'odd' ? index % 2 === 1 : index % 2 === 0), - matchesFilter: (datum: Datum) => - !style.if || - style.if.filter_query === undefined || - (ast = ast || new QuerySyntaxTree(style.if.filter_query)).evaluate(datum), + matchesSelected: (selected: boolean) => ifColumnStateSelected(style.if, selected), style: convertStyle(style) }; } diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index f8eac4c69..21811a841 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -456,19 +456,19 @@ storiesOf('DashTable/Style type condition', module) id='active-styling' style_data_conditional={[{ if: { - is_active: true + state: 'active' }, backgroundColor: 'lightblue', border: '1px solid blue' }]} active_cell={{ row: 1, column: 1 }} />)) - .add('selected styling (not applied to active)', () => ( ()) - .add('selected styling + active styling', () => ( ()) - .add('inactive style applied to all but active', () => ()) - .add('unselected style applied to all but active', () => ()); From 1db84ebc206fbeeaf0692ca6b29ca882a7360c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Tue, 14 Apr 2020 17:15:56 -0400 Subject: [PATCH 15/26] Update src/dash-table/conditional/index.ts Co-Authored-By: alexcjohnson --- src/dash-table/conditional/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index a00eda259..3a66d2801 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -87,7 +87,7 @@ export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, head const headerCondition = condition.header_index; return typeof headerCondition === 'string' ? - headerCondition === 'odd' ? headerIndex % 2 === 1 : headerIndex % 2 === 0 : + headerIndex % 2 === (headerCondition === 'odd' ? 1 : 0) : Array.isArray(headerCondition) ? R.includes(headerIndex, headerCondition) : headerIndex === headerCondition; @@ -149,4 +149,4 @@ export const getHeaderOpStyles = (i: number): Filter => R.filte style.matchesRow(i) && !style.checksState() && !style.checksColumn() -)); \ No newline at end of file +)); From df99bf65bc32cded4baec8b3eeab93b7a0486077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 14 Apr 2020 18:09:02 -0400 Subject: [PATCH 16/26] update unit tests --- tests/cypress/tests/unit/edges_test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/cypress/tests/unit/edges_test.ts b/tests/cypress/tests/unit/edges_test.ts index 0e047e0b7..9e1fa68a3 100644 --- a/tests/cypress/tests/unit/edges_test.ts +++ b/tests/cypress/tests/unit/edges_test.ts @@ -2,14 +2,17 @@ import { derivedPartialDataEdges } from 'dash-table/derived/edges/data'; import Environment from 'core/environment'; const converter = { - checksActive: () => true, + checksState: () => true, + checksStateActive: () => true, + checksStateSelected: () => true, checksColumn: () => true, checksFilter: () => true, checksRow: () => true, matchesActive: () => true, matchesColumn: () => true, matchesFilter: () => true, - matchesRow: () => true + matchesRow: () => true, + matchesSelected: () => true }; describe('data edges', () => { From ce95cfc55841d6ab82575c26b898216de62a3811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 14 Apr 2020 20:12:43 -0400 Subject: [PATCH 17/26] fix conditional styles regression --- src/dash-table/components/Table/props.ts | 2 +- src/dash-table/derived/data/virtual.ts | 2 +- src/dash-table/derived/edges/data.ts | 7 +++---- src/dash-table/derived/header/operations.tsx | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 9fc5771d6..a8e6cd27a 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -84,7 +84,7 @@ export interface ICellCoordinates { export type ColumnId = string; export type Columns = IColumn[]; export type Data = Datum[]; -export type Datum = IDatumObject | any; +export type Datum = IDatumObject; export type Indices = number[]; export type RowId = string | number; export type SelectedCells = ICellCoordinates[]; diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts index 426eec8eb..df62db0ab 100644 --- a/src/dash-table/derived/data/virtual.ts +++ b/src/dash-table/derived/data/virtual.ts @@ -22,7 +22,7 @@ const getter = ( sort_by: SortBy = [] ): IDerivedData => { const map = new Map(); - R.addIndex(R.forEach)((datum, index) => { + R.addIndex(R.forEach)((datum, index) => { map.set(datum, index); }, data); diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index a1508fe7a..805cdcf1a 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -81,14 +81,13 @@ const getter = ( } const active = isActiveCell(activeCell, i, j); - const column_id = columns[jWithOffset].id; const priority = active ? ACTIVE_PRIORITY : SELECTED_PRIORITY; const defaultEdge = active ? Environment.activeEdge : Environment.defaultEdge; const style: BorderStyle = { ...getDataCellEdges( - data[iWithOffset][column_id], + data[iWithOffset], iWithOffset, columns[j], active, @@ -102,7 +101,7 @@ const getter = ( borderTop: [defaultEdge, priority], ...getDataCellEdges( - data[iWithOffset][column_id], + data[iWithOffset], iWithOffset, columns[j], active, @@ -110,7 +109,7 @@ const getter = ( priority )(selectedStyles), ...getDataCellEdges( - data[iWithOffset][column_id], + data[iWithOffset], iWithOffset, columns[j], active, diff --git a/src/dash-table/derived/header/operations.tsx b/src/dash-table/derived/header/operations.tsx index aaf65c438..4cf15b618 100644 --- a/src/dash-table/derived/header/operations.tsx +++ b/src/dash-table/derived/header/operations.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { memoizeOneFactory } from 'core/memoizer'; import { - Datum, Selection } from 'dash-table/components/Table/props'; @@ -29,7 +28,7 @@ const getter = ( headerRows: number, rowSelectable: Selection, rowDeletable: boolean -): JSX.Element[][] => R.addIndex(R.map)( +): JSX.Element[][] => R.addIndex(R.map)( () => [ ...(rowDeletable ? [rowDeleteHeader()] : []), ...(rowSelectable ? [rowSelectCell()] : []) From 254fb2c0f0e2c66e35397192e006c7271c433e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 16 Apr 2020 08:52:47 -0400 Subject: [PATCH 18/26] Replace use of var(--accent) and var(--selected-background) for background/edge color --- src/core/environment/index.ts | 2 +- src/dash-table/components/Table/Table.less | 1 - src/dash-table/derived/cell/wrapperStyles.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/environment/index.ts b/src/core/environment/index.ts index 8343129f9..f90c56fc1 100644 --- a/src/core/environment/index.ts +++ b/src/core/environment/index.ts @@ -7,7 +7,7 @@ const DASH_DEBUG = 'dash_debug'; const DASH_LOG = 'dash_log'; const DEFAULT_EDGE: Edge = '1px solid #d3d3d3'; -const ACTIVE_EDGE: Edge = '1px solid var(--accent)'; +const ACTIVE_EDGE: Edge = '1px solid hotpink'; interface ISearchParams { get: (key: string) => string | null; diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 7162d7034..61201e3e6 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -521,7 +521,6 @@ --background-color-ellipses: rgb(253, 253, 253); --faded-text: rgb(250, 250, 250); --faded-text-header: rgb(180, 180, 180); - --selected-background: rgba(255, 65, 54, 0.2); --faded-dropdown: rgb(240, 240, 240); --muted: rgb(200, 200, 200); } diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 045313c2e..6837599e6 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -8,7 +8,7 @@ import { traverseMap2, shallowClone } from 'core/math/matrixZipMap'; import isActiveCell from 'dash-table/derived/cell/isActive'; -const SELECTED_CELL_STYLE = { backgroundColor: 'var(--selected-background)' }; +const SELECTED_CELL_STYLE = { backgroundColor: 'rgba(255, 65, 54, 0.2)' }; const partialGetter = ( columns: Columns, From 7252b038d301d19bdb0401c1c2599538721a334d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 17 Apr 2020 08:43:05 -0400 Subject: [PATCH 19/26] fix bug with row_index and header_index handling + new test --- src/dash-table/conditional/index.ts | 26 ++++++++++++-------- src/dash-table/dash/DataTable.js | 10 ++++---- src/dash-table/derived/style/index.ts | 22 ++++++++--------- tests/visual/percy-storybook/Style.percy.tsx | 10 ++++++++ 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index 3a66d2801..92db28b3b 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -66,22 +66,20 @@ export function ifColumnType(condition: ITypedElement | undefined, columnType?: } export function ifRowIndex(condition: IIndexedRowElement | undefined, rowIndex: number) { - if (!condition || - condition.row_index === undefined) { + if (!condition || condition.row_index === undefined) { return true; } const rowCondition = condition.row_index; return typeof rowCondition === 'string' ? - rowCondition === 'odd' ? rowIndex % 2 === 1 : rowIndex % 2 === 0 : + rowIndex % 2 === (rowCondition === 'odd' ? 1 : 0) : Array.isArray(rowCondition) ? R.includes(rowIndex, rowCondition) : rowIndex === rowCondition; } export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, headerIndex: number) { - if (!condition || - condition.header_index === undefined) { + if (!condition || condition.header_index === undefined) { return true; } @@ -115,38 +113,46 @@ export const matchesDataCell = ( active: boolean, selected: boolean ): Filter => R.filter((style => + !style.checksHeaderRow() && style.matchesActive(active) && style.matchesSelected(selected) && - style.matchesRow(i) && + style.matchesDataRow(i) && style.matchesColumn(column) && style.matchesFilter(datum) )); export const matchesFilterCell = (column: IColumn): Filter => R.filter((style => !style.checksState() && + !style.checksDataRow() && + !style.checksHeaderRow() && style.matchesColumn(column) )); export const matchesHeaderCell = (i: number, column: IColumn): Filter => R.filter((style => !style.checksState() && - style.matchesRow(i) && + !style.checksDataRow() && + style.matchesHeaderRow(i) && style.matchesColumn(column) )); export const matchesDataOpCell = (datum: Datum, i: number): Filter => R.filter((style => !style.checksState() && !style.checksColumn() && - style.matchesRow(i) && + !style.checksHeaderRow() && + style.matchesDataRow(i) && style.matchesFilter(datum) )); export const getFilterOpStyles: Filter = R.filter((style => !style.checksState() && + !style.checksDataRow() && + !style.checksHeaderRow() && !style.checksColumn() )); export const getHeaderOpStyles = (i: number): Filter => R.filter((style => - style.matchesRow(i) && + !style.checksDataRow() && !style.checksState() && - !style.checksColumn() + !style.checksColumn() && + style.matchesHeaderRow(i) )); diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 0f7bf6cc8..b57ede4ec 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -110,7 +110,7 @@ export const defaultProps = { export const propTypes = { /** - * The row and column indices and IDs of the currently active cell. + * The row and column indices and IDs of the currently active cell. * `row_id` is only returned if the data rows have an `id` key. */ active_cell: PropTypes.exact({ @@ -1063,7 +1063,7 @@ export const propTypes = { */ style_cell_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) }) })), @@ -1074,7 +1074,7 @@ export const propTypes = { */ style_data_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), filter_query: PropTypes.string, state: PropTypes.oneOf(['active', 'selected']), @@ -1093,7 +1093,7 @@ export const propTypes = { */ style_filter_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), column_editable: PropTypes.bool }) @@ -1105,7 +1105,7 @@ export const propTypes = { */ style_header_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), header_index: PropTypes.oneOfType([ PropTypes.number, diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index ed597608a..dd3819fe6 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -27,7 +27,9 @@ import { ifEditable, ifColumnStateActive, IStateElement, - ifColumnStateSelected + ifColumnStateSelected, + ifRowIndex, + ifHeaderIndex } from 'dash-table/conditional'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; @@ -36,7 +38,8 @@ import { matchesDataCell, matchesDataOpCell, matchesFilterCell, getFilterOpStyle export interface IConvertedStyle { style: CSSProperties; checksColumn: () => boolean; - checksRow: () => boolean; + checksDataRow: () => boolean; + checksHeaderRow: () => boolean; checksFilter: () => boolean; checksState: () => boolean; checksStateActive: () => boolean; @@ -44,7 +47,8 @@ export interface IConvertedStyle { matchesActive: (active: boolean) => boolean; matchesColumn: (column: IColumn | undefined) => boolean; matchesFilter: (datum: Datum) => boolean; - matchesRow: (index: number | undefined) => boolean; + matchesDataRow: (index: number) => boolean; + matchesHeaderRow: (index: number) => boolean; matchesSelected: (selected: boolean) => boolean; } @@ -52,7 +56,6 @@ type GenericIf = Partial; function convertElement(style: GenericStyle): IConvertedStyle { - const indexFilter = style.if && (style.if.header_index || style.if.row_index); let ast: QuerySyntaxTree; return { @@ -62,7 +65,8 @@ function convertElement(style: GenericStyle): IConvertedStyle { !R.isNil(style.if.column_editable) ), checksFilter: () => !R.isNil(style.if) && !R.isNil(style.if.filter_query), - checksRow: () => !R.isNil(indexFilter), + checksDataRow: () => !R.isNil(style.if) && !R.isNil(style.if.row_index), + checksHeaderRow: () => !R.isNil(style.if) && !R.isNil(style.if.header_index), checksState: () => !R.isNil(style.if?.state), checksStateActive: () => style.if?.state === 'active', checksStateSelected: () => style.if?.state === 'selected', @@ -78,12 +82,8 @@ function convertElement(style: GenericStyle): IConvertedStyle { !style.if || style.if.filter_query === undefined || (ast = ast || new QuerySyntaxTree(style.if.filter_query)).evaluate(datum), - matchesRow: (index: number | undefined) => - indexFilter === undefined ? - true : - typeof indexFilter === 'number' ? - index === indexFilter : - !R.isNil(index) && (indexFilter === 'odd' ? index % 2 === 1 : index % 2 === 0), + matchesDataRow: (index: number) => ifRowIndex(style.if, index), + matchesHeaderRow: (index: number) => ifHeaderIndex(style.if, index), matchesSelected: (selected: boolean) => ifColumnStateSelected(style.if, selected), style: convertStyle(style) }; diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 21811a841..8a70338d2 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -409,6 +409,16 @@ storiesOf('DashTable/Style type condition', module) backgroundColor: 'MediumPurple' }]} />)) + .add('header header_index array', () => ( ({ ...c, name: [c.name, c.name, c.name] }))} + style_header_conditional={[ + { if: { header_index: [0, 2] }, background_color: 'blue', color: 'white' } + ]} + />)) + .add('cell column_id array', () => ( Date: Fri, 17 Apr 2020 08:47:28 -0400 Subject: [PATCH 20/26] clean up visual tests --- tests/visual/percy-storybook/Style.percy.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 8a70338d2..c3b35d17d 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -450,17 +450,6 @@ storiesOf('DashTable/Style type condition', module) backgroundColor: 'MediumPurple' }]} />)) - .add('header row_index array', () => ( ({ ...c, name: [c.name, c.name, c.name, c.name] }))} - id='data-column-id-array' - style_header_conditional={[{ - if: { - row_index: [1, 3] - }, - backgroundColor: 'MediumPurple' - }]} - />)) .add('active styling', () => ( Date: Fri, 17 Apr 2020 09:35:00 -0400 Subject: [PATCH 21/26] update edges unit cases --- tests/cypress/tests/unit/edges_test.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/cypress/tests/unit/edges_test.ts b/tests/cypress/tests/unit/edges_test.ts index 9e1fa68a3..a5ba6e01a 100644 --- a/tests/cypress/tests/unit/edges_test.ts +++ b/tests/cypress/tests/unit/edges_test.ts @@ -1,17 +1,20 @@ import { derivedPartialDataEdges } from 'dash-table/derived/edges/data'; import Environment from 'core/environment'; - -const converter = { - checksState: () => true, - checksStateActive: () => true, - checksStateSelected: () => true, - checksColumn: () => true, - checksFilter: () => true, - checksRow: () => true, +import { IConvertedStyle } from 'dash-table/derived/style'; + +const converter: Omit = { + checksColumn: () => false, + checksDataRow: () => false, + checksFilter: () => false, + checksHeaderRow: () => false, + checksState: () => false, + checksStateActive: () => false, + checksStateSelected: () => false, matchesActive: () => true, matchesColumn: () => true, + matchesDataRow: () => true, matchesFilter: () => true, - matchesRow: () => true, + matchesHeaderRow: () => true, matchesSelected: () => true }; From 345918ab54d26cd51d037f5c474833ccac8e29b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 17 Apr 2020 13:28:53 -0400 Subject: [PATCH 22/26] Use hard-coded values for IE11 / browsers that don't support variables - use the varible by default otherwise --- src/core/environment/index.ts | 14 +++++++++----- src/dash-table/components/Table/Table.less | 1 + src/dash-table/derived/cell/wrapperStyles.ts | 5 ++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/environment/index.ts b/src/core/environment/index.ts index f90c56fc1..9e266bf03 100644 --- a/src/core/environment/index.ts +++ b/src/core/environment/index.ts @@ -6,14 +6,14 @@ import { Edge } from 'dash-table/derived/edges/type'; const DASH_DEBUG = 'dash_debug'; const DASH_LOG = 'dash_log'; -const DEFAULT_EDGE: Edge = '1px solid #d3d3d3'; -const ACTIVE_EDGE: Edge = '1px solid hotpink'; - interface ISearchParams { get: (key: string) => string | null; } export default class Environment { + private static readonly _supportsCssVariables = Boolean(window.CSS?.supports?.('.some-selector', 'var(--some-var)')); + private static readonly _activeEdge: Edge = Environment._supportsCssVariables ? '1px solid var(--accent)' : '1px solid hotpink'; + public static get searchParams(): ISearchParams { return ( typeof URL !== 'undefined' && @@ -40,10 +40,14 @@ export default class Environment { } public static get defaultEdge(): Edge { - return DEFAULT_EDGE; + return '1px solid #d3d3d3'; } public static get activeEdge(): Edge { - return ACTIVE_EDGE; + return Environment._activeEdge; + } + + public static get supportsCssVariables(): boolean { + return Environment._supportsCssVariables; } } \ No newline at end of file diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index 61201e3e6..f6b9d3e5a 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -521,6 +521,7 @@ --background-color-ellipses: rgb(253, 253, 253); --faded-text: rgb(250, 250, 250); --faded-text-header: rgb(180, 180, 180); + --selected-background: rgba(255, 65, 54, 0.2) --faded-dropdown: rgb(240, 240, 240); --muted: rgb(200, 200, 200); } diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 6837599e6..a000e24e8 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -7,8 +7,11 @@ import { IConvertedStyle, getDataCellStyle, getDataOpCellStyle } from '../style' import { traverseMap2, shallowClone } from 'core/math/matrixZipMap'; import isActiveCell from 'dash-table/derived/cell/isActive'; +import Environment from 'core/environment'; -const SELECTED_CELL_STYLE = { backgroundColor: 'rgba(255, 65, 54, 0.2)' }; +const SELECTED_CELL_STYLE = { + backgroundColor: Environment.supportsCssVariables ? 'var(--selected-background)' : 'rgba(255, 65, 54, 0.2)' +}; const partialGetter = ( columns: Columns, From bb1f6e54f2ad7ea926da0ac85ff624d05e78f556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 17 Apr 2020 14:42:07 -0400 Subject: [PATCH 23/26] missing `;` --- src/dash-table/components/Table/Table.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index f6b9d3e5a..7162d7034 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -521,7 +521,7 @@ --background-color-ellipses: rgb(253, 253, 253); --faded-text: rgb(250, 250, 250); --faded-text-header: rgb(180, 180, 180); - --selected-background: rgba(255, 65, 54, 0.2) + --selected-background: rgba(255, 65, 54, 0.2); --faded-dropdown: rgb(240, 240, 240); --muted: rgb(200, 200, 200); } From 8085950b3ffbc3c618db19df52a314640387a4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 20 Apr 2020 16:51:37 -0400 Subject: [PATCH 24/26] active partially overrides selected --- tests/visual/percy-storybook/Style.percy.tsx | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index c3b35d17d..900e29aa8 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -479,6 +479,32 @@ storiesOf('DashTable/Style type condition', module) { row: 2, column: 2, column_id: 'c' }]} active_cell={{ row: 1, column: 1 }} />)) + .add('active styling partially overrides selected', () => ()) .add('active styling overrides selected', () => ( Date: Mon, 20 Apr 2020 20:35:20 -0400 Subject: [PATCH 25/26] Remove invalid check - object.length === 0 -> always `le` to offset --- src/dash-table/derived/edges/data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/derived/edges/data.ts b/src/dash-table/derived/edges/data.ts index 805cdcf1a..ee4b92b34 100644 --- a/src/dash-table/derived/edges/data.ts +++ b/src/dash-table/derived/edges/data.ts @@ -76,7 +76,7 @@ const getter = ( const iWithOffset = i - offset.rows; const jWithOffset = j - offset.columns; - if (iWithOffset < 0 || jWithOffset < 0 || data.length <= iWithOffset || data[iWithOffset].length <= jWithOffset) { + if (iWithOffset < 0 || jWithOffset < 0 || data.length <= iWithOffset) { return; } From 8350c64684358f436a1bc6112db4fa0e631ae978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 20 Apr 2020 20:50:35 -0400 Subject: [PATCH 26/26] Improving typing on `data` prop and related manipulations --- src/dash-table/components/Table/derivedPropsHelper.ts | 9 +++++---- src/dash-table/components/Table/props.ts | 4 +++- src/dash-table/derived/cell/cellProps.ts | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/dash-table/components/Table/derivedPropsHelper.ts b/src/dash-table/components/Table/derivedPropsHelper.ts index f7328a5ad..04f27fa40 100644 --- a/src/dash-table/components/Table/derivedPropsHelper.ts +++ b/src/dash-table/components/Table/derivedPropsHelper.ts @@ -6,6 +6,7 @@ import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree'; import { ControlledTableProps, + IndexedData, SanitizedAndDerivedProps, SetProps, TableAction @@ -67,19 +68,19 @@ export default () => { if (!virtualCached) { newProps.derived_virtual_data = virtual.data; newProps.derived_virtual_indices = virtual.indices; - newProps.derived_virtual_row_ids = R.pluck('id', virtual.data); + newProps.derived_virtual_row_ids = R.pluck('id', virtual.data as IndexedData); } if (!viewportCached) { newProps.derived_viewport_data = viewport.data; newProps.derived_viewport_indices = viewport.indices; - newProps.derived_viewport_row_ids = R.pluck('id', viewport.data); + newProps.derived_viewport_row_ids = R.pluck('id', viewport.data as IndexedData); } if (!virtualSelectedRowsCached) { newProps.derived_virtual_selected_rows = virtual_selected_rows; newProps.derived_virtual_selected_row_ids = R.map( - i => virtual.data[i].id, + i => (virtual.data as IndexedData)[i].id, virtual_selected_rows ); } @@ -91,7 +92,7 @@ export default () => { if (!viewportSelectedRowsCached) { newProps.derived_viewport_selected_rows = viewport_selected_rows; newProps.derived_viewport_selected_row_ids = R.map( - i => viewport.data[i].id, + i => (viewport.data as IndexedData)[i].id, viewport_selected_rows ); } diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index a8e6cd27a..d65a9ae60 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -84,7 +84,9 @@ export interface ICellCoordinates { export type ColumnId = string; export type Columns = IColumn[]; export type Data = Datum[]; +export type IndexedData = IndexedDatum[]; export type Datum = IDatumObject; +export type IndexedDatum = Omit & { id: number | string; }; export type Indices = number[]; export type RowId = string | number; export type SelectedCells = ICellCoordinates[]; @@ -199,7 +201,7 @@ export type IColumnType = INumberColumn | ITextColumn | IDatetimeColumn | IAnyCo export type IColumn = IBaseColumn & IColumnType; interface IDatumObject { - [key: string]: any; + [key: string]: boolean | number | string | null | undefined; } export interface IDropdownValue { diff --git a/src/dash-table/derived/cell/cellProps.ts b/src/dash-table/derived/cell/cellProps.ts index a76051847..6c28a2f98 100644 --- a/src/dash-table/derived/cell/cellProps.ts +++ b/src/dash-table/derived/cell/cellProps.ts @@ -1,5 +1,5 @@ import { map, range, xprod } from 'ramda'; -import { ICellCoordinates, Columns, IDerivedData } from 'dash-table/components/Table/props'; +import { ICellCoordinates, Columns, IDerivedData, IndexedData } from 'dash-table/components/Table/props'; export function makeCell ( row: number, @@ -12,7 +12,7 @@ export function makeCell ( column, column_id: columns[column].id }; - const rowId = viewport.data[row].id; + const rowId = (viewport.data as IndexedData)[row].id; if (rowId !== undefined) { cell.row_id = rowId; }