diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e13c4aa9..3064384ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- [#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 + - `style_cell_conditional`: Add support for `column_id` array of values + - `style_data_conditional`: Add new conditions `state: 'active'|'selected'` to customize selected and active cell styles + ### Fixed - [#722](https://github.com/plotly/dash-table/pull/722) Fix a bug where row height is misaligned when using fixed_columns and/or fixed_rows - [#728](https://github.com/plotly/dash-table/pull/728) Fix copy/paste on readonly cells diff --git a/src/core/environment/index.ts b/src/core/environment/index.ts index 8343129f9..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 var(--accent)'; - 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/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 14d1e9c0f..d7edd8b8f 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, @@ -200,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( @@ -226,19 +230,28 @@ export default class EdgeFactory { const headerRows = getHeaderRows(columns); + const partialDataEdges = this.getPartialDataEdges( + visibleColumns, + dataStyles, + virtualizedData, + offset, + style_as_list_view + ) + let dataEdges = this.getDataEdges( + partialDataEdges, visibleColumns, dataStyles, - data, + virtualizedData, offset, active_cell, - style_as_list_view + selected_cells ); let dataOpEdges = this.getDataOpEdges( operations, dataStyles, - data, + virtualizedData, offset, style_as_list_view ); diff --git a/src/dash-table/components/Table/derivedPropsHelper.ts b/src/dash-table/components/Table/derivedPropsHelper.ts index 90a3d9f80..f81d4357a 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 fbe428185..6456c79e8 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -94,7 +94,9 @@ export interface ICellCoordinates { export type ColumnId = string; export type Columns = IColumn[]; export type Data = Datum[]; -export type Datum = IDatumObject | any; +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[]; @@ -209,7 +211,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/conditional/index.ts b/src/dash-table/conditional/index.ts index 92f2f5691..92db28b3b 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -13,11 +13,15 @@ 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 IStateElement { + state?: 'active' | 'selected'; } export interface ITypedElement { @@ -29,7 +33,7 @@ export interface IEditableElement { } export type ConditionalBasicFilter = INamedElement & ITypedElement; -export type ConditionalDataCell = 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; @@ -37,9 +41,21 @@ function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) { return ast.isValid && ast.evaluate(datum); } +export function ifColumnStateActive(condition: IStateElement | undefined, active: boolean) { + return condition?.state !== 'active' || active; +} + +export function ifColumnStateSelected(condition: IStateElement | undefined, selected: boolean) { + return condition?.state !== 'selected' || selected; +} + 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.includes(columnId, condition.column_id) : condition.column_id === columnId; } @@ -50,27 +66,29 @@ 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 === 'number' ? - rowIndex === rowCondition : - rowCondition === 'odd' ? rowIndex % 2 === 1 : rowIndex % 2 === 0; + return typeof rowCondition === 'string' ? + 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; } const headerCondition = condition.header_index; - return typeof headerCondition === 'number' ? - headerIndex === headerCondition : - headerCondition === 'odd' ? headerIndex % 2 === 1 : headerIndex % 2 === 0; + return typeof headerCondition === 'string' ? + headerIndex % 2 === (headerCondition === 'odd' ? 1 : 0) : + Array.isArray(headerCondition) ? + R.includes(headerIndex, headerCondition) : + headerIndex === headerCondition; } export function ifFilter(condition: IConditionalElement | undefined, datum: Datum) { @@ -89,32 +107,52 @@ 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 => - style.matchesRow(i) && +export const matchesDataCell = ( + datum: Datum, + i: number, column: IColumn, + active: boolean, + selected: boolean +): Filter => R.filter((style => + !style.checksHeaderRow() && + style.matchesActive(active) && + style.matchesSelected(selected) && + 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.matchesRow(i) && + !style.checksState() && + !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.checksColumn() -)); \ No newline at end of file + !style.checksDataRow() && + !style.checksState() && + !style.checksColumn() && + style.matchesHeaderRow(i) +)); diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index e391d09b9..75004c57a 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -108,7 +108,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({ @@ -1077,7 +1077,7 @@ export const propTypes = { */ style_cell_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.string, + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) }) })), @@ -1088,12 +1088,14 @@ export const propTypes = { */ style_data_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: 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']), row_index: PropTypes.oneOfType([ PropTypes.number, - PropTypes.oneOf(['odd', 'even']) + PropTypes.oneOf(['odd', 'even']), + PropTypes.arrayOf(PropTypes.number) ]), column_editable: PropTypes.bool }) @@ -1105,7 +1107,7 @@ export const propTypes = { */ style_filter_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: PropTypes.string, + column_id: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), column_type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), column_editable: PropTypes.bool }) @@ -1117,10 +1119,11 @@ export const propTypes = { */ style_header_conditional: PropTypes.arrayOf(PropTypes.shape({ if: PropTypes.exact({ - column_id: 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, + PropTypes.arrayOf(PropTypes.number), PropTypes.oneOf(['odd', 'even']) ]), column_editable: PropTypes.bool 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; } diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 7ebc7ff22..a000e24e8 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -2,11 +2,16 @@ 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'; -const SELECTED_CELL_STYLE = { backgroundColor: 'var(--selected-background)' }; +import isActiveCell from 'dash-table/derived/cell/isActive'; +import Environment from 'core/environment'; + +const SELECTED_CELL_STYLE = { + backgroundColor: Environment.supportsCssVariables ? 'var(--selected-background)' : 'rgba(255, 65, 54, 0.2)' +}; const partialGetter = ( columns: Columns, @@ -16,28 +21,49 @@ 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); + + const cells = selectedCells.length ? + selectedCells : + activeCell ? [activeCell] : []; + + 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 }) => { - 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); - }, selectedCells); + 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)(selectedStyles), + ...getDataCellStyle(data[i], i + offset.rows, columns[j], active, true)(activeStyles) + } + + baseline[iNoOffset][jNoOffset] = style; + }, cells); - return styles; + return baseline; }; const opGetter = ( diff --git a/src/dash-table/derived/cell/wrappers.tsx b/src/dash-table/derived/cell/wrappers.tsx index 01ad0ba27..095ec705f 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 { className: w.props.className + ' cell--selected' + (active ? ' focused' : '') }); - }, selectedCells); + }, cells); return wrappers; }); diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts index fe1b386fd..cbad4d093 100644 --- a/src/dash-table/derived/data/virtual.ts +++ b/src/dash-table/derived/data/virtual.ts @@ -23,7 +23,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 d97169f74..ee4b92b34 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,25 @@ 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 ACTIVE_PRIORITY = Number.MAX_SAFE_INTEGER; +const SELECTED_PRIORITY = Number.MAX_SAFE_INTEGER - 1; + +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 +37,92 @@ 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(); + + const cells = selectedCells.length ? + selectedCells : + activeCell ? [activeCell] : []; + + 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; + const jWithOffset = j - offset.columns; + + if (iWithOffset < 0 || jWithOffset < 0 || data.length <= iWithOffset) { + return; + } + + const active = isActiveCell(activeCell, i, j); + + const priority = active ? ACTIVE_PRIORITY : SELECTED_PRIORITY; + const defaultEdge = active ? Environment.activeEdge : Environment.defaultEdge; + + const style: BorderStyle = { + ...getDataCellEdges( + data[iWithOffset], + iWithOffset, + columns[j], + active, + true, + priority + )(inactiveStyles), + + borderBottom: [defaultEdge, priority], + borderLeft: [defaultEdge, priority], + borderRight: [defaultEdge, priority], + borderTop: [defaultEdge, priority], + + ...getDataCellEdges( + data[iWithOffset], + iWithOffset, + columns[j], + active, + true, + priority + )(selectedStyles), + ...getDataCellEdges( + data[iWithOffset], + iWithOffset, + columns[j], + active, + true, + priority + )(activeStyles) + }; + + edges.setEdges(iWithOffset, j, style); + }, cells); + 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/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()] : []) diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index b885234d9..dd3819fe6 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -24,7 +24,12 @@ import { IEditableElement, ifColumnId, ifColumnType, - ifEditable + ifEditable, + ifColumnStateActive, + IStateElement, + ifColumnStateSelected, + ifRowIndex, + ifHeaderIndex } from 'dash-table/conditional'; import { QuerySyntaxTree } from 'dash-table/syntax-tree'; import { BORDER_PROPERTIES_AND_FRAGMENTS } from '../edges/type'; @@ -33,18 +38,24 @@ import { matchesDataCell, matchesDataOpCell, matchesFilterCell, getFilterOpStyle export interface IConvertedStyle { style: CSSProperties; checksColumn: () => boolean; - checksRow: () => boolean; + checksDataRow: () => boolean; + checksHeaderRow: () => boolean; checksFilter: () => 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; + matchesDataRow: (index: number) => boolean; + matchesHeaderRow: (index: number) => boolean; + matchesSelected: (selected: boolean) => boolean; } -type GenericIf = Partial; +type GenericIf = Partial; type GenericStyle = Style & Partial<{ if: GenericIf }>; function convertElement(style: GenericStyle): IConvertedStyle { - const indexFilter = style.if && (style.if.header_index || style.if.row_index); let ast: QuerySyntaxTree; return { @@ -53,9 +64,13 @@ function convertElement(style: GenericStyle): IConvertedStyle { !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), - + 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', + matchesActive: (active: boolean) => ifColumnStateActive(style.if, active), matchesColumn: (column: IColumn | undefined) => !style.if || ( !R.isNil(column) && @@ -63,16 +78,13 @@ function convertElement(style: GenericStyle): IConvertedStyle { ifColumnType(style.if, column && column.type) && ifEditable (style.if, column && column.editable) ), - 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), + matchesDataRow: (index: number) => ifRowIndex(style.if, index), + matchesHeaderRow: (index: number) => ifHeaderIndex(style.if, index), + matchesSelected: (selected: boolean) => ifColumnStateSelected(style.if, selected), style: convertStyle(style) }; } @@ -139,7 +151,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/cypress/tests/unit/edges_test.ts b/tests/cypress/tests/unit/edges_test.ts index e8e926d86..a5ba6e01a 100644 --- a/tests/cypress/tests/unit/edges_test.ts +++ b/tests/cypress/tests/unit/edges_test.ts @@ -1,17 +1,25 @@ -import dataEdges from 'dash-table/derived/edges/data'; +import { derivedPartialDataEdges } from 'dash-table/derived/edges/data'; import Environment from 'core/environment'; - -const converter = { - 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 }; describe('data edges', () => { - const edgesFn = dataEdges(); + const edgesFn = derivedPartialDataEdges(); it('without data has no edges', () => { const res = edgesFn( @@ -19,7 +27,6 @@ describe('data edges', () => { [], [], { columns: 0, rows: 0 }, - undefined, false ); @@ -32,7 +39,6 @@ describe('data edges', () => { [], [{ id: 1 }], { columns: 0, rows: 0 }, - undefined, false ); @@ -45,7 +51,6 @@ describe('data edges', () => { [], [{ id: 1 }], { columns: 0, rows: 0 }, - undefined, false ); @@ -72,7 +77,6 @@ describe('data edges', () => { [], [{ id: 1 }], { columns: 0, rows: 0 }, - undefined, false ); @@ -107,7 +111,6 @@ describe('data edges', () => { { id: 2, name: 'b' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -152,7 +155,6 @@ describe('data edges', () => { { id: 2, name: 'b' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -197,7 +199,6 @@ describe('data edges', () => { { id: 2, name: 'b' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -248,7 +249,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -281,7 +281,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -314,7 +313,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); @@ -359,7 +357,6 @@ describe('data edges', () => { { id: 1, name: 'a' } ], { columns: 0, rows: 0 }, - undefined, false ); diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 6499c775c..900e29aa8 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('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', () => ()) + .add('filter column_id array', () => ()) + .add('header column_id array', () => ()) + .add('active styling', () => ()) + .add('selected styling (applied to active)', () => ()) + .add('active styling partially overrides selected', () => ()) + .add('active styling overrides selected', () => ()); + + +