diff --git a/examples/src/pages/tests/table/props/data/flashing-when-ticking.page.tsx b/examples/src/pages/tests/table/props/data/flashing-when-ticking.page.tsx new file mode 100644 index 00000000..10144293 --- /dev/null +++ b/examples/src/pages/tests/table/props/data/flashing-when-ticking.page.tsx @@ -0,0 +1,165 @@ +import { DataSourceApi } from '@infinite-table/infinite-react'; +import { useEffect } from 'react'; + +type Developer = { + birthDate: string; + id: number; + firstName: string; + country: string; + city: string; + currency: string; + email: string; + preferredLanguage: string; + hobby: string; + salary: number; +}; + +export const TICK_INTERVAL = 100; + +let tickingInterval: any | null = null; + +function singleUpdate(dataSourceApi: DataSourceApi) { + const arr = dataSourceApi.getRowInfoArray(); + const len = arr.length; + const randomIndex = Math.floor(Math.random() * len); + const rowInfo = arr[randomIndex]; + const id = rowInfo.id; + + if (rowInfo.isGroupRow) { + return; + } + + const currentData = rowInfo.data; + + // generate random signs for the updates for each column + const sign = Math.random() > 0.5 ? 1 : -1; + const currentSalary = currentData.salary; + + const randomDelta = Math.round(Math.random() * Math.abs(currentSalary * 0.1)); + + const newSalary = Math.max(currentSalary + randomDelta * sign, 1000); + + const partialData: Partial = { + id, + salary: newSalary, + }; + dataSourceApi.updateData(partialData); +} +function start(dataSourceApi: DataSourceApi) { + tickingInterval = setInterval(() => { + singleUpdate(dataSourceApi); + }, TICK_INTERVAL); +} + +function stop() { + if (tickingInterval) { + clearInterval(tickingInterval); + } +} + +export function useTickingData( + dataSourceApi: DataSourceApi | null, + tick: boolean, +) { + useEffect(() => { + if (!dataSourceApi) { + return; + } + + if (tick) { + start(dataSourceApi); + } else { + stop(); + } + }, [dataSourceApi, tick]); +} + +import { + InfiniteTable, + DataSource, + InfiniteTablePropColumns, + // FlashingColumnCell, + createFlashingColumnCellComponent, +} from '@infinite-table/infinite-react'; +import * as React from 'react'; + +const FlashingColumnCell = createFlashingColumnCellComponent({ + flashDuration: 1000, + flashClassName: 'flash-cell', +}); +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + }, + firstName: { + field: 'firstName', + }, + salary: { + field: 'salary', + type: 'number', + defaultEditable: true, + getValueToPersist: ({ value }) => { + return value * 1; + }, + components: { + ColumnCell: FlashingColumnCell, + }, + }, +}; + +export default function App() { + const [ticking, setTicking] = React.useState(false); + const [dataSourceApi, setDataSourceApi] = + React.useState | null>(null); + + useTickingData(dataSourceApi, ticking); + + return ( + <> +
+
+ +
+ + + data={dataSource} + primaryKey="id" + onReady={setDataSourceApi} + > + + columns={columns} + domProps={{ + style: { + height: '80%', + }, + }} + /> + +
+ + ); +} + +const dataSource = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => { + console.log(data); + return data; + }); +}; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx index f6f5b119..ff5d1e13 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx @@ -29,6 +29,20 @@ const defaultRender: FlashingCellOptions['render'] = ({ children }) => { export const DEFAULT_FLASH_DURATION = 1000; +type FlashDirection = 'up' | 'down' | 'neutral'; + +const INTERNAL_FLASH_CLS_FOR_DIRECTION: Record = { + up: FlashingColumnCellRecipe({ + direction: 'up', + }).split(' '), + down: FlashingColumnCellRecipe({ + direction: 'down', + }).split(' '), + neutral: FlashingColumnCellRecipe({ + direction: 'neutral', + }).split(' '), +}; + export const createFlashingColumnCellComponent = ( options: FlashingCellOptions = {}, ) => { @@ -70,6 +84,7 @@ export const createFlashingColumnCellComponent = ( initialRef.current = false; const flashTimeoutIdRef = React.useRef(); + const flashDirectionRef = React.useRef(); const fadeTimeoutIdRef = React.useRef(); useEffectWhen( @@ -77,67 +92,67 @@ export const createFlashingColumnCellComponent = ( if (value === oldValueRef.current) { return; } - if (flashTimeoutIdRef.current) { - clearTimeout(flashTimeoutIdRef.current); - } - if (fadeTimeoutIdRef.current) { - clearTimeout(fadeTimeoutIdRef.current); - } - const el = htmlElementRef.current; + const clear = () => { + const el = htmlElementRef.current; + + if (flashTimeoutIdRef.current) { + clearTimeout(flashTimeoutIdRef.current); + flashTimeoutIdRef.current = undefined; + } + if (fadeTimeoutIdRef.current) { + clearTimeout(fadeTimeoutIdRef.current); + fadeTimeoutIdRef.current = undefined; + } + + if (flashDirectionRef.current && el) { + const flashDirection = flashDirectionRef.current; + + const internalflashCls = + INTERNAL_FLASH_CLS_FOR_DIRECTION[flashDirection]; + + el.classList.remove( + ...(flashClassName + ? [flashClassName, ...internalflashCls] + : internalflashCls), + ); + el.style.removeProperty(currentFlashingDurationVar); + + flashDirectionRef.current = undefined; + } + }; oldValueRef.current = value; + const el = htmlElementRef.current; if (!el) { return; } - const flashDirection = + clear(); + + const flashDirection: FlashDirection = typeof value === 'number' ? value > oldValue ? 'up' : 'down' : 'neutral'; - const internalflashCls = FlashingColumnCellRecipe({ - direction: flashDirection, - }).split(' '); + const internalflashCls = + INTERNAL_FLASH_CLS_FOR_DIRECTION[flashDirection]; el.style.setProperty(currentFlashingDurationVar, `${duration}`); - if (flashClassName) { - el.classList.add(flashClassName); - } - - el.classList.add(...internalflashCls); - - flashTimeoutIdRef.current = setTimeout(() => { - flashTimeoutIdRef.current = undefined; - - if (flashClassName) { - el.classList.remove(flashClassName); - } - el.classList.remove(...internalflashCls); - el.style.removeProperty(currentFlashingDurationVar); - // if (!fadeDuration || !fadeClassName) { - // return; - // } - // el.classList.add(fadeClassName); + el.classList.add( + ...(flashClassName + ? [flashClassName, ...internalflashCls] + : internalflashCls), + ); - // fadeTimeoutIdRef.current = setTimeout(() => { - // el.classList.remove(fadeClassName); - // fadeTimeoutIdRef.current = undefined; - // }, fadeDuration); - }, duration); + flashDirectionRef.current = flashDirection; + flashTimeoutIdRef.current = setTimeout(clear, duration); - return () => { - if (flashTimeoutIdRef.current) { - clearTimeout(flashTimeoutIdRef.current); - } - if (fadeTimeoutIdRef.current) { - clearTimeout(fadeTimeoutIdRef.current); - } - }; + return clear; }, { same: [columnId, rowId], diff --git a/source/src/components/InfiniteTable/components/cell.css.ts b/source/src/components/InfiniteTable/components/cell.css.ts index e63a8021..9a271c5c 100644 --- a/source/src/components/InfiniteTable/components/cell.css.ts +++ b/source/src/components/InfiniteTable/components/cell.css.ts @@ -158,6 +158,7 @@ export const FlashingColumnCellRecipe = recipe({ '100%': { opacity: 0 }, }), ), + animationFillMode: 'forwards', animationDuration: `calc(1ms * ${fallbackVar( InternalVars.currentFlashingDuration, diff --git a/source/src/components/InfiniteTable/vars-common.css.ts b/source/src/components/InfiniteTable/vars-common.css.ts new file mode 100644 index 00000000..e700e5ed --- /dev/null +++ b/source/src/components/InfiniteTable/vars-common.css.ts @@ -0,0 +1,7 @@ +import { ThemeVars } from './vars.css'; + +export const CommonThemeVars = { + [ThemeVars.components.Cell.flashingBackground]: ThemeVars.color.accent, + [ThemeVars.components.Cell.flashingUpBackground]: ThemeVars.color.success, + [ThemeVars.components.Cell.flashingDownBackground]: ThemeVars.color.error, +}; diff --git a/source/src/components/InfiniteTable/vars-minimalist-light.css.ts b/source/src/components/InfiniteTable/vars-minimalist-light.css.ts index 7c5710b5..4e7e058f 100644 --- a/source/src/components/InfiniteTable/vars-minimalist-light.css.ts +++ b/source/src/components/InfiniteTable/vars-minimalist-light.css.ts @@ -1,7 +1,8 @@ import { ThemeVars } from './vars.css'; - +import { CommonThemeVars } from './vars-common.css'; const borderColor = '#EDF2F7'; // chakra gray 100 export const MinimalistLightVars = { + ...CommonThemeVars, [ThemeVars.background]: 'white', [ThemeVars.color.color]: '#2D3748', // chakra gray 700 [ThemeVars.components.Row.background]: 'transparent', diff --git a/source/src/components/InfiniteTable/vars-ocean-dark.css.ts b/source/src/components/InfiniteTable/vars-ocean-dark.css.ts index 0f75133c..92799fcd 100644 --- a/source/src/components/InfiniteTable/vars-ocean-dark.css.ts +++ b/source/src/components/InfiniteTable/vars-ocean-dark.css.ts @@ -8,6 +8,8 @@ export const OceanDarkVars = { [ThemeVars.background]: '#032c4f', [ThemeVars.components.Menu.separatorColor]: borderColor, [ThemeVars.color.color]: '#96a0aa', + [ThemeVars.color.success]: '#176417', + [ThemeVars.color.error]: '#5e1414', [ThemeVars.components.Cell.borderTop]: `1px solid ${borderColor}`, [ThemeVars.components.HeaderCell.background]: '#04233d', [ThemeVars.components.HeaderCell.hoverBackground]: '#021f35', diff --git a/source/src/components/InfiniteTable/vars-ocean-light.css.ts b/source/src/components/InfiniteTable/vars-ocean-light.css.ts index f6a70a7a..5c87095e 100644 --- a/source/src/components/InfiniteTable/vars-ocean-light.css.ts +++ b/source/src/components/InfiniteTable/vars-ocean-light.css.ts @@ -1,12 +1,17 @@ +import { CommonThemeVars } from './vars-common.css'; import { ThemeVars } from './vars.css'; const borderColor = `color-mix(in srgb, transparent, ${ThemeVars.color.color} 10%)`; export const OceanLightVars = { + ...CommonThemeVars, // TODO we need to implement ocean light theme [ThemeVars.color.accent]: '#8b5cf6', [ThemeVars.background]: '#d1e8fc', [ThemeVars.color.color]: '#04233d', + [ThemeVars.color.success]: '#64ce64', + [ThemeVars.color.error]: '#fc6565', + [ThemeVars.components.HeaderCell.background]: '#7dd3fc', // tw sky [ThemeVars.components.HeaderCell.hoverBackground]: '#38bdf8', diff --git a/www/content/blog/2024/10/10/how-do-i-flash-cells.md b/www/content/blog/2024/10/10/how-do-i-flash-cells.md new file mode 100644 index 00000000..8ef34b3e --- /dev/null +++ b/www/content/blog/2024/10/10/how-do-i-flash-cells.md @@ -0,0 +1,37 @@ +--- +title: Flashing column cells in Infinite Table +author: admin +draft: true +--- + +Flashing cells is an important feature that has been requested by some of our users - both [in public](https://github.com/infinite-table/infinite-react/issues/250) and private conversations. + +It's also a very useful addition for DataGrids users that work in the financial industry. Version `5.0.0` of `` shipped flashing and in this blogpost we want to show how to use it. + +## Configuring a flashing column. + +In order to configure a column to flash its cells when the data changes, you need to specify a custom `ColumnCell` component. + +```tsx + +import { FlashingColumnCell } from '@infinite-table/infinite-react'; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + }, + firstName: { + field: 'firstName', + }, + monthlyBonus: { + field: 'monthlyBonus', + components: { + ColumnCell: FlashingColumnCell, + } + }, +}; +``` + +`@infinite-table/infinite-react` exports a `FlashingColumnCell` React component that you can pass to the `components.ColumnCell` prop of any column you want to flash. + +