From 6d4dd8c580581095210203a0a06ee8c0227d309d Mon Sep 17 00:00:00 2001 From: gizeasy Date: Mon, 20 Oct 2025 17:33:32 +0300 Subject: [PATCH 1/5] docs(Table): filter --- .../Table/__stand__/Table.dev.stand.mdx | 167 ++++++++++++++++++ .../TableExampleFilter/TableExampleFilter.tsx | 152 ++++++++++++++++ yarn.lock | 6 +- 3 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 src/components/Table/__stand__/examples/TableExampleFilter/TableExampleFilter.tsx diff --git a/src/components/Table/__stand__/Table.dev.stand.mdx b/src/components/Table/__stand__/Table.dev.stand.mdx index 0ed199f..28feb65 100644 --- a/src/components/Table/__stand__/Table.dev.stand.mdx +++ b/src/components/Table/__stand__/Table.dev.stand.mdx @@ -22,6 +22,7 @@ import { TableExampleLoadingDataWithSkeleton } from './examples/TableExampleLoad import { TableExampleLoadingRowWithLoader } from './examples/TableExampleLoadingRowWithLoader/TableExampleLoadingRowWithLoader'; import { TableExampleLoadingRowWithSkeleton } from './examples/TableExampleLoadingRowWithSkeleton/TableExampleLoadingRowWithSkeleton'; import { TableExampleLoadingNestedRowWithLoader } from './examples/TableExampleLoadingNestedRowWithLoader/TableExampleLoadingNestedRowWithLoader'; +import { TableExampleFilter } from './examples/TableExampleFilter/TableExampleFilter'; import { TableExampleResizableInside, TableExampleResizableOutside, @@ -55,6 +56,7 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; - [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) - [Ключ строки](#ключ-строки) - [Адаптивная ширина колонок](#адаптивная-ширина-колонок) +- [Фильтрация](#фильтрация) @@ -2812,6 +2814,171 @@ export const TableExampleAdaptiveColumns = () => { +## Фильтрация + +Для фильтрации используйте в `renderHeaderCell` компонент [`FlatSelect`](##LIBS.LIB.STAND/lib:uikit/stand:components-flatselect-stable) + + + + + +```tsx +import { IconFunnel } from '@consta/icons/IconFunnel'; +import { Example } from '@consta/stand'; +import { Button } from '@consta/uikit/Button'; +import { FlatSelect } from '@consta/uikit/FlatSelect'; +import { atom } from '@reatom/core'; +import { useAtom } from '@reatom/npm-react'; +import React, { useRef } from 'react'; + +import { HeaderDataCell } from '@consta/table/HeaderDataCell'; +import { Table, TableColumn, TableRenderHeaderCell } from '@consta/table/Table'; + +const dataAtom = atom([ + { + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]); + +const statusFilterValueAtom = atom([]); +const nameFilterValueAtom = atom([]); + +const rowsAtom = atom((ctx) => { + const data = ctx.spy(dataAtom); + const statusFilterValue = ctx.spy(statusFilterValueAtom); + const nameFilterValue = ctx.spy(nameFilterValueAtom); + + return data.filter((row) => { + return ( + (nameFilterValue.length + ? nameFilterValue.some((el) => + row.name.toLowerCase().includes(el.toLowerCase()), + ) + : true) && + (statusFilterValue.length + ? statusFilterValue.some((el) => + row.status.toLowerCase().includes(el.toLowerCase()), + ) + : true) + ); + }); +}); + +type ROW = { + name: string; + profession: string; + status: string; +}; + +const nameFilterItems = ['Антон Григорьев', 'Василий Пупкин']; +const statusFilterItems = ['недоступен', 'на связи']; + +const NameHeaderCell: TableRenderHeaderCell = ({ title }) => { + const buttonRef = useRef(null); + const [nameFilterValue, setNameFilterValue] = useAtom(nameFilterValueAtom); + return ( + <> + + } + > + {title} + + item} + getItemKey={(item) => item} + onChange={(value) => setNameFilterValue(value || [])} + items={nameFilterItems} + multiple + style={{ zIndex: 10 }} + /> + + ); +}; + +const StatusHeaderCell: TableRenderHeaderCell = ({ title }) => { + const buttonRef = useRef(null); + const [statusFilterValue, setStatusFilterValue] = useAtom( + statusFilterValueAtom, + ); + return ( + <> + + } + > + {title} + + item} + getItemKey={(item) => item} + onChange={(value) => setStatusFilterValue(value || [])} + items={statusFilterItems} + multiple + style={{ zIndex: 10 }} + /> + + ); +}; + +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + width: 240, + renderHeaderCell: NameHeaderCell, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + renderHeaderCell: StatusHeaderCell, + }, +]; + +export const TableExampleFilter = () => { + const [rows] = useAtom(rowsAtom); + return ( + + + + ); +}; +``` + + + ## Свойства ```ts diff --git a/src/components/Table/__stand__/examples/TableExampleFilter/TableExampleFilter.tsx b/src/components/Table/__stand__/examples/TableExampleFilter/TableExampleFilter.tsx new file mode 100644 index 0000000..421d42a --- /dev/null +++ b/src/components/Table/__stand__/examples/TableExampleFilter/TableExampleFilter.tsx @@ -0,0 +1,152 @@ +import { IconFunnel } from '@consta/icons/IconFunnel'; +import { Example } from '@consta/stand'; +import { Button } from '@consta/uikit/Button'; +import { FlatSelect } from '@consta/uikit/FlatSelect'; +import { atom } from '@reatom/core'; +import { useAtom } from '@reatom/npm-react'; +import React, { useRef } from 'react'; + +import { HeaderDataCell } from '##/components/HeaderDataCell'; +import { Table, TableColumn, TableRenderHeaderCell } from '##/components/Table'; + +const dataAtom = atom([ + { + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]); + +const statusFilterValueAtom = atom([]); +const nameFilterValueAtom = atom([]); + +const rowsAtom = atom((ctx) => { + const data = ctx.spy(dataAtom); + const statusFilterValue = ctx.spy(statusFilterValueAtom); + const nameFilterValue = ctx.spy(nameFilterValueAtom); + + return data.filter((row) => { + return ( + (nameFilterValue.length + ? nameFilterValue.some((el) => + row.name.toLowerCase().includes(el.toLowerCase()), + ) + : true) && + (statusFilterValue.length + ? statusFilterValue.some((el) => + row.status.toLowerCase().includes(el.toLowerCase()), + ) + : true) + ); + }); +}); + +type ROW = { + name: string; + profession: string; + status: string; +}; + +const nameFilterItems = ['Антон Григорьев', 'Василий Пупкин']; +const statusFilterItems = ['недоступен', 'на связи']; + +const NameHeaderCell: TableRenderHeaderCell = ({ title }) => { + const buttonRef = useRef(null); + const [nameFilterValue, setNameFilterValue] = useAtom(nameFilterValueAtom); + return ( + <> + + } + > + {title} + + item} + getItemKey={(item) => item} + onChange={(value) => setNameFilterValue(value || [])} + items={nameFilterItems} + multiple + style={{ zIndex: 10 }} + /> + + ); +}; + +const StatusHeaderCell: TableRenderHeaderCell = ({ title }) => { + const buttonRef = useRef(null); + const [statusFilterValue, setStatusFilterValue] = useAtom( + statusFilterValueAtom, + ); + return ( + <> + + } + > + {title} + + item} + getItemKey={(item) => item} + onChange={(value) => setStatusFilterValue(value || [])} + items={statusFilterItems} + multiple + style={{ zIndex: 10 }} + /> + + ); +}; + +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + width: 240, + renderHeaderCell: NameHeaderCell, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + renderHeaderCell: StatusHeaderCell, + }, +]; + +export const TableExampleFilter = () => { + const [rows] = useAtom(rowsAtom); + return ( + +
+ + ); +}; diff --git a/yarn.lock b/yarn.lock index 80788cf..eca9aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1873,9 +1873,9 @@ workbox-webpack-plugin "^6.4.1" "@consta/uikit@^5.22.0": - version "5.23.0" - resolved "https://registry.yarnpkg.com/@consta/uikit/-/uikit-5.23.0.tgz#81fab63df3f3dd05a55e8cf67d7d33991854a289" - integrity sha512-gH5cIYQbniUhMLtmIYCDg3N7UvEBYdLMS6wmZ6OWr6X1muSoOfICxPI6P9ECPCxcn62DUe5C8F2yRV1WaSsSHA== + version "5.26.0" + resolved "https://registry.yarnpkg.com/@consta/uikit/-/uikit-5.26.0.tgz#db188813404cd58486ceae7b7a3a44172cc390fb" + integrity sha512-rbkrl4PPVsg+Ly8UtC4B8y1VD0cbOHFGTpNrDg6qsUyuSAqT1e80XGgzefACmEmlh4NDt9kexWiUIpdlckvZ2Q== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From 5ed3fffe7c6e4d10a6b53cae509d68d0e2bda272 Mon Sep 17 00:00:00 2001 From: alyonurchick1 Date: Wed, 5 Nov 2025 14:36:22 +0300 Subject: [PATCH 2/5] docs: fix table --- .../Table/__stand__/Table.dev.stand.mdx | 3021 ++++++++--------- 1 file changed, 1490 insertions(+), 1531 deletions(-) diff --git a/src/components/Table/__stand__/Table.dev.stand.mdx b/src/components/Table/__stand__/Table.dev.stand.mdx index 28feb65..a3afa68 100644 --- a/src/components/Table/__stand__/Table.dev.stand.mdx +++ b/src/components/Table/__stand__/Table.dev.stand.mdx @@ -32,9 +32,13 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; +- [Обзор](#обзор) +- [Импорт](#импорт) - [Свойства](#свойства) -- [Как формируется таблица](#как-формируется-таблица) -- [Колонка](#колонка) +- [Содержимое](#содержимое) + - [Строки и колонки](#строки-и-колонки) + - [Колонка](#колонка) +- [Внешний вид](#внешний-вид) - [Ширина колонки](#ширина-колонки) - [Разделитель колонок](#разделитель-колонок) - [Закрепление колонок](#закрепление-колонок) @@ -42,75 +46,54 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; - [Представление данных в ячейке](#представление-данных-в-ячейке) - [Представление данных в ячейке в шапке](#представление-данных-в-ячейке-в-шапке) - [Объединение ячеек](#объединение-ячеек) -- [Закрепление шапки](#закрепление-шапки) -- [Виртуальный скролл](#виртуальный-скролл) -- [onScrollToBottom](#onScrollToBottom) -- [Таблица в полоску](#таблица-в-полоску) -- [Выделение строк](#выделение-строк) - - [Наведение на строку](#наведение-на-строку) - - [Выбор строки](#выбор-строки) -- [Управление шириной столбцов](#управление-шириной-столбцов) -- [headerZIndex](#headerzindex) -- [Вложенные строки](#вложенные-строки) -- [Рендер строки](#рендер-строки) -- [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) -- [Ключ строки](#ключ-строки) -- [Адаптивная ширина колонок](#адаптивная-ширина-колонок) -- [Фильтрация](#фильтрация) + - [Закрепление шапки](#закрепление-шапки) + - [Таблица в полоску](#таблица-в-полоску) + - [Управление шириной столбцов](#управление-шириной-столбцов) + - [headerZIndex](#headerzindex) + - [Вложенные строки](#вложенные-строки) + - [Рендер строки](#рендер-строки) + - [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) + - [Адаптивная ширина колонок](#адаптивная-ширина-колонок) +- [Поведение](#поведение) + - [Виртуальный скролл](#виртуальный-скролл) + - [onScrollToBottom](#onscrolltobottom) + - [Выделение строк](#выделение-строк) + - [Состояние загрузки](#состояние-загрузки) + - [Ключ строки](#ключ-строки) + - [Фильтрация](#фильтрация) +- [Пример использования](#пример-использования) -## Как формируется таблица +## Обзор -Для вывода самой простой таблицы, используйте 2 свойства: - -- `rows` - строки. Тип строки может быть любым массивом объекта `Record`. -- `columns`- колонки. Представляют массив объектов, у которого `accessor` это свойство объекта ваших данных. +Компонент Table предназначен для отображения табличных данных. Он поддерживает настройку колонок, строк, виртуальную прокрутку, закрепление элементов и другие функции для работы с большими наборами данных. - - - +## Импорт ```tsx import { Table, TableColumn } from '@consta/table/Table'; - -type Row = { name: string; profession: string; status: string }; - -const rows: Row[] = [ - { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; - -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - }, - { - title: 'Профессия', - accessor: 'profession', - }, - { - title: 'Статус', - accessor: 'status', - }, -]; - -export const TableExampleSimple = () =>
; ``` - - ## Свойства +| Свойство | Тип | По умолчанию | Описание | +| -------------------------------------------- | ----------------------------- | ----------------- | ------------------------------------------------------------ | +| [`columns?`](#колонка) | `TableColumn[]` | - | Колонки | +| [`rows?`](#строки-и-колонки) | `ROW[]` | - | Строки | +| [`getRowKey?`](#ключ-строки) | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | +| [`onRowMouseEnter?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | +| [`onRowMouseLeave?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | +| [`onRowClick?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onClick` на строке | +| [`virtualScroll?`](#виртуальный-скролл) | `boolean` | - | Включение виртуальной прокрутки | +| [`stickyHeader?`](#закрепление-шапки) | `boolean` | - | Зафиксировать шапку сверху | +| [`resizable?`](#управление-шириной-столбцов) | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | +| [`zebraStriped?`](#таблица-в-полоску) | `boolean` | - | Окрашивание строк через одну | +| [`headerZIndex?`](#headerzindex) | `number` | `1` | `zIndex` шапки | +| [`rowHoverEffect?`](#выделение-строк) | `boolean` | - | Включает эффект наведения на строку | +| `className?` | `string` | - | Дополнительный CSS-класс | +| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | + ```ts export type TableRenderHeaderCell = (props: { title?: string; @@ -157,24 +140,58 @@ type TableRowMouseEvent = ( type GetRowKey = (row: ROW) => string | number; ``` -| Свойство | Тип | По умолчанию | Описание | -| ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------ | -| `columns?` | `TableColumn[]` | - | Колонки | -| `rows?` | `ROW[]` | - | Строки | -| `getRowKey?` | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | -| `onRowMouseEnter?` | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | -| `onRowMouseLeave?` | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | -| `onRowClick?` | `TableRowMouseEvent` | - | Событие `onClick` на строке | -| `virtualScroll?` | `boolean` | - | Включение виртуальной прокрутки | -| `stickyHeader?` | `boolean` | - | Зафиксировать шапку сверху | -| `resizable?` | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | -| `zebraStriped?` | `boolean` | - | Окрашивание строк через одну | -| `headerZIndex?` | `number` | `1` | `zIndex` шапки | -| `rowHoverEffect?` | `boolean` | - | Включает эффект наведения на строку | -| `className?` | `string` | - | Дополнительный CSS-класс | -| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | - -## Колонка +## Содержимое + +### Строки и колонки + +Для вывода самой простой таблицы, используйте 2 свойства: + +- `rows` - строки. Тип строки может быть любым массивом объекта `Record`. +- `columns`- колонки. Представляют массив объектов, у которого `accessor` это свойство объекта ваших данных. + + + + + +```tsx +import { Table, TableColumn } from '@consta/table/Table'; + +type Row = { name: string; profession: string; status: string }; + +const rows: Row[] = [ + { + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; + +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + }, + { + title: 'Профессия', + accessor: 'profession', + }, + { + title: 'Статус', + accessor: 'status', + }, +]; + +export const TableExampleSimple = () =>
; +``` + + + +### Колонка Колонки формируются с помощью CSS-свойств `display: grid` и `grid-template-columns`. @@ -220,7 +237,9 @@ export type TableColumn = { | `columns` | Вложенные колонки | | [`colSpan`](#объединение-ячеек) | Функция для вывода количества занятых колонок ячейкой | -## Ширина колонки +## Внешний вид + +### Ширина колонки Для настройки ширины колонок используйте `width`, `maxWidth`, `minWidth`, где `width` — это желаемая ширина колонки. Если все колонки будут помещаться в таблицу без скролла, она останется неизменной. Чтобы таблица не уменьшала или не увеличивала колонку вне желаемых размеров, ограничьте ее, используя `maxWidth` и `minWidth`. @@ -320,7 +339,9 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleWidth = () =>
; +export const TableExampleSeparator = () => ( +
+); ``` @@ -509,7 +530,7 @@ export const TableExampleGroupColumns = () => ( ### Представление данных в ячейке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`DataCell`](##LIBS.LIB.STAND/lib:table/stand:components-datacell-stable). Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `DataCell`. Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. @@ -600,9 +621,7 @@ const columns: TableColumn[] = [ ]; export const TableExampleRenderCell = () => ( - -
- +
); ``` @@ -610,7 +629,7 @@ export const TableExampleRenderCell = () => ( ### Представление данных в ячейке в шапке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`HeaderDataCell`](##LIBS.LIB.STAND/lib:table/stand:components-headerdatacell-stable). +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `HeaderDataCell`. Вы можете в колонке использовать свойство `renderHeaderCell` и выводить данные, как вам потребуется. @@ -712,7 +731,6 @@ export const TableExampleRenderHeaderCell = () => ( import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; import { IconArrowRight } from '@consta/icons/IconArrowRight'; import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; -import { Example } from '@consta/stand'; import { Button } from '@consta/uikit/Button'; import { useMutableRef } from '@consta/uikit/useMutableRef'; import React, { useCallback, useMemo, useState } from 'react'; @@ -916,35 +934,28 @@ export const TableExampleColSpan = () => { -## Закрепление шапки +### Закрепление шапки Чтобы закрепить шапку, используйте свойство `stickyHeader` и ограничьте высоту таблицы в свойстве `maxHeight`. + + ```tsx
``` - - -## Виртуальный скролл - -Если в таблице много данных, включите свойство `virtualScroll` и ограничьте высоту таблицы для лучшей производительности. Может принимать два значения: - -- `boolean` – будет включен виртуальный скролл по горизонтали и вертикали, -- `[boolean, boolean]` – первый элемент определяет, включается ли виртуальный скролл по горизонтали, второй – по вертикали. - - - -Чтобы избежать скачков строки при включении виртуального скролла, задайте одинаковую высоту ячеек в строке. +### Таблица в полоску - +Для отображения таблицы в полоску используйте `zebraStriped`. + + ```tsx
{ columns={columns} stickyHeader virtualScroll + zebraStriped /> ``` - - -## onScrollToBottom - -Свойство `onScrollToBottom` позволяет отслеживать, когда пользователь достиг конца таблицы. Полезно для дозагрузки данных в момент достижения конца таблицы. +### Управление шириной столбцов - +Чтобы дать пользователю возможность управлять шириной столбцов, используйте свойство `resizable`. Уменьшение и увеличение будет в пределах `minWidth` и `maxWidth` у колонки. -```tsx -const TableExampleOnScrollToBottom = () => { - const [isScrollToBottom, setIsScrollToBottom] = useState(false); + - return ( - <> - - {isScrollToBottom ? 'Вы проскроллилили до конца' : 'Скролльте вниз'} - -
setIsScrollToBottom(true)} - /> - - ); -}; -``` +Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. - + - +Закрепленные колонки (`pinned`) не могут изменять размер. -## Таблица в полоску +Есть 2 режима работы: -Для отображения таблицы в полоску используйте `zebraStriped`. +- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. -```tsx -
-``` - - - - - -## Выделение строк + -Строки могут иметь состояние: +```tsx +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; -- Наведение на строку – [`hover`](#наведение-на-строку), -- Выбор строки - [`active`](#выбор-строки) +type ROW = { + athlete: string; + age: number | null; + country: string; +}; - +const columns: TableColumn[] = [ + { + title: 'Имя', + width: 'auto', + accessor: 'athlete', + // Запретили менять ширину у колонки + minWidth: 200, + maxWidth: 200, + }, + { + title: 'Страна', + accessor: 'country', + width: 'auto', + minWidth: 140, + }, + { + title: 'Возраст', + accessor: 'age', + width: 'auto', + minWidth: 100, + }, +]; -Чтобы не рендерить всю таблицу при каждом изменении данных в ячейке, используйте [стейт-менеджер](https://www.reatom.dev/blog/what-is-state-manager/) или [контекст](https://react.dev/reference/react/createContext). +export const TableExampleResizableInside = () => ( +
+); +``` - + -Пример с наведением и выбором строки: +- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. - + ```tsx -import { Example } from '@consta/stand'; -import { action, atom, AtomMut } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; - -import { DataCell } from '@consta/table/DataCell'; -import { DataNumberingCell } from '@consta/table/DataNumberingCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - -// Types +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; type ROW = { - id: number; - name: string; - profession: string; - status: string; - hover: AtomMut; - active: AtomMut; -}; - -// Atoms -const rowsAtom = atom[]>([ - atom({ - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - hover: atom(false), - active: atom(false), - }), - atom({ - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - hover: atom(false), - active: atom(false), - }), -]); - -// Actions - -const onRowClickAction = action<[AtomMut]>((ctx, rowAtom) => { - const row = ctx.get(rowAtom); - row.active(ctx, !ctx.get(row.active)); -}); - -const onRowMouseEnterAction = action<[AtomMut]>((ctx, rowAtom) => { - ctx.get(rowAtom).hover(ctx, true); -}); - -const onRowMouseLeaveAction = action<[AtomMut]>((ctx, rowAtom) => { - ctx.get(rowAtom).hover(ctx, false); -}); - -const DataCellName: TableRenderCell> = (props) => { - const [row] = useAtom(props.row); - const [active] = useAtom(row.active); - const [hover] = useAtom(row.hover); - - return ( - - {row.id} - - ); -}; - -const createDataCellOther = ( - accessor: Exclude, -) => { - const Component: TableRenderCell> = (props) => { - const [row] = useAtom(props.row); - - return {row[accessor]}; - }; - - return Component; -}; - -const columns: TableColumn>[] = [ - { - title: '', - accessor: 'id', - width: 48, - maxWidth: 48, - minWidth: 48, - renderCell: DataCellName, - }, - { - title: 'Имя', - accessor: 'name', - width: 240, - renderCell: createDataCellOther('name'), - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - renderCell: createDataCellOther('profession'), - }, - { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, - renderCell: createDataCellOther('status'), - }, -]; - -export const TableExampleActiveRowWithNumbering = () => { - const onRowClick = useAction(onRowClickAction); - const onRowMouseEnter = useAction(onRowMouseEnterAction); - const onRowMouseLeave = useAction(onRowMouseLeaveAction); - const [rows] = useAtom(rowsAtom); - - return ( - -
- - ); -}; -``` - - - -### Наведение на строку - -Есть 2 способа задать состояние `hover` строкам: - -- Автоматически, с помощью Свойства `rowHoverEffect`. В этом случае все строки будут подсвечиваться при наведении мыши. - -Пример автоматического способа: - - - - - -```tsx -import React from 'react'; - -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; - -type ROW = { - athlete: string; - age: number | null; - country: string; - year: number; - date: string; - sport: string; - gold: number; - silver: number; - bronze: number; - total: number; + athlete: string; + age: number | null; + country: string; }; const columns: TableColumn[] = [ @@ -1200,140 +1057,244 @@ const columns: TableColumn[] = [ title: 'Имя', width: 'auto', accessor: 'athlete', + minWidth: 200, }, { title: 'Страна', accessor: 'country', width: 'auto', + minWidth: 140, + pinned: 'left', }, { title: 'Возраст', accessor: 'age', + width: 'auto', minWidth: 100, }, + { + title: 'Медали', + columns: [ + { + title: 'Бронза', + accessor: 'bronze', + width: 'auto', + minWidth: 100, + }, + { + title: 'Серебро', + accessor: 'silver', + width: 'auto', + minWidth: 100, + }, + { + title: 'Золото', + accessor: 'gold', + width: 'auto', + minWidth: 100, + }, + { + title: 'Всего', + accessor: 'total', + width: 'auto', + minWidth: 100, + }, + ], + }, + { + title: 'Год', + accessor: 'year', + width: 'auto', + minWidth: 140, + pinned: 'right', + }, ]; -export const TableExampleRowHoverEffect = () => ( +export const TableExampleResizableOutside = () => (
); ``` -- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. +### headerZIndex -Пример подконтрольного способа: +Свойство `headerZIndex` влияет на `z-index` следующих элементов: - +- Шапка таблицы. +- Ячейки в закрепленных колонках `z-index: calc(var(--table-header-z-index) - 1)`. +- Ячейки в закрепленных колонках в шапке `z-index: calc(var(--table-header-z-index) + 1)`. +- Ресайзеры колонок `z-index: calc(var(--table-header-z-index) + 10)`. - + -```tsx -import { action, atom } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; +Если вы используете всплывающие окна (`Popover`, `Tooltip`), привязывайте их `z-index` к переменной `--table-header-z-index`. +Например: `z-index: calc(var(--table-header-z-index) + 1)`. Таким образом плавающий элемент будет находится под шапкой таблицы. -import { DataNumberingCell } from '@consta/table/DataNumberingCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; + -// Types +### Вложенные строки -type ROW = { - id: number; - name: string; - profession: string; - status: string; -}; +Реализовать вложенные строки возможно через `renderCell` у колонки, взяв компонент `DataCell` и назначив ему свойство `level`. -const rows: ROW[] = [ - { - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; + -// Atoms + -const hoverIdAtom = atom(undefined); +```tsx +import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; +import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Button } from '@consta/uikit/Button'; +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import React, { useCallback, useMemo, useState } from 'react'; -// Actions +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; +import { range } from '##/utils/array'; -const onRowMouseEnterAction = action<[ROW]>((ctx, row) => - hoverIdAtom(ctx, row.id), -); +type ROW = { + idx: number; + col1: string; + col2: string; + col3: string; + parent: number | undefined; + level: number; +}; -const onRowMouseLeaveAction = action<[ROW]>((ctx, row) => { - const hoverId = ctx.get(hoverIdAtom); - if (hoverId === row.id) { - hoverIdAtom(ctx, undefined); - } +const IconArrow = withAnimateSwitcherHOC({ + startIcon: IconArrowRight, + startDirection: 0, + endDirection: 90, }); -const DataCellName: TableRenderCell = (props) => { - const [hover] = useAtom((ctx) => { - const hoverId = ctx.spy(hoverIdAtom); - return hoverId === props.row.id; - }); +const getDataCell = (idx: number): ROW => { + const parent = Math.floor(idx / 10) * 10; - return ( - {props.row.id} - ); + return { + idx, + col1: `Данные 1 - ${idx}`, + col2: `Данные 2 - ${idx}`, + col3: `Данные 3 - ${idx}`, + parent: parent === idx ? undefined : parent, + level: parent === idx ? 0 : 1, + }; }; -const columns: TableColumn[] = [ - { - title: '', - accessor: 'id', - width: 48, - maxWidth: 48, - minWidth: 48, - renderCell: DataCellName, - }, - { - title: 'Имя', - accessor: 'name', - width: 240, - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - }, - { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, - }, -]; +const data = range(100000).map(getDataCell); -export const TableExampleHoveredControlled = () => { - const onRowMouseEnter = useAction(onRowMouseEnterAction); - const onRowMouseLeave = useAction(onRowMouseLeaveAction); +const DataCellCol1 = (props: { + row: { + col1: string; + parent: number | undefined; + idx: number; + level: number; + }; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { + row: { col1, parent, idx, level }, + opened, + toggle, + } = props; + + return ( + + toggle(idx)} + /> + ) : undefined + } + > + {col1} + + + ); +}; + +export const TableExampleNestedRows = () => { + const [openedList, setOpenedList] = useState([]); + + const openedListRef = useMutableRef(openedList); + + const rows = useMemo(() => { + return data.filter( + (dataItem) => + dataItem.parent === undefined || + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.parent, + ) !== -1, + ); + }, [openedList]); + + const toggle = useCallback((idx: number) => { + setOpenedList((state) => { + const open = state.findIndex((value) => value === idx) !== -1; + if (open) { + return state.filter((value) => value !== idx); + } + return [...state, idx]; + }); + }, []); + + const renderCellCol1: TableRenderCell = useCallback( + (props) => ( + item === props.row.idx) !== + -1 + } + /> + ), + [], + ); + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Колонка - 1', + accessor: 'col1', + renderCell: renderCellCol1, + }, + { + title: 'Колонка - 2', + accessor: 'col2', + }, + { + title: 'Колонка - 3', + accessor: 'col3', + }, + ], + [], + ); return (
row.idx} /> ); }; @@ -1341,699 +1302,849 @@ export const TableExampleHoveredControlled = () => { -### Выбор строки - -Для выбора строки укажите в любой ячейке строки атрибут `data-row-active='true'`. +### Рендер строки -Пример с использованием стейт-менеджера: +Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. - + ```tsx -import { Example } from '@consta/stand'; -import { Checkbox } from '@consta/uikit/Checkbox'; -import { atom, AtomMut } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; +import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; +import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Button } from '@consta/uikit/Button'; +import { Grid, GridItem } from '@consta/uikit/Grid'; +import { cnMixSpace } from '@consta/uikit/MixSpace'; +import { Text } from '@consta/uikit/Text'; +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import React, { useCallback, useMemo, useState } from 'react'; import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type ROW = { - id: number; - name: string; - profession: string; - status: string; -}; - -const activeIdsAtom = atom>>({}); +const IconArrow = withAnimateSwitcherHOC({ + startIcon: IconArrowRight, + startDirection: 0, + endDirection: 90, +}); -const DataCellName: TableRenderCell = (props) => { - const [active] = useAtom((ctx) => { - const activeAtom = ctx.spy(activeIdsAtom)[props.row.id]; - return activeAtom ? ctx.spy(activeAtom) : false; - }); +type Option = { label: string; value: string }; +type Options = { label: string; value: Option[] }; - const onChange = useAction((ctx) => { - const activeIds = ctx.get(activeIdsAtom); - const activeAtom = ctx.get(activeIdsAtom)[props.row.id]; +type Item = { + id: number; + label: string; + formula: string; + type: string; +}; - if (activeAtom) { - activeAtom(ctx, !ctx.get(activeAtom)); - } else { - activeIdsAtom(ctx, { ...activeIds, [props.row.id]: atom(true) }); - } - }); +type ItemInfo = { + isInfo: number; + options: Options[]; +}; - return ( - } - > - {props.row.name} - - ); +type Row = { + id?: number; + label?: string; + formula?: string; + type?: string; + status?: 'work' | 'problem' | 'wait' | 'success'; + isInfo?: number; + options?: Options[]; }; -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - width: 240, - renderCell: DataCellName, - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - }, +const data: Row[] = [ { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, + id: 1, + label: 'Запись инклинометрии', + formula: 'Время замера * Количество', + type: 'Кондуктор', }, -]; - -const rows: ROW[] = [ { - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', + isInfo: 1, + options: [ + { + label: 'Порты', + value: [ + { label: 'Входящий', value: 'A2-папа' }, + { label: 'Исходящий', value: 'A2-папа' }, + ], + }, + { + label: 'Размеры', + value: [ + { label: 'ширина(мм)', value: '60' }, + { label: 'длинна(мм)', value: '80' }, + ], + }, + ], }, { id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', + label: 'Шаблонирование при бурении', + formula: 'Интервал/Скорость СПО', + type: 'Труба бурильная', }, -]; - -export const TableExampleActiveRow = () => ( - -
- -); -``` - - - -## Управление шириной столбцов - -Чтобы дать пользователю возможность управлять шириной столбцов, используйте свойство `resizable`. Уменьшение и увеличение будет в пределах `minWidth` и `maxWidth` у колонки. + { + isInfo: 2, + options: [ + { + label: 'Диаметры', + value: [ + { label: 'Внешний', value: '14.7' }, + { label: 'Внутренний', value: '12.7' }, + ], + }, + { + label: 'Нагрузка и моменты', + value: [ + { label: 'Растягивающая нагрузка тела трубы, кН', value: '30' }, + { label: 'Допустимый момент на кручение ЗС,кН*м', value: '10' }, + { + label: 'Допустимый момент на кручение тела трубы, кН*м', + value: '10', + }, + { + label: 'Рекомендуемый момент свинчивания, кН*м', + value: '10', + }, + ], + }, + ], + }, +]; - +const isItemInfo = (arg: Row): arg is ItemInfo => + Object.prototype.hasOwnProperty.call(arg, 'isInfo'); -Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. +const isItem = (arg: Row): arg is Item => + Object.prototype.hasOwnProperty.call(arg, 'id'); - +const LabelCell = (props: { + id: number; + label: string; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { id, opened, toggle, label } = props; -Закрепленные колонки (`pinned`) не могут изменять размер. + return ( + + toggle(id)} + /> + } + > + {label} + + + ); +}; -Есть 2 режима работы: +const InfoCell = (props: { options: Options[] }) => { + const { options } = props; + return ( + + {options.map((opt) => { + return ( + <> + + {opt.label} + + {opt.value.map((val) => ( + + {val.value} + + {val.label} + + + ))} + + ); + })} + + ); +}; -- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. +export const TableExampleRenderRow = () => { + const [openedList, setOpenedList] = useState([]); + + const openedListRef = useMutableRef(openedList); + + const rows = useMemo(() => { + return data.filter( + (dataItem) => + Object.prototype.hasOwnProperty.call(dataItem, 'id') || + (isItemInfo(dataItem) && + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.isInfo, + ) !== -1), + ); + }, [openedList]); + + const toggle = useCallback((idx: number) => { + setOpenedList((state) => { + const open = state.findIndex((value) => value === idx) !== -1; + if (open) { + return state.filter((value) => value !== idx); + } + return [...state, idx]; + }); + }, []); + + const renderLabelCell: TableRenderCell = useCallback(({ row }) => { + if (isItem(row)) { + return ( + id === row.id) !== -1} + toggle={toggle} + /> + ); + } + if (isItemInfo(row)) { + return ; + } + return null; + }, []); + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Название', + accessor: 'label', + renderCell: renderLabelCell, + colSpan: ({ row }) => (isItemInfo(row) ? 'end' : 1), + minWidth: 300, + }, + { + title: 'Формула', + accessor: 'formula', + minWidth: 200, + }, + { + title: 'Тип', + accessor: 'type', + minWidth: 180, + }, + ], + [], + ); + + return ( +
{ + if (isItemInfo(row)) { + return `${row.isInfo}-info`; + } + if (isItem(row)) { + return `${row.id}`; + } + return ''; + }} + /> + ); +}; +``` + + + +### Индикатор ячейки и подсказки + +Для отображения индикатора используйте свойство `indicator` у компонента `DataCell`. Для вывода подсказок используйте компонент `Popover`. В примере ниже реализована логика показа подсказок по наведению курсора. - + + +```tsx +import { Informer } from '@consta/uikit/Informer'; +import { + animateTimeout, + cnMixPopoverAnimate, +} from '@consta/uikit/MixPopoverAnimate'; +import { Popover } from '@consta/uikit/Popover'; +import { useDebounce } from '@consta/uikit/useDebounce'; +import { useFlag } from '@consta/uikit/useFlag'; +import React, { useCallback, useRef } from 'react'; +import { Transition } from 'react-transition-group'; + +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn } from '@consta/table/Table'; + +type Cell = { + data: T; + status?: 'alert' | 'warning'; + statusMessage?: string; +}; + +type Row = { + name: Cell; + profession: Cell; + status: Cell; +}; + +const rows: Row[] = [ + { + name: { + data: 'Антон', + }, + profession: { + data: 'РОЮЛАВТМЯО', + status: 'alert', + statusMessage: 'Неизвестное название диапазона: РОЮЛАВТМЯО.', + }, + status: { + data: 'недоступен', + }, + }, + // ... +]; + +const titleMap: Record<'alert' | 'warning', string> = { + alert: 'Ошибка', + warning: 'Предупреждение', +}; + +const DataCellWithInformer = ({ + data, + tableRef, + statusMessage, + status, +}: { + data: string; + status?: 'alert' | 'warning'; + statusMessage?: string; + tableRef: React.RefObject; +}) => { + const cellRef = useRef(null); + const popoverContentRef = useRef(null); + const [informerVisible, setInformerVisible] = useFlag(); + + const hoverStateRef = useRef<{ + popover: boolean; + anchor: boolean; + }>({ + popover: false, + anchor: false, + }); + + const mouseEnterController = useDebounce( + useCallback(() => { + (hoverStateRef.current.anchor || hoverStateRef.current.popover) && + setInformerVisible.on(); + }, []), + 200, + ); + + const mouseLeaveController = useDebounce( + useCallback(() => { + !hoverStateRef.current.anchor && + !hoverStateRef.current.popover && + setInformerVisible.off(); + }, []), + 200, + ); + + const anchorOnMouseEnter: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.anchor = true; + mouseEnterController(); + }, []); + + const anchorOnMouseLeave: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.anchor = false; + mouseLeaveController(); + }, []); -```tsx -import { Example } from '@consta/stand'; -import React from 'react'; + const popoverOnMouseEnter: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.popover = true; + mouseEnterController(); + }, []); -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + const popoverOnMouseLeave: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.popover = false; + mouseLeaveController(); + }, []); -type ROW = { - athlete: string; - age: number | null; - country: string; + return ( + <> + + {status === 'alert' ? '#ТИПДАННЫХ?' : data} + + {status && ( + + {(animate) => ( + + + + )} + + )} + + ); }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { title: 'Имя', - width: 'auto', - accessor: 'athlete', - // Запретили менять ширину у колонки - minWidth: 200, - maxWidth: 200, + accessor: 'name', + renderCell: ({ row, tableRef }) => ( + + ), }, { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, + title: 'Профессия', + accessor: 'profession', + renderCell: ({ row, tableRef }) => ( + + ), }, { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, + title: 'Статус', + accessor: 'status', + renderCell: ({ row, tableRef }) => ( + + ), }, ]; -export const TableExampleResizable = () => ( -
-); +export const TableExampleWithIndicator = () => { + return ( +
+ ); +}; ``` -- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. +### Адаптивная ширина колонок + +Вы можете менять ширину колонок в зависимости от ширины самой таблицы. - + ```tsx -import { Example } from '@consta/stand'; -import React from 'react'; +import { Button } from '@consta/uikit/Button'; +import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; -type ROW = { - athlete: string; - age: number | null; - country: string; -}; +type Row = { name: string; profession: string; status: string }; -const columns: TableColumn[] = [ +const rows: Row[] = [ { - title: 'Имя', - width: 'auto', - accessor: 'athlete', - minWidth: 200, + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', }, { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, - pinned: 'left', + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', }, - { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, +]; + +const columnsWidthMap: Record< + 's' | 'm' | 'l', + Record<'name' | 'profession' | 'status', TableColumn> +> = { + s: { + name: { + width: 100, + minWidth: 100, + maxWidth: 100, + }, + profession: { + width: '1fr', + minWidth: 150, + }, + status: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, }, - { - title: 'Медали', - columns: [ - { - title: 'Бронза', - accessor: 'bronze', - width: 'auto', - minWidth: 100, - }, + m: { + name: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + profession: { + width: '1fr', + minWidth: 250, + }, + status: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + }, + l: { + name: { + width: 150, + minWidth: 150, + maxWidth: 150, + }, + profession: { + width: '1fr', + }, + status: { + width: 150, + minWidth: 150, + maxWidth: 150, + }, + }, +}; + +const breakpointsMap = { s: 300, m: 500, l: 760 }; +const breakpointsSequence: (keyof typeof breakpointsMap)[] = ['s', 'm', 'l']; + +export const TableExampleAdaptiveColumns = () => { + const [widthSequence, setWidthSequence] = useState(0); + + const handleWidthChange = useCallback(() => { + setWidthSequence((state) => { + const newState = state + 1; + return newState >= breakpointsSequence.length ? 0 : newState; + }); + }, []); + + const tableRef = useRef(null); + + const point = + getLastPoint( + useBreakpoints({ + ref: tableRef, + map: breakpointsMap, + isActive: true, + }), + ) || 's'; + + const columns: TableColumn[] = useMemo( + () => [ { - title: 'Серебро', - accessor: 'silver', - width: 'auto', - minWidth: 100, + title: 'Имя', + accessor: 'name', + ...columnsWidthMap[point].name, }, { - title: 'Золото', - accessor: 'gold', - width: 'auto', - minWidth: 100, + title: 'Профессия', + accessor: 'profession', + ...columnsWidthMap[point].profession, }, { - title: 'Всего', - accessor: 'total', - width: 'auto', - minWidth: 100, + title: 'Статус', + accessor: 'status', + ...columnsWidthMap[point].status, }, ], - }, - { - title: 'Год', - accessor: 'year', - width: 'auto', - minWidth: 140, - pinned: 'right', - }, -]; + [point], + ); -export const TableExampleResizableOutside = () => ( -
-); + return ( + <> +
+
+``` -type ROW = { - idx: number; - col1: string; - col2: string; - col3: string; - parent: number | undefined; - level: number; -}; + -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); +### onScrollToBottom -const getDataCell = (idx: number): ROW => { - const parent = Math.floor(idx / 10) * 10; +Свойство `onScrollToBottom` позволяет отслеживать, когда пользователь достиг конца таблицы. Полезно для дозагрузки данных в момент достижения конца таблицы. - return { - idx, - col1: `Данные 1 - ${idx}`, - col2: `Данные 2 - ${idx}`, - col3: `Данные 3 - ${idx}`, - parent: parent === idx ? undefined : parent, - level: parent === idx ? 0 : 1, - }; -}; + -const data = range(100000).map(getDataCell); + -const DataCellCol1 = (props: { - row: { - col1: string; - parent: number | undefined; - idx: number; - level: number; - }; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { - row: { col1, parent, idx, level }, - opened, - toggle, - } = props; +```tsx +const TableExampleOnScrollToBottom = () => { + const [isScrollToBottom, setIsScrollToBottom] = useState(false); return ( - - toggle(idx)} - /> - ) : undefined - } - > - {col1} - - + <> + + {isScrollToBottom ? 'Вы проскроллили до конца' : 'Скролльте вниз'} + +
setIsScrollToBottom(true)} + /> + ); }; +``` -export const TableExampleNestedRows = () => { - const [openedList, setOpenedList] = useState([]); - - const openedListRef = useMutableRef(openedList); - - const rows = useMemo(() => { - return data.filter( - (dataItem) => - dataItem.parent === undefined || - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.parent, - ) !== -1, - ); - }, [openedList]); + - const toggle = useCallback((idx: number) => { - setOpenedList((state) => { - const open = state.findIndex((value) => value === idx) !== -1; - if (open) { - return state.filter((value) => value !== idx); - } - return [...state, idx]; - }); - }, []); +### Выделение строк - const renderCellCol1: TableRenderCell = useCallback( - (props) => ( - item === props.row.idx) !== - -1 - } - /> - ), - [], - ); +Строки могут иметь состояние: - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Колонка - 1', - accessor: 'col1', - renderCell: renderCellCol1, - }, - { - title: 'Колонка - 2', - accessor: 'col2', - }, - { - title: 'Колонка - 3', - accessor: 'col3', - }, - ], - [], - ); +- Наведение на строку – `hover`, +- Выбор строки - `active` - return ( -
row.idx} - /> - ); -}; -``` + - +Чтобы не рендерить всю таблицу при каждом изменении данных в ячейке, используйте стейт-менеджер или контекст. -## Рендер строки + -Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. +Пример с наведением и выбором строки: - + ```tsx -import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; -import { IconArrowRight } from '@consta/icons/IconArrowRight'; -import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; -import { Example } from '@consta/stand'; -import { Button } from '@consta/uikit/Button'; -import { Grid, GridItem } from '@consta/uikit/Grid'; -import { cnMixSpace } from '@consta/uikit/MixSpace'; -import { Text } from '@consta/uikit/Text'; -import { useMutableRef } from '@consta/uikit/useMutableRef'; -import React, { useCallback, useMemo, useState } from 'react'; +import { action, atom, AtomMut } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; +import React from 'react'; import { DataCell } from '@consta/table/DataCell'; +import { DataNumberingCell } from '@consta/table/DataNumberingCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); - -type Option = { label: string; value: string }; -type Options = { label: string; value: Option[] }; +// Types -type Item = { +type ROW = { id: number; - label: string; - formula: string; - type: string; -}; - -type ItemInfo = { - isInfo: number; - options: Options[]; -}; - -type Row = { - id?: number; - label?: string; - formula?: string; - type?: string; - status?: 'work' | 'problem' | 'wait' | 'success'; - isInfo?: number; - options?: Options[]; + name: string; + profession: string; + status: string; + hover: AtomMut; + active: AtomMut; }; -const data: Row[] = [ - { +// Atoms +const rowsAtom = atom[]>([ + atom({ id: 1, - label: 'Запись инклинометрии', - formula: 'Время замера * Количество', - type: 'Кондуктор', - }, - { - isInfo: 1, - options: [ - { - label: 'Порты', - value: [ - { label: 'Входящий', value: 'A2-папа' }, - { label: 'Исходящий', value: 'A2-папа' }, - ], - }, - { - label: 'Размеры', - value: [ - { label: 'ширина(мм)', value: '60' }, - { label: 'длинна(мм)', value: '80' }, - ], - }, - ], - }, - { + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + hover: atom(false), + active: atom(false), + }), + atom({ id: 2, - label: 'Шаблонирование при бурении', - formula: 'Интервал/Скорость СПО', - type: 'Труба бурильная', - }, - { - isInfo: 2, - options: [ - { - label: 'Диаметры', - value: [ - { label: 'Внешний', value: '14.7' }, - { label: 'Внутренний', value: '12.7' }, - ], - }, - { - label: 'Нагрузка и моменты', - value: [ - { label: 'Растягивающая нагрузка тела трубы, кН', value: '30' }, - { label: 'Допустимый момент на кручение ЗС,кН*м', value: '10' }, - { - label: 'Допустимый момент на кручение тела трубы, кН*м', - value: '10', - }, - { - label: 'Рекомендуемый момент свинчивания, кН*м', - value: '10', - }, - ], - }, - ], - }, -]; + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + hover: atom(false), + active: atom(false), + }), +]); -const isItemInfo = (arg: Row): arg is ItemInfo => - Object.prototype.hasOwnProperty.call(arg, 'isInfo'); +// Actions -const isItem = (arg: Row): arg is Item => - Object.prototype.hasOwnProperty.call(arg, 'id'); +const onRowClickAction = action<[AtomMut]>((ctx, rowAtom) => { + const row = ctx.get(rowAtom); + row.active(ctx, !ctx.get(row.active)); +}); -const LabelCell = (props: { - id: number; - label: string; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { id, opened, toggle, label } = props; +const onRowMouseEnterAction = action<[AtomMut]>((ctx, rowAtom) => { + ctx.get(rowAtom).hover(ctx, true); +}); - return ( - - toggle(id)} - /> - } - > - {label} - - - ); -}; +const onRowMouseLeaveAction = action<[AtomMut]>((ctx, rowAtom) => { + ctx.get(rowAtom).hover(ctx, false); +}); + +const DataCellName: TableRenderCell> = (props) => { + const [row] = useAtom(props.row); + const [active] = useAtom(row.active); + const [hover] = useAtom(row.hover); -const InfoCell = (props: { options: Options[] }) => { - const { options } = props; return ( - - {options.map((opt) => { - return ( - <> - - {opt.label} - - {opt.value.map((val) => ( - - {val.value} - - {val.label} - - - ))} - - ); - })} - + + {row.id} + ); }; -export const TableExampleRenderRow = () => { - const [openedList, setOpenedList] = useState([]); - - const openedListRef = useMutableRef(openedList); +const createDataCellOther = ( + accessor: Exclude, +) => { + const Component: TableRenderCell> = (props) => { + const [row] = useAtom(props.row); - const rows = useMemo(() => { - return data.filter( - (dataItem) => - Object.prototype.hasOwnProperty.call(dataItem, 'id') || - (isItemInfo(dataItem) && - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.isInfo, - ) !== -1), - ); - }, [openedList]); + return {row[accessor]}; + }; - const toggle = useCallback((idx: number) => { - setOpenedList((state) => { - const open = state.findIndex((value) => value === idx) !== -1; - if (open) { - return state.filter((value) => value !== idx); - } - return [...state, idx]; - }); - }, []); + return Component; +}; - const renderLabelCell: TableRenderCell = useCallback(({ row }) => { - if (isItem(row)) { - return ( - id === row.id) !== -1} - toggle={toggle} - /> - ); - } - if (isItemInfo(row)) { - return ; - } - return null; - }, []); +const columns: TableColumn>[] = [ + { + title: '', + accessor: 'id', + width: 48, + maxWidth: 48, + minWidth: 48, + renderCell: DataCellName, + }, + { + title: 'Имя', + accessor: 'name', + width: 240, + renderCell: createDataCellOther('name'), + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + renderCell: createDataCellOther('profession'), + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + renderCell: createDataCellOther('status'), + }, +]; - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Название', - accessor: 'label', - renderCell: renderLabelCell, - colSpan: ({ row }) => (isItemInfo(row) ? 'end' : 1), - minWidth: 300, - }, - { - title: 'Формула', - accessor: 'formula', - minWidth: 200, - }, - { - title: 'Тип', - accessor: 'type', - minWidth: 180, - }, - ], - [], - ); +export const TableExampleActiveRowWithNumbering = () => { + const onRowClick = useAction(onRowClickAction); + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); + const [rows] = useAtom(rowsAtom); return (
{ - if (isItemInfo(row)) { - return `${row.isInfo}-info`; - } - if (isItem(row)) { - return `${row.id}`; - } - return ''; - }} + zebraStriped + onRowClick={onRowClick} + onRowMouseEnter={onRowMouseEnter} + onRowMouseLeave={onRowMouseLeave} /> ); }; @@ -2041,72 +2152,170 @@ export const TableExampleRenderRow = () => { -## Состояние загрузки +Есть 2 способа задать состояние `hover` строкам: -Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton. +- Автоматически, с помощью свойства `rowHoverEffect`. В этом случае все строки будут подсвечиваться при наведении мыши. -**Пример загрузки данных всей таблицы с помощью `Loader`:** + + + + +```tsx +import React from 'react'; + +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + +type ROW = { + athlete: string; + age: number | null; + country: string; + year: number; + date: string; + sport: string; + gold: number; + silver: number; + bronze: number; + total: number; +}; + +const columns: TableColumn[] = [ + { + title: 'Имя', + width: 'auto', + accessor: 'athlete', + }, + { + title: 'Страна', + accessor: 'country', + width: 'auto', + }, + { + title: 'Возраст', + accessor: 'age', + minWidth: 100, + }, +]; + +export const TableExampleRowHoverEffect = () => ( +
+); +``` + + + +- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. - + ```tsx -import { Loader } from '@consta/uikit/Loader'; +import { action, atom } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; import React from 'react'; -import { DataCell } from '@consta/table/DataCell'; +import { DataNumberingCell } from '@consta/table/DataNumberingCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type Item = { +// Types + +type ROW = { id: number; - label: string; + name: string; + profession: string; + status: string; }; -type Loader = { - isLoader: true; -}; +const rows: ROW[] = [ + { + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; -type Row = Item | Loader; +// Atoms -const data: Row[] = [{ isLoader: true }]; +const hoverIdAtom = atom(undefined); -const isLoader = (arg: Row): arg is Loader => - Object.prototype.hasOwnProperty.call(arg, 'isLoader'); +// Actions -const renderIdCell: TableRenderCell = ({ row }) => { - if (isLoader(row)) { - return ( - - - - ); +const onRowMouseEnterAction = action<[ROW]>((ctx, row) => + hoverIdAtom(ctx, row.id), +); + +const onRowMouseLeaveAction = action<[ROW]>((ctx, row) => { + const hoverId = ctx.get(hoverIdAtom); + if (hoverId === row.id) { + hoverIdAtom(ctx, undefined); } - return {row.id}; +}); + +const DataCellName: TableRenderCell = (props) => { + const [hover] = useAtom((ctx) => { + const hoverId = ctx.spy(hoverIdAtom); + return hoverId === props.row.id; + }); + + return ( + {props.row.id} + ); }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { - title: 'Номер', + title: '', accessor: 'id', - renderCell: renderIdCell, - colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), - minWidth: 300, + width: 48, + maxWidth: 48, + minWidth: 48, + renderCell: DataCellName, }, { - title: 'Наименование', - accessor: 'label', - minWidth: 300, + title: 'Имя', + accessor: 'name', + width: 240, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, }, ]; -export const TableExampleLoadingDataWithLoader = () => { +export const TableExampleHoveredControlled = () => { + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); + return (
); }; @@ -2114,86 +2323,108 @@ export const TableExampleLoadingDataWithLoader = () => { -**Пример загрузки данных всей таблицы с помощью `Skeleton`:** +Для выбора строки укажите в любой ячейке строки атрибут `data-row-active='true'`. - + ```tsx -import { Loader } from '@consta/uikit/Loader'; -import { cnMixSpace } from '@consta/uikit/MixSpace'; -import { SkeletonBrick } from '@consta/uikit/Skeleton'; +import { Checkbox } from '@consta/uikit/Checkbox'; +import { atom, AtomMut } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; import React from 'react'; import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type Item = { +type ROW = { id: number; - label: string; + name: string; + profession: string; + status: string; }; -type Loader = { - isLoader: true; +const activeIdsAtom = atom>>({}); + +const DataCellName: TableRenderCell = (props) => { + const [active] = useAtom((ctx) => { + const activeAtom = ctx.spy(activeIdsAtom)[props.row.id]; + return activeAtom ? ctx.spy(activeAtom) : false; + }); + + const onChange = useAction((ctx) => { + const activeIds = ctx.get(activeIdsAtom); + const activeAtom = ctx.get(activeIdsAtom)[props.row.id]; + + if (activeAtom) { + activeAtom(ctx, !ctx.get(activeAtom)); + } else { + activeIdsAtom(ctx, { ...activeIds, [props.row.id]: atom(true) }); + } + }); + + return ( + } + > + {props.row.name} + + ); }; -type Row = Item | Loader; - -const data: Row[] = [ - { isLoader: true }, - { isLoader: true }, - { isLoader: true }, +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + width: 240, + renderCell: DataCellName, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + }, ]; -const isLoader = (arg: Row): arg is Loader => - Object.prototype.hasOwnProperty.call(arg, 'isLoader'); - -const renderIdCell: TableRenderCell = ({ row }) => { - if (isLoader(row)) { - return ( -
- -
- ); - } - return {row.id}; -}; - -const columns: TableColumn[] = [ +const rows: ROW[] = [ { - title: 'Номер', - accessor: 'id', - renderCell: renderIdCell, - colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), - minWidth: 300, + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', }, { - title: 'Наименование', - accessor: 'label', - minWidth: 300, + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', }, ]; -export const TableExampleLoadingDataWithSkeleton = () => { - return ( -
- ); -}; +export const TableExampleActiveRow = () => ( +
+); ``` -**Пример подгрузки строки с помощью `Loader`:** +### Состояние загрузки + +Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton`. + +**Пример загрузки данных всей таблицы с помощью `Loader`:** - + ```tsx import { Loader } from '@consta/uikit/Loader'; @@ -2213,12 +2444,7 @@ type Loader = { type Row = Item | Loader; -const data: Row[] = [ - { id: 1, label: 'Item 1' }, - { id: 2, label: 'Item 2' }, - { id: 3, label: 'Item 3' }, - { isLoader: true }, -]; +const data: Row[] = [{ isLoader: true }]; const isLoader = (arg: Row): arg is Loader => Object.prototype.hasOwnProperty.call(arg, 'isLoader'); @@ -2249,14 +2475,13 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleLoadingRowWithLoader = () => { +export const TableExampleLoadingDataWithLoader = () => { return (
(isLoader(row) ? `loader` : `${row.id}`)} /> ); }; @@ -2264,14 +2489,13 @@ export const TableExampleLoadingRowWithLoader = () => { -**Пример подгрузки строки с помощью `Skeleton`:** +**Пример загрузки данных всей таблицы с помощью `Skeleton`:** - + ```tsx -import { Loader } from '@consta/uikit/Loader'; import { cnMixSpace } from '@consta/uikit/MixSpace'; import { SkeletonBrick } from '@consta/uikit/Skeleton'; import React from 'react'; @@ -2291,9 +2515,8 @@ type Loader = { type Row = Item | Loader; const data: Row[] = [ - { id: 1, label: 'Item 1' }, - { id: 2, label: 'Item 2' }, - { id: 3, label: 'Item 3' }, + { isLoader: true }, + { isLoader: true }, { isLoader: true }, ]; @@ -2326,91 +2549,13 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleLoadingRowWithSkeleton = () => { - return ( -
(isLoader(row) ? `loader` : `${row.id}`)} - /> - ); -}; -``` - - - -**Пример подгрузки вложенной строки с помощью `Loader`:** - - - - - -```tsx -import { IconArrowRight } from '@consta/icons/IconArrowRight'; - -import { Button } from '@consta/uikit/Button'; -import React from 'react'; - -import { DataCell } from '@consta/table/DataCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - -type Item = { - id: number; - label: string; - loading?: boolean; -}; - -type Row = Item; - -const data: Row[] = [ - { id: 1, label: 'Item 1' }, - { id: 2, label: 'Item 2' }, - { id: 3, label: 'Item 3' }, - { id: 4, label: 'Item 4', loading: true }, - { id: 5, label: 'Item 5' }, - { id: 6, label: 'Item 6' }, - { id: 7, label: 'Item 7' }, -]; - -const renderIdCell: TableRenderCell = ({ row }) => ( - - } - > - {row.id} - -); - -const columns: TableColumn[] = [ - { - title: 'Номер', - accessor: 'id', - renderCell: renderIdCell, - minWidth: 300, - }, - { - title: 'Наименование', - accessor: 'label', - minWidth: 300, - }, -]; - -export const TableExampleLoadingNestedRowWithLoader = () => { +export const TableExampleLoadingDataWithSkeleton = () => { return (
row.id} /> ); }; @@ -2418,405 +2563,251 @@ export const TableExampleLoadingNestedRowWithLoader = () => { -## Индикатор ячейки и подсказки - -Для отображения индикатора используйте свойство `indicator` у компонента `DataCell`. Для вывода подсказок используйте компонент `Popover`. В примере ниже реализована логика показа подсказок по наведению курсора. - -Пример со всплывающими подсказками: +**Пример подгрузки строки с помощью `Loader`:** - - -```tsx -import { Example } from '@consta/stand'; -import { Informer } from '@consta/uikit/Informer'; -import { - animateTimeout, - cnMixPopoverAnimate, -} from '@consta/uikit/MixPopoverAnimate'; -import { Popover } from '@consta/uikit/Popover'; -import { useDebounce } from '@consta/uikit/useDebounce'; -import { useFlag } from '@consta/uikit/useFlag'; -import React, { useCallback, useRef } from 'react'; -import { Transition } from 'react-transition-group'; - -import { DataCell } from '@consta/table/DataCell'; -import { Table, TableColumn } from '@consta/table/Table'; - -type Cell = { - data: T; - status?: 'alert' | 'warning'; - statusMessage?: string; -}; - -type Row = { - name: Cell; - profession: Cell; - status: Cell; -}; - -const rows: Row[] = [ - { - name: { - data: 'Антон', - }, - profession: { - data: 'РОЮЛАВТМЯО', - status: 'alert', - statusMessage: 'Неизвестное название диапазона: РОЮЛАВТМЯО.', - }, - status: { - data: 'недоступен', - }, - }, - ..//..//.. -]; - -const titleMap: Record<'alert' | 'warning', string> = { - alert: 'Ошибка', - warning: 'Предупреждение', -}; - -const DataCellWithInformer = ({ - data, - tableRef, - statusMessage, - status, -}: { - data: string; - status?: 'alert' | 'warning'; - statusMessage?: string; - tableRef: React.RefObject; -}) => { - const cellRef = useRef(null); - const popoverContentRef = useRef(null); - const [informerVisible, setInformerVisible] = useFlag(); + - const hoverStateRef = useRef<{ - popover: boolean; - anchor: boolean; - }>({ - popover: false, - anchor: false, - }); +```tsx +import { Loader } from '@consta/uikit/Loader'; +import React from 'react'; - const mouseEnterController = useDebounce( - useCallback(() => { - (hoverStateRef.current.anchor || hoverStateRef.current.popover) && - setInformerVisible.on(); - }, []), - 200, - ); +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - const mouseLeaveController = useDebounce( - useCallback(() => { - !hoverStateRef.current.anchor && - !hoverStateRef.current.popover && - setInformerVisible.off(); - }, []), - 200, - ); +type Item = { + id: number; + label: string; +}; - const anchorOnMouseEnter: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.anchor = true; - mouseEnterController(); - }, []); +type Loader = { + isLoader: true; +}; - const anchorOnMouseLeave: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.anchor = false; - mouseLeaveController(); - }, []); +type Row = Item | Loader; - const popoverOnMouseEnter: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.popover = true; - mouseEnterController(); - }, []); +const data: Row[] = [ + { id: 1, label: 'Item 1' }, + { id: 2, label: 'Item 2' }, + { id: 3, label: 'Item 3' }, + { isLoader: true }, +]; - const popoverOnMouseLeave: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.popover = false; - mouseLeaveController(); - }, []); +const isLoader = (arg: Row): arg is Loader => + Object.prototype.hasOwnProperty.call(arg, 'isLoader'); - return ( - <> - - {status === 'alert' ? '#ТИПДАННЫХ?' : data} +const renderIdCell: TableRenderCell = ({ row }) => { + if (isLoader(row)) { + return ( + + - {status && ( - - {(animate) => ( - - - - )} - - )} - - ); + ); + } + return {row.id}; }; const columns: TableColumn[] = [ { - title: 'Имя', - accessor: 'name', - renderCell: ({ row, tableRef }) => ( - - ), - }, - { - title: 'Профессия', - accessor: 'profession', - renderCell: ({ row, tableRef }) => ( - - ), + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), + minWidth: 300, }, { - title: 'Статус', - accessor: 'status', - renderCell: ({ row, tableRef }) => ( - - ), + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -export const TableExampleWithIndicator = () => { +export const TableExampleLoadingRowWithLoader = () => { return ( -
+
(isLoader(row) ? `loader` : `${row.id}`)} + /> ); }; ``` -## Ключ строки +**Пример подгрузки строки с помощью `Skeleton`:** -Чтобы оптимизировать рендеринг, укажите уникальный ключ строки в `getRowKey`. + - + -Если не указывать ключ строки, компонент попытается взять `row.id`. В случае, если и там ничего нет — `index`. При этом ключи ячеек будут брать ключ строки и добавлять к нему `accessor`. +```tsx +import { cnMixSpace } from '@consta/uikit/MixSpace'; +import { SkeletonBrick } from '@consta/uikit/Skeleton'; +import React from 'react'; - +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -```tsx -
row.uniqueKey} /> +type Item = { + id: number; + label: string; +}; + +type Loader = { + isLoader: true; +}; + +type Row = Item | Loader; + +const data: Row[] = [ + { id: 1, label: 'Item 1' }, + { id: 2, label: 'Item 2' }, + { id: 3, label: 'Item 3' }, + { isLoader: true }, +]; + +const isLoader = (arg: Row): arg is Loader => + Object.prototype.hasOwnProperty.call(arg, 'isLoader'); + +const renderIdCell: TableRenderCell = ({ row }) => { + if (isLoader(row)) { + return ( +
+ +
+ ); + } + return {row.id}; +}; + +const columns: TableColumn[] = [ + { + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), + minWidth: 300, + }, + { + title: 'Наименование', + accessor: 'label', + minWidth: 300, + }, +]; + +export const TableExampleLoadingRowWithSkeleton = () => { + return ( +
(isLoader(row) ? `loader` : `${row.id}`)} + /> + ); +}; ``` -## Адаптивная ширина колонок + -Вы можете менять ширину колонок в зависимости от ширины самой таблицы. +**Пример подгрузки вложенной строки с помощью `Loader`:** - + ```tsx -import { Example } from '@consta/stand'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; + import { Button } from '@consta/uikit/Button'; -import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React from 'react'; -import { Table, TableColumn } from '@consta/table/Table'; +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type Row = { name: string; profession: string; status: string }; +type Item = { + id: number; + label: string; + loading?: boolean; +}; -const rows: Row[] = [ +type Row = Item; + +const data: Row[] = [ + { id: 1, label: 'Item 1' }, + { id: 2, label: 'Item 2' }, + { id: 3, label: 'Item 3' }, + { id: 4, label: 'Item 4', loading: true }, + { id: 5, label: 'Item 5' }, + { id: 6, label: 'Item 6' }, + { id: 7, label: 'Item 7' }, +]; + +const renderIdCell: TableRenderCell = ({ row }) => ( + + } + > + {row.id} + +); + +const columns: TableColumn[] = [ { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + minWidth: 300, }, { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -const columnsWidthMap: Record< - 's' | 'm' | 'l', - Record<'name' | 'profession' | 'status', TableColumn> -> = { - s: { - name: { - width: 100, - minWidth: 100, - maxWidth: 100, - }, - profession: { - width: '1fr', - minWidth: 150, - }, - status: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - }, - m: { - name: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - profession: { - width: '1fr', - minWidth: 250, - }, - status: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - }, - l: { - name: { - width: 150, - minWidth: 150, - maxWidth: 150, - }, - profession: { - width: '1fr', - }, - status: { - width: 150, - minWidth: 150, - maxWidth: 150, - }, - }, +export const TableExampleLoadingNestedRowWithLoader = () => { + return ( +
row.id} + /> + ); }; +``` -const breakpointsMap = { s: 300, m: 500, l: 760 }; -const breakpointsSequence: (keyof typeof breakpointsMap)[] = ['s', 'm', 'l']; + -export const TableExampleAdaptiveColumns = () => { - const [widthSequence, setWidthSequence] = useState(0); +### Ключ строки - const handleWidthChange = useCallback(() => { - setWidthSequence((state) => { - const newState = state + 1; - return newState >= breakpointsSequence.length ? 0 : newState; - }); - }, []); +Чтобы оптимизировать рендеринг, укажите уникальный ключ строки в `getRowKey`. - const tableRef = useRef(null); + - const point = - getLastPoint( - useBreakpoints({ - ref: tableRef, - map: breakpointsMap, - isActive: true, - }), - ) || 's'; +Если не указывать ключ строки, компонент попытается взять `row.id`. В случае, если и там ничего нет — `index`. При этом ключи ячеек будут брать ключ строки и добавлять к нему `accessor`. - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Имя', - accessor: 'name', - ...columnsWidthMap[point].name, - }, - { - title: 'Профессия', - accessor: 'profession', - ...columnsWidthMap[point].profession, - }, - { - title: 'Статус', - accessor: 'status', - ...columnsWidthMap[point].status, - }, - ], - [point], - ); + - return ( - <> -
-
row.uniqueKey} /> ``` - - -## Фильтрация +### Фильтрация -Для фильтрации используйте в `renderHeaderCell` компонент [`FlatSelect`](##LIBS.LIB.STAND/lib:uikit/stand:components-flatselect-stable) +Для фильтрации используйте в `renderHeaderCell` компонент `FlatSelect`. @@ -2824,7 +2815,6 @@ export const TableExampleAdaptiveColumns = () => { ```tsx import { IconFunnel } from '@consta/icons/IconFunnel'; -import { Example } from '@consta/stand'; import { Button } from '@consta/uikit/Button'; import { FlatSelect } from '@consta/uikit/FlatSelect'; import { atom } from '@reatom/core'; @@ -2969,77 +2959,46 @@ const columns: TableColumn[] = [ export const TableExampleFilter = () => { const [rows] = useAtom(rowsAtom); - return ( - -
- - ); + return
; }; ``` -## Свойства +## Пример использования -```ts -export type TableRenderHeaderCell = (props: { - title?: string; - index: number; -}) => React.ReactNode | null; +```tsx +import { Table, TableColumn } from '@consta/table/Table'; -export type TableRenderCell = (props: { - row: T; - rowIndex: number; - columnIndex: number; -}) => React.ReactNode | null; +type Row = { name: string; profession: string; status: string }; -export type TableColumn = { - title?: string; - width?: - | number - | 'auto' - | '1fr' - | '2fr' - | '3fr' - | '4fr' - | '5fr' - | '6fr' - | '7fr' - | '8fr' - | '9fr' - | '10fr'; - maxWidth?: number; - minWidth?: number; - renderHeaderCell?: TableRenderHeaderCell; - isSeparator?: boolean; - pinned?: TableColumnPropPinned; - renderCell?: TableRenderCell; - colSpan?: TabletColSpan; - accessor?: string; - columns?: TableColumn[]; -}; +const rows: Row[] = [ + { + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; -type TableRowMouseEvent = ( - row: ROW, - props: { e: React.MouseEvent }, -) => void; +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + }, + { + title: 'Профессия', + accessor: 'profession', + }, + { + title: 'Статус', + accessor: 'status', + }, +]; -type GetRowKey = (row: ROW) => string | number; +
; ``` - -| Свойство | Тип | По умолчанию | Описание | -| ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------ | -| `columns?` | `TableColumn[]` | - | Колонки | -| `rows?` | `ROW[]` | - | Строки | -| `getRowKey?` | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | -| `onRowMouseEnter?` | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | -| `onRowMouseLeave?` | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | -| `onRowClick?` | `TableRowMouseEvent` | - | Событие `onClick` на строке | -| `virtualScroll?` | `boolean` | - | Включение виртуальной прокрутки | -| `stickyHeader?` | `boolean` | - | Зафиксировать шапку сверху | -| `resizable?` | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | -| `zebraStriped?` | `boolean` | - | Окрашивание строк через одну | -| `headerZIndex?` | `number` | `1` | `zIndex` шапки | -| `rowHoverEffect?` | `boolean` | - | Включает эффект наведения на строку | -| `className?` | `string` | - | Дополнительный CSS-класс | -| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | From 9f247cbed77ed813fbb8f1354425d207b05fc501 Mon Sep 17 00:00:00 2001 From: alyonurchick1 Date: Wed, 5 Nov 2025 14:37:11 +0300 Subject: [PATCH 3/5] Revert "docs: fix table" This reverts commit 5ed3fffe7c6e4d10a6b53cae509d68d0e2bda272. --- .../Table/__stand__/Table.dev.stand.mdx | 2841 +++++++++-------- 1 file changed, 1441 insertions(+), 1400 deletions(-) diff --git a/src/components/Table/__stand__/Table.dev.stand.mdx b/src/components/Table/__stand__/Table.dev.stand.mdx index a3afa68..28feb65 100644 --- a/src/components/Table/__stand__/Table.dev.stand.mdx +++ b/src/components/Table/__stand__/Table.dev.stand.mdx @@ -32,13 +32,9 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; -- [Обзор](#обзор) -- [Импорт](#импорт) - [Свойства](#свойства) -- [Содержимое](#содержимое) - - [Строки и колонки](#строки-и-колонки) - - [Колонка](#колонка) -- [Внешний вид](#внешний-вид) +- [Как формируется таблица](#как-формируется-таблица) +- [Колонка](#колонка) - [Ширина колонки](#ширина-колонки) - [Разделитель колонок](#разделитель-колонок) - [Закрепление колонок](#закрепление-колонок) @@ -46,53 +42,74 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; - [Представление данных в ячейке](#представление-данных-в-ячейке) - [Представление данных в ячейке в шапке](#представление-данных-в-ячейке-в-шапке) - [Объединение ячеек](#объединение-ячеек) - - [Закрепление шапки](#закрепление-шапки) - - [Таблица в полоску](#таблица-в-полоску) - - [Управление шириной столбцов](#управление-шириной-столбцов) - - [headerZIndex](#headerzindex) - - [Вложенные строки](#вложенные-строки) - - [Рендер строки](#рендер-строки) - - [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) - - [Адаптивная ширина колонок](#адаптивная-ширина-колонок) -- [Поведение](#поведение) - - [Виртуальный скролл](#виртуальный-скролл) - - [onScrollToBottom](#onscrolltobottom) - - [Выделение строк](#выделение-строк) - - [Состояние загрузки](#состояние-загрузки) - - [Ключ строки](#ключ-строки) - - [Фильтрация](#фильтрация) -- [Пример использования](#пример-использования) +- [Закрепление шапки](#закрепление-шапки) +- [Виртуальный скролл](#виртуальный-скролл) +- [onScrollToBottom](#onScrollToBottom) +- [Таблица в полоску](#таблица-в-полоску) +- [Выделение строк](#выделение-строк) + - [Наведение на строку](#наведение-на-строку) + - [Выбор строки](#выбор-строки) +- [Управление шириной столбцов](#управление-шириной-столбцов) +- [headerZIndex](#headerzindex) +- [Вложенные строки](#вложенные-строки) +- [Рендер строки](#рендер-строки) +- [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) +- [Ключ строки](#ключ-строки) +- [Адаптивная ширина колонок](#адаптивная-ширина-колонок) +- [Фильтрация](#фильтрация) -## Обзор +## Как формируется таблица -Компонент Table предназначен для отображения табличных данных. Он поддерживает настройку колонок, строк, виртуальную прокрутку, закрепление элементов и другие функции для работы с большими наборами данных. +Для вывода самой простой таблицы, используйте 2 свойства: + +- `rows` - строки. Тип строки может быть любым массивом объекта `Record`. +- `columns`- колонки. Представляют массив объектов, у которого `accessor` это свойство объекта ваших данных. -## Импорт + + + ```tsx import { Table, TableColumn } from '@consta/table/Table'; + +type Row = { name: string; profession: string; status: string }; + +const rows: Row[] = [ + { + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; + +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + }, + { + title: 'Профессия', + accessor: 'profession', + }, + { + title: 'Статус', + accessor: 'status', + }, +]; + +export const TableExampleSimple = () =>
; ``` -## Свойства + -| Свойство | Тип | По умолчанию | Описание | -| -------------------------------------------- | ----------------------------- | ----------------- | ------------------------------------------------------------ | -| [`columns?`](#колонка) | `TableColumn[]` | - | Колонки | -| [`rows?`](#строки-и-колонки) | `ROW[]` | - | Строки | -| [`getRowKey?`](#ключ-строки) | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | -| [`onRowMouseEnter?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | -| [`onRowMouseLeave?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | -| [`onRowClick?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onClick` на строке | -| [`virtualScroll?`](#виртуальный-скролл) | `boolean` | - | Включение виртуальной прокрутки | -| [`stickyHeader?`](#закрепление-шапки) | `boolean` | - | Зафиксировать шапку сверху | -| [`resizable?`](#управление-шириной-столбцов) | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | -| [`zebraStriped?`](#таблица-в-полоску) | `boolean` | - | Окрашивание строк через одну | -| [`headerZIndex?`](#headerzindex) | `number` | `1` | `zIndex` шапки | -| [`rowHoverEffect?`](#выделение-строк) | `boolean` | - | Включает эффект наведения на строку | -| `className?` | `string` | - | Дополнительный CSS-класс | -| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | +## Свойства ```ts export type TableRenderHeaderCell = (props: { @@ -140,58 +157,24 @@ type TableRowMouseEvent = ( type GetRowKey = (row: ROW) => string | number; ``` -## Содержимое - -### Строки и колонки - -Для вывода самой простой таблицы, используйте 2 свойства: - -- `rows` - строки. Тип строки может быть любым массивом объекта `Record`. -- `columns`- колонки. Представляют массив объектов, у которого `accessor` это свойство объекта ваших данных. - - - - - -```tsx -import { Table, TableColumn } from '@consta/table/Table'; - -type Row = { name: string; profession: string; status: string }; - -const rows: Row[] = [ - { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; - -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - }, - { - title: 'Профессия', - accessor: 'profession', - }, - { - title: 'Статус', - accessor: 'status', - }, -]; - -export const TableExampleSimple = () =>
; -``` - - - -### Колонка +| Свойство | Тип | По умолчанию | Описание | +| ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------ | +| `columns?` | `TableColumn[]` | - | Колонки | +| `rows?` | `ROW[]` | - | Строки | +| `getRowKey?` | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | +| `onRowMouseEnter?` | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | +| `onRowMouseLeave?` | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | +| `onRowClick?` | `TableRowMouseEvent` | - | Событие `onClick` на строке | +| `virtualScroll?` | `boolean` | - | Включение виртуальной прокрутки | +| `stickyHeader?` | `boolean` | - | Зафиксировать шапку сверху | +| `resizable?` | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | +| `zebraStriped?` | `boolean` | - | Окрашивание строк через одну | +| `headerZIndex?` | `number` | `1` | `zIndex` шапки | +| `rowHoverEffect?` | `boolean` | - | Включает эффект наведения на строку | +| `className?` | `string` | - | Дополнительный CSS-класс | +| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | + +## Колонка Колонки формируются с помощью CSS-свойств `display: grid` и `grid-template-columns`. @@ -237,9 +220,7 @@ export type TableColumn = { | `columns` | Вложенные колонки | | [`colSpan`](#объединение-ячеек) | Функция для вывода количества занятых колонок ячейкой | -## Внешний вид - -### Ширина колонки +## Ширина колонки Для настройки ширины колонок используйте `width`, `maxWidth`, `minWidth`, где `width` — это желаемая ширина колонки. Если все колонки будут помещаться в таблицу без скролла, она останется неизменной. Чтобы таблица не уменьшала или не увеличивала колонку вне желаемых размеров, ограничьте ее, используя `maxWidth` и `minWidth`. @@ -339,9 +320,7 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleSeparator = () => ( -
-); +export const TableExampleWidth = () =>
; ``` @@ -530,7 +509,7 @@ export const TableExampleGroupColumns = () => ( ### Представление данных в ячейке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `DataCell`. Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`DataCell`](##LIBS.LIB.STAND/lib:table/stand:components-datacell-stable). Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. @@ -621,7 +600,9 @@ const columns: TableColumn[] = [ ]; export const TableExampleRenderCell = () => ( -
+ +
+ ); ``` @@ -629,7 +610,7 @@ export const TableExampleRenderCell = () => ( ### Представление данных в ячейке в шапке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `HeaderDataCell`. +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`HeaderDataCell`](##LIBS.LIB.STAND/lib:table/stand:components-headerdatacell-stable). Вы можете в колонке использовать свойство `renderHeaderCell` и выводить данные, как вам потребуется. @@ -731,6 +712,7 @@ export const TableExampleRenderHeaderCell = () => ( import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; import { IconArrowRight } from '@consta/icons/IconArrowRight'; import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Example } from '@consta/stand'; import { Button } from '@consta/uikit/Button'; import { useMutableRef } from '@consta/uikit/useMutableRef'; import React, { useCallback, useMemo, useState } from 'react'; @@ -934,27 +916,34 @@ export const TableExampleColSpan = () => { -### Закрепление шапки +## Закрепление шапки Чтобы закрепить шапку, используйте свойство `stickyHeader` и ограничьте высоту таблицы в свойстве `maxHeight`. - - ```tsx
``` + + -### Таблица в полоску +## Виртуальный скролл -Для отображения таблицы в полоску используйте `zebraStriped`. +Если в таблице много данных, включите свойство `virtualScroll` и ограничьте высоту таблицы для лучшей производительности. Может принимать два значения: - +- `boolean` – будет включен виртуальный скролл по горизонтали и вертикали, +- `[boolean, boolean]` – первый элемент определяет, включается ли виртуальный скролл по горизонтали, второй – по вертикали. - + + +Чтобы избежать скачков строки при включении виртуального скролла, задайте одинаковую высоту ячеек в строке. + + + + ```tsx
{ columns={columns} stickyHeader virtualScroll - zebraStriped /> ``` + + -### Управление шириной столбцов +## onScrollToBottom -Чтобы дать пользователю возможность управлять шириной столбцов, используйте свойство `resizable`. Уменьшение и увеличение будет в пределах `minWidth` и `maxWidth` у колонки. +Свойство `onScrollToBottom` позволяет отслеживать, когда пользователь достиг конца таблицы. Полезно для дозагрузки данных в момент достижения конца таблицы. - + -Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. +```tsx +const TableExampleOnScrollToBottom = () => { + const [isScrollToBottom, setIsScrollToBottom] = useState(false); - + return ( + <> + + {isScrollToBottom ? 'Вы проскроллилили до конца' : 'Скролльте вниз'} + +
setIsScrollToBottom(true)} + /> + + ); +}; +``` -Закрепленные колонки (`pinned`) не могут изменять размер. + -Есть 2 режима работы: + -- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. +## Таблица в полоску - +Для отображения таблицы в полоску используйте `zebraStriped`. - + ```tsx -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; +
+``` -type ROW = { - athlete: string; - age: number | null; - country: string; -}; + -const columns: TableColumn[] = [ - { - title: 'Имя', - width: 'auto', - accessor: 'athlete', - // Запретили менять ширину у колонки - minWidth: 200, - maxWidth: 200, - }, - { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, - }, - { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, - }, -]; + -export const TableExampleResizableInside = () => ( -
-); -``` +## Выделение строк - +Строки могут иметь состояние: -- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. +- Наведение на строку – [`hover`](#наведение-на-строку), +- Выбор строки - [`active`](#выбор-строки) + + + +Чтобы не рендерить всю таблицу при каждом изменении данных в ячейке, используйте [стейт-менеджер](https://www.reatom.dev/blog/what-is-state-manager/) или [контекст](https://react.dev/reference/react/createContext). + + + +Пример с наведением и выбором строки: - + ```tsx -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; +import { Example } from '@consta/stand'; +import { action, atom, AtomMut } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; +import React from 'react'; + +import { DataCell } from '@consta/table/DataCell'; +import { DataNumberingCell } from '@consta/table/DataNumberingCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; + +// Types type ROW = { - athlete: string; - age: number | null; - country: string; + id: number; + name: string; + profession: string; + status: string; + hover: AtomMut; + active: AtomMut; }; -const columns: TableColumn[] = [ - { - title: 'Имя', - width: 'auto', - accessor: 'athlete', - minWidth: 200, - }, - { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, - pinned: 'left', - }, - { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, - }, - { - title: 'Медали', - columns: [ - { - title: 'Бронза', - accessor: 'bronze', - width: 'auto', - minWidth: 100, - }, - { - title: 'Серебро', - accessor: 'silver', - width: 'auto', - minWidth: 100, - }, - { - title: 'Золото', - accessor: 'gold', - width: 'auto', - minWidth: 100, - }, - { - title: 'Всего', - accessor: 'total', - width: 'auto', - minWidth: 100, - }, - ], +// Atoms +const rowsAtom = atom[]>([ + atom({ + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + hover: atom(false), + active: atom(false), + }), + atom({ + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + hover: atom(false), + active: atom(false), + }), +]); + +// Actions + +const onRowClickAction = action<[AtomMut]>((ctx, rowAtom) => { + const row = ctx.get(rowAtom); + row.active(ctx, !ctx.get(row.active)); +}); + +const onRowMouseEnterAction = action<[AtomMut]>((ctx, rowAtom) => { + ctx.get(rowAtom).hover(ctx, true); +}); + +const onRowMouseLeaveAction = action<[AtomMut]>((ctx, rowAtom) => { + ctx.get(rowAtom).hover(ctx, false); +}); + +const DataCellName: TableRenderCell> = (props) => { + const [row] = useAtom(props.row); + const [active] = useAtom(row.active); + const [hover] = useAtom(row.hover); + + return ( + + {row.id} + + ); +}; + +const createDataCellOther = ( + accessor: Exclude, +) => { + const Component: TableRenderCell> = (props) => { + const [row] = useAtom(props.row); + + return {row[accessor]}; + }; + + return Component; +}; + +const columns: TableColumn>[] = [ + { + title: '', + accessor: 'id', + width: 48, + maxWidth: 48, + minWidth: 48, + renderCell: DataCellName, }, { - title: 'Год', - accessor: 'year', + title: 'Имя', + accessor: 'name', + width: 240, + renderCell: createDataCellOther('name'), + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + renderCell: createDataCellOther('profession'), + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + renderCell: createDataCellOther('status'), + }, +]; + +export const TableExampleActiveRowWithNumbering = () => { + const onRowClick = useAction(onRowClickAction); + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); + const [rows] = useAtom(rowsAtom); + + return ( + +
+ + ); +}; +``` + + + +### Наведение на строку + +Есть 2 способа задать состояние `hover` строкам: + +- Автоматически, с помощью Свойства `rowHoverEffect`. В этом случае все строки будут подсвечиваться при наведении мыши. + +Пример автоматического способа: + + + + + +```tsx +import React from 'react'; + +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + +type ROW = { + athlete: string; + age: number | null; + country: string; + year: number; + date: string; + sport: string; + gold: number; + silver: number; + bronze: number; + total: number; +}; + +const columns: TableColumn[] = [ + { + title: 'Имя', + width: 'auto', + accessor: 'athlete', + }, + { + title: 'Страна', + accessor: 'country', width: 'auto', - minWidth: 140, - pinned: 'right', + }, + { + title: 'Возраст', + accessor: 'age', + minWidth: 100, }, ]; -export const TableExampleResizableOutside = () => ( +export const TableExampleRowHoverEffect = () => (
); ``` -### headerZIndex - -Свойство `headerZIndex` влияет на `z-index` следующих элементов: - -- Шапка таблицы. -- Ячейки в закрепленных колонках `z-index: calc(var(--table-header-z-index) - 1)`. -- Ячейки в закрепленных колонках в шапке `z-index: calc(var(--table-header-z-index) + 1)`. -- Ресайзеры колонок `z-index: calc(var(--table-header-z-index) + 10)`. - - - -Если вы используете всплывающие окна (`Popover`, `Tooltip`), привязывайте их `z-index` к переменной `--table-header-z-index`. -Например: `z-index: calc(var(--table-header-z-index) + 1)`. Таким образом плавающий элемент будет находится под шапкой таблицы. - - - -### Вложенные строки +- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. -Реализовать вложенные строки возможно через `renderCell` у колонки, взяв компонент `DataCell` и назначив ему свойство `level`. +Пример подконтрольного способа: - + ```tsx -import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; -import { IconArrowRight } from '@consta/icons/IconArrowRight'; -import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; -import { Button } from '@consta/uikit/Button'; -import { useMutableRef } from '@consta/uikit/useMutableRef'; -import React, { useCallback, useMemo, useState } from 'react'; +import { action, atom } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; +import React from 'react'; -import { DataCell } from '@consta/table/DataCell'; +import { DataNumberingCell } from '@consta/table/DataNumberingCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -import { range } from '##/utils/array'; + +// Types type ROW = { - idx: number; - col1: string; - col2: string; - col3: string; - parent: number | undefined; - level: number; + id: number; + name: string; + profession: string; + status: string; }; -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); +const rows: ROW[] = [ + { + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; -const getDataCell = (idx: number): ROW => { - const parent = Math.floor(idx / 10) * 10; +// Atoms - return { - idx, - col1: `Данные 1 - ${idx}`, - col2: `Данные 2 - ${idx}`, - col3: `Данные 3 - ${idx}`, - parent: parent === idx ? undefined : parent, - level: parent === idx ? 0 : 1, - }; -}; +const hoverIdAtom = atom(undefined); -const data = range(100000).map(getDataCell); +// Actions -const DataCellCol1 = (props: { - row: { - col1: string; - parent: number | undefined; - idx: number; - level: number; - }; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { - row: { col1, parent, idx, level }, - opened, - toggle, - } = props; +const onRowMouseEnterAction = action<[ROW]>((ctx, row) => + hoverIdAtom(ctx, row.id), +); + +const onRowMouseLeaveAction = action<[ROW]>((ctx, row) => { + const hoverId = ctx.get(hoverIdAtom); + if (hoverId === row.id) { + hoverIdAtom(ctx, undefined); + } +}); + +const DataCellName: TableRenderCell = (props) => { + const [hover] = useAtom((ctx) => { + const hoverId = ctx.spy(hoverIdAtom); + return hoverId === props.row.id; + }); return ( - - toggle(idx)} - /> - ) : undefined - } - > - {col1} - - + {props.row.id} ); }; -export const TableExampleNestedRows = () => { - const [openedList, setOpenedList] = useState([]); - - const openedListRef = useMutableRef(openedList); - - const rows = useMemo(() => { - return data.filter( - (dataItem) => - dataItem.parent === undefined || - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.parent, - ) !== -1, - ); - }, [openedList]); - - const toggle = useCallback((idx: number) => { - setOpenedList((state) => { - const open = state.findIndex((value) => value === idx) !== -1; - if (open) { - return state.filter((value) => value !== idx); - } - return [...state, idx]; - }); - }, []); - - const renderCellCol1: TableRenderCell = useCallback( - (props) => ( - item === props.row.idx) !== - -1 - } - /> - ), - [], - ); +const columns: TableColumn[] = [ + { + title: '', + accessor: 'id', + width: 48, + maxWidth: 48, + minWidth: 48, + renderCell: DataCellName, + }, + { + title: 'Имя', + accessor: 'name', + width: 240, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + }, +]; - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Колонка - 1', - accessor: 'col1', - renderCell: renderCellCol1, - }, - { - title: 'Колонка - 2', - accessor: 'col2', - }, - { - title: 'Колонка - 3', - accessor: 'col3', - }, - ], - [], - ); +export const TableExampleHoveredControlled = () => { + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); return (
row.idx} + zebraStriped + onRowMouseEnter={onRowMouseEnter} + onRowMouseLeave={onRowMouseLeave} + rowHoverEffect /> ); }; @@ -1302,1123 +1341,709 @@ export const TableExampleNestedRows = () => { -### Рендер строки +### Выбор строки -Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. +Для выбора строки укажите в любой ячейке строки атрибут `data-row-active='true'`. + +Пример с использованием стейт-менеджера: - + ```tsx -import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; -import { IconArrowRight } from '@consta/icons/IconArrowRight'; -import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; -import { Button } from '@consta/uikit/Button'; -import { Grid, GridItem } from '@consta/uikit/Grid'; -import { cnMixSpace } from '@consta/uikit/MixSpace'; -import { Text } from '@consta/uikit/Text'; -import { useMutableRef } from '@consta/uikit/useMutableRef'; -import React, { useCallback, useMemo, useState } from 'react'; +import { Example } from '@consta/stand'; +import { Checkbox } from '@consta/uikit/Checkbox'; +import { atom, AtomMut } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; +import React from 'react'; import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); - -type Option = { label: string; value: string }; -type Options = { label: string; value: Option[] }; - -type Item = { +type ROW = { id: number; - label: string; - formula: string; - type: string; + name: string; + profession: string; + status: string; }; -type ItemInfo = { - isInfo: number; - options: Options[]; -}; +const activeIdsAtom = atom>>({}); -type Row = { - id?: number; - label?: string; - formula?: string; - type?: string; - status?: 'work' | 'problem' | 'wait' | 'success'; - isInfo?: number; - options?: Options[]; +const DataCellName: TableRenderCell = (props) => { + const [active] = useAtom((ctx) => { + const activeAtom = ctx.spy(activeIdsAtom)[props.row.id]; + return activeAtom ? ctx.spy(activeAtom) : false; + }); + + const onChange = useAction((ctx) => { + const activeIds = ctx.get(activeIdsAtom); + const activeAtom = ctx.get(activeIdsAtom)[props.row.id]; + + if (activeAtom) { + activeAtom(ctx, !ctx.get(activeAtom)); + } else { + activeIdsAtom(ctx, { ...activeIds, [props.row.id]: atom(true) }); + } + }); + + return ( + } + > + {props.row.name} + + ); }; -const data: Row[] = [ +const columns: TableColumn[] = [ { - id: 1, - label: 'Запись инклинометрии', - formula: 'Время замера * Количество', - type: 'Кондуктор', + title: 'Имя', + accessor: 'name', + width: 240, + renderCell: DataCellName, }, { - isInfo: 1, - options: [ - { - label: 'Порты', - value: [ - { label: 'Входящий', value: 'A2-папа' }, - { label: 'Исходящий', value: 'A2-папа' }, - ], - }, - { - label: 'Размеры', - value: [ - { label: 'ширина(мм)', value: '60' }, - { label: 'длинна(мм)', value: '80' }, - ], - }, - ], + title: 'Профессия', + accessor: 'profession', + width: '1fr', }, { - id: 2, - label: 'Шаблонирование при бурении', - formula: 'Интервал/Скорость СПО', - type: 'Труба бурильная', + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, }, +]; + +const rows: ROW[] = [ { - isInfo: 2, - options: [ - { - label: 'Диаметры', - value: [ - { label: 'Внешний', value: '14.7' }, - { label: 'Внутренний', value: '12.7' }, - ], - }, - { - label: 'Нагрузка и моменты', - value: [ - { label: 'Растягивающая нагрузка тела трубы, кН', value: '30' }, - { label: 'Допустимый момент на кручение ЗС,кН*м', value: '10' }, - { - label: 'Допустимый момент на кручение тела трубы, кН*м', - value: '10', - }, - { - label: 'Рекомендуемый момент свинчивания, кН*м', - value: '10', - }, - ], - }, - ], + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', }, ]; -const isItemInfo = (arg: Row): arg is ItemInfo => - Object.prototype.hasOwnProperty.call(arg, 'isInfo'); +export const TableExampleActiveRow = () => ( + +
+ +); +``` -const isItem = (arg: Row): arg is Item => - Object.prototype.hasOwnProperty.call(arg, 'id'); + -const LabelCell = (props: { - id: number; - label: string; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { id, opened, toggle, label } = props; +## Управление шириной столбцов - return ( - - toggle(id)} - /> - } - > - {label} - - - ); -}; +Чтобы дать пользователю возможность управлять шириной столбцов, используйте свойство `resizable`. Уменьшение и увеличение будет в пределах `minWidth` и `maxWidth` у колонки. -const InfoCell = (props: { options: Options[] }) => { - const { options } = props; - return ( - - {options.map((opt) => { - return ( - <> - - {opt.label} - - {opt.value.map((val) => ( - - {val.value} - - {val.label} - - - ))} - - ); - })} - - ); -}; + -export const TableExampleRenderRow = () => { - const [openedList, setOpenedList] = useState([]); +Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. - const openedListRef = useMutableRef(openedList); + - const rows = useMemo(() => { - return data.filter( - (dataItem) => - Object.prototype.hasOwnProperty.call(dataItem, 'id') || - (isItemInfo(dataItem) && - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.isInfo, - ) !== -1), - ); - }, [openedList]); +Закрепленные колонки (`pinned`) не могут изменять размер. - const toggle = useCallback((idx: number) => { - setOpenedList((state) => { - const open = state.findIndex((value) => value === idx) !== -1; - if (open) { - return state.filter((value) => value !== idx); - } - return [...state, idx]; - }); - }, []); +Есть 2 режима работы: - const renderLabelCell: TableRenderCell = useCallback(({ row }) => { - if (isItem(row)) { - return ( - id === row.id) !== -1} - toggle={toggle} - /> - ); - } - if (isItemInfo(row)) { - return ; - } - return null; - }, []); +- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Название', - accessor: 'label', - renderCell: renderLabelCell, - colSpan: ({ row }) => (isItemInfo(row) ? 'end' : 1), - minWidth: 300, - }, - { - title: 'Формула', - accessor: 'formula', - minWidth: 200, - }, - { - title: 'Тип', - accessor: 'type', - minWidth: 180, - }, - ], - [], - ); + - return ( -
{ - if (isItemInfo(row)) { - return `${row.isInfo}-info`; - } - if (isItem(row)) { - return `${row.id}`; - } - return ''; - }} - /> - ); + + +```tsx +import { Example } from '@consta/stand'; +import React from 'react'; + +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + +type ROW = { + athlete: string; + age: number | null; + country: string; }; + +const columns: TableColumn[] = [ + { + title: 'Имя', + width: 'auto', + accessor: 'athlete', + // Запретили менять ширину у колонки + minWidth: 200, + maxWidth: 200, + }, + { + title: 'Страна', + accessor: 'country', + width: 'auto', + minWidth: 140, + }, + { + title: 'Возраст', + accessor: 'age', + width: 'auto', + minWidth: 100, + }, +]; + +export const TableExampleResizable = () => ( +
+); ``` -### Индикатор ячейки и подсказки - -Для отображения индикатора используйте свойство `indicator` у компонента `DataCell`. Для вывода подсказок используйте компонент `Popover`. В примере ниже реализована логика показа подсказок по наведению курсора. +- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. - + ```tsx -import { Informer } from '@consta/uikit/Informer'; -import { - animateTimeout, - cnMixPopoverAnimate, -} from '@consta/uikit/MixPopoverAnimate'; -import { Popover } from '@consta/uikit/Popover'; -import { useDebounce } from '@consta/uikit/useDebounce'; -import { useFlag } from '@consta/uikit/useFlag'; -import React, { useCallback, useRef } from 'react'; -import { Transition } from 'react-transition-group'; +import { Example } from '@consta/stand'; +import React from 'react'; -import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; -type Cell = { - data: T; - status?: 'alert' | 'warning'; - statusMessage?: string; -}; - -type Row = { - name: Cell; - profession: Cell; - status: Cell; +type ROW = { + athlete: string; + age: number | null; + country: string; }; -const rows: Row[] = [ - { - name: { - data: 'Антон', - }, - profession: { - data: 'РОЮЛАВТМЯО', - status: 'alert', - statusMessage: 'Неизвестное название диапазона: РОЮЛАВТМЯО.', - }, - status: { - data: 'недоступен', - }, - }, - // ... -]; - -const titleMap: Record<'alert' | 'warning', string> = { - alert: 'Ошибка', - warning: 'Предупреждение', -}; - -const DataCellWithInformer = ({ - data, - tableRef, - statusMessage, - status, -}: { - data: string; - status?: 'alert' | 'warning'; - statusMessage?: string; - tableRef: React.RefObject; -}) => { - const cellRef = useRef(null); - const popoverContentRef = useRef(null); - const [informerVisible, setInformerVisible] = useFlag(); - - const hoverStateRef = useRef<{ - popover: boolean; - anchor: boolean; - }>({ - popover: false, - anchor: false, - }); - - const mouseEnterController = useDebounce( - useCallback(() => { - (hoverStateRef.current.anchor || hoverStateRef.current.popover) && - setInformerVisible.on(); - }, []), - 200, - ); - - const mouseLeaveController = useDebounce( - useCallback(() => { - !hoverStateRef.current.anchor && - !hoverStateRef.current.popover && - setInformerVisible.off(); - }, []), - 200, - ); - - const anchorOnMouseEnter: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.anchor = true; - mouseEnterController(); - }, []); - - const anchorOnMouseLeave: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.anchor = false; - mouseLeaveController(); - }, []); - - const popoverOnMouseEnter: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.popover = true; - mouseEnterController(); - }, []); - - const popoverOnMouseLeave: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.popover = false; - mouseLeaveController(); - }, []); - - return ( - <> - - {status === 'alert' ? '#ТИПДАННЫХ?' : data} - - {status && ( - - {(animate) => ( - - - - )} - - )} - - ); -}; - -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - renderCell: ({ row, tableRef }) => ( - - ), - }, - { - title: 'Профессия', - accessor: 'profession', - renderCell: ({ row, tableRef }) => ( - - ), - }, - { - title: 'Статус', - accessor: 'status', - renderCell: ({ row, tableRef }) => ( - - ), - }, -]; - -export const TableExampleWithIndicator = () => { - return ( -
- ); -}; -``` - - - -### Адаптивная ширина колонок - -Вы можете менять ширину колонок в зависимости от ширины самой таблицы. - - - - - -```tsx -import { Button } from '@consta/uikit/Button'; -import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; - -import { Table, TableColumn } from '@consta/table/Table'; - -type Row = { name: string; profession: string; status: string }; - -const rows: Row[] = [ - { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; - -const columnsWidthMap: Record< - 's' | 'm' | 'l', - Record<'name' | 'profession' | 'status', TableColumn> -> = { - s: { - name: { - width: 100, - minWidth: 100, - maxWidth: 100, - }, - profession: { - width: '1fr', - minWidth: 150, - }, - status: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - }, - m: { - name: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - profession: { - width: '1fr', - minWidth: 250, - }, - status: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - }, - l: { - name: { - width: 150, - minWidth: 150, - maxWidth: 150, - }, - profession: { - width: '1fr', - }, - status: { - width: 150, - minWidth: 150, - maxWidth: 150, - }, - }, -}; - -const breakpointsMap = { s: 300, m: 500, l: 760 }; -const breakpointsSequence: (keyof typeof breakpointsMap)[] = ['s', 'm', 'l']; - -export const TableExampleAdaptiveColumns = () => { - const [widthSequence, setWidthSequence] = useState(0); - - const handleWidthChange = useCallback(() => { - setWidthSequence((state) => { - const newState = state + 1; - return newState >= breakpointsSequence.length ? 0 : newState; - }); - }, []); - - const tableRef = useRef(null); - - const point = - getLastPoint( - useBreakpoints({ - ref: tableRef, - map: breakpointsMap, - isActive: true, - }), - ) || 's'; - - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Имя', - accessor: 'name', - ...columnsWidthMap[point].name, - }, - { - title: 'Профессия', - accessor: 'profession', - ...columnsWidthMap[point].profession, - }, - { - title: 'Статус', - accessor: 'status', - ...columnsWidthMap[point].status, - }, - ], - [point], - ); - - return ( - <> -
-
-``` - - - -### onScrollToBottom - -Свойство `onScrollToBottom` позволяет отслеживать, когда пользователь достиг конца таблицы. Полезно для дозагрузки данных в момент достижения конца таблицы. - - - - - -```tsx -const TableExampleOnScrollToBottom = () => { - const [isScrollToBottom, setIsScrollToBottom] = useState(false); - - return ( - <> - - {isScrollToBottom ? 'Вы проскроллили до конца' : 'Скролльте вниз'} - -
setIsScrollToBottom(true)} - /> - - ); -}; -``` - - - -### Выделение строк - -Строки могут иметь состояние: - -- Наведение на строку – `hover`, -- Выбор строки - `active` - - - -Чтобы не рендерить всю таблицу при каждом изменении данных в ячейке, используйте стейт-менеджер или контекст. - - - -Пример с наведением и выбором строки: - - - - - -```tsx -import { action, atom, AtomMut } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; - -import { DataCell } from '@consta/table/DataCell'; -import { DataNumberingCell } from '@consta/table/DataNumberingCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - -// Types - -type ROW = { - id: number; - name: string; - profession: string; - status: string; - hover: AtomMut; - active: AtomMut; -}; - -// Atoms -const rowsAtom = atom[]>([ - atom({ - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - hover: atom(false), - active: atom(false), - }), - atom({ - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - hover: atom(false), - active: atom(false), - }), -]); - -// Actions - -const onRowClickAction = action<[AtomMut]>((ctx, rowAtom) => { - const row = ctx.get(rowAtom); - row.active(ctx, !ctx.get(row.active)); -}); - -const onRowMouseEnterAction = action<[AtomMut]>((ctx, rowAtom) => { - ctx.get(rowAtom).hover(ctx, true); -}); - -const onRowMouseLeaveAction = action<[AtomMut]>((ctx, rowAtom) => { - ctx.get(rowAtom).hover(ctx, false); -}); - -const DataCellName: TableRenderCell> = (props) => { - const [row] = useAtom(props.row); - const [active] = useAtom(row.active); - const [hover] = useAtom(row.hover); - - return ( - - {row.id} - - ); -}; - -const createDataCellOther = ( - accessor: Exclude, -) => { - const Component: TableRenderCell> = (props) => { - const [row] = useAtom(props.row); - - return {row[accessor]}; - }; - - return Component; -}; - -const columns: TableColumn>[] = [ - { - title: '', - accessor: 'id', - width: 48, - maxWidth: 48, - minWidth: 48, - renderCell: DataCellName, - }, +const columns: TableColumn[] = [ { title: 'Имя', - accessor: 'name', - width: 240, - renderCell: createDataCellOther('name'), + width: 'auto', + accessor: 'athlete', + minWidth: 200, }, { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - renderCell: createDataCellOther('profession'), + title: 'Страна', + accessor: 'country', + width: 'auto', + minWidth: 140, + pinned: 'left', }, { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, - renderCell: createDataCellOther('status'), + title: 'Возраст', + accessor: 'age', + width: 'auto', + minWidth: 100, }, -]; - -export const TableExampleActiveRowWithNumbering = () => { - const onRowClick = useAction(onRowClickAction); - const onRowMouseEnter = useAction(onRowMouseEnterAction); - const onRowMouseLeave = useAction(onRowMouseLeaveAction); - const [rows] = useAtom(rowsAtom); - - return ( -
- ); -}; -``` - - - -Есть 2 способа задать состояние `hover` строкам: - -- Автоматически, с помощью свойства `rowHoverEffect`. В этом случае все строки будут подсвечиваться при наведении мыши. - - - - - -```tsx -import React from 'react'; - -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; - -type ROW = { - athlete: string; - age: number | null; - country: string; - year: number; - date: string; - sport: string; - gold: number; - silver: number; - bronze: number; - total: number; -}; - -const columns: TableColumn[] = [ { - title: 'Имя', - width: 'auto', - accessor: 'athlete', + title: 'Медали', + columns: [ + { + title: 'Бронза', + accessor: 'bronze', + width: 'auto', + minWidth: 100, + }, + { + title: 'Серебро', + accessor: 'silver', + width: 'auto', + minWidth: 100, + }, + { + title: 'Золото', + accessor: 'gold', + width: 'auto', + minWidth: 100, + }, + { + title: 'Всего', + accessor: 'total', + width: 'auto', + minWidth: 100, + }, + ], }, { - title: 'Страна', - accessor: 'country', + title: 'Год', + accessor: 'year', width: 'auto', - }, - { - title: 'Возраст', - accessor: 'age', - minWidth: 100, + minWidth: 140, + pinned: 'right', }, ]; -export const TableExampleRowHoverEffect = () => ( +export const TableExampleResizableOutside = () => (
); ``` -- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. +## headerZIndex + +Свойство `headerZIndex` влияет на [`z-index`](https://developer.mozilla.org/ru/docs/Web/CSS/z-index) следующих элементов: + +- Шапка таблицы. +- Ячейки в закрепленных колонках `z-index: calc(var(--table-header-z-index) - 1)`. +- Ячейки в закрепленных колонках в шапке `z-index: calc(var(--table-header-z-index) + 1)`. +- Ресайзеры колонок `z-index: calc(var(--table-header-z-index) + 10)`. + + + +Если вы используете всплывающие окна (`Popover`, `Tooltip`), привязывайте их `z-index` к переменной `--table-header-z-index`. +Например: `z-index: calc(var(--table-header-z-index) + 1)`. Таким образом плавающий элемент будет находится под шапкой таблицы. + + + +## Вложенные строки + +Реализовать вложенные строки возможно через `renderCell` у колонки, взяв компонент [`DataCell`](##LIBS.LIB.STAND/lib:table/stand:components-datacell-stable) и назначив ему свойство `level`. - + ```tsx -import { action, atom } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; +import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; +import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Button } from '@consta/uikit/Button'; +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import React, { useCallback, useMemo, useState } from 'react'; -import { DataNumberingCell } from '@consta/table/DataNumberingCell'; +import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - -// Types +import { range } from '##/utils/array'; type ROW = { - id: number; - name: string; - profession: string; - status: string; + idx: number; + col1: string; + col2: string; + col3: string; + parent: number | undefined; + level: number; }; -const rows: ROW[] = [ - { - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; +const IconArrow = withAnimateSwitcherHOC({ + startIcon: IconArrowRight, + startDirection: 0, + endDirection: 90, +}); -// Atoms +const getDataCell = (idx: number): ROW => { + const parent = Math.floor(idx / 10) * 10; -const hoverIdAtom = atom(undefined); + return { + idx, + col1: `Данные 1 - ${idx}`, + col2: `Данные 2 - ${idx}`, + col3: `Данные 3 - ${idx}`, + parent: parent === idx ? undefined : parent, + level: parent === idx ? 0 : 1, + }; +}; -// Actions +const data = range(100000).map(getDataCell); -const onRowMouseEnterAction = action<[ROW]>((ctx, row) => - hoverIdAtom(ctx, row.id), -); +const DataCellCol1 = (props: { + row: { + col1: string; + parent: number | undefined; + idx: number; + level: number; + }; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { + row: { col1, parent, idx, level }, + opened, + toggle, + } = props; -const onRowMouseLeaveAction = action<[ROW]>((ctx, row) => { - const hoverId = ctx.get(hoverIdAtom); - if (hoverId === row.id) { - hoverIdAtom(ctx, undefined); - } -}); + return ( + + toggle(idx)} + /> + ) : undefined + } + > + {col1} + + + ); +}; -const DataCellName: TableRenderCell = (props) => { - const [hover] = useAtom((ctx) => { - const hoverId = ctx.spy(hoverIdAtom); - return hoverId === props.row.id; - }); +export const TableExampleNestedRows = () => { + const [openedList, setOpenedList] = useState([]); + + const openedListRef = useMutableRef(openedList); + + const rows = useMemo(() => { + return data.filter( + (dataItem) => + dataItem.parent === undefined || + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.parent, + ) !== -1, + ); + }, [openedList]); + + const toggle = useCallback((idx: number) => { + setOpenedList((state) => { + const open = state.findIndex((value) => value === idx) !== -1; + if (open) { + return state.filter((value) => value !== idx); + } + return [...state, idx]; + }); + }, []); + + const renderCellCol1: TableRenderCell = useCallback( + (props) => ( + item === props.row.idx) !== + -1 + } + /> + ), + [], + ); + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Колонка - 1', + accessor: 'col1', + renderCell: renderCellCol1, + }, + { + title: 'Колонка - 2', + accessor: 'col2', + }, + { + title: 'Колонка - 3', + accessor: 'col3', + }, + ], + [], + ); return ( - {props.row.id} +
row.idx} + /> ); }; +``` + + + +## Рендер строки + +Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. + + + + + +```tsx +import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; +import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Example } from '@consta/stand'; +import { Button } from '@consta/uikit/Button'; +import { Grid, GridItem } from '@consta/uikit/Grid'; +import { cnMixSpace } from '@consta/uikit/MixSpace'; +import { Text } from '@consta/uikit/Text'; +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; + +const IconArrow = withAnimateSwitcherHOC({ + startIcon: IconArrowRight, + startDirection: 0, + endDirection: 90, +}); + +type Option = { label: string; value: string }; +type Options = { label: string; value: Option[] }; + +type Item = { + id: number; + label: string; + formula: string; + type: string; +}; + +type ItemInfo = { + isInfo: number; + options: Options[]; +}; + +type Row = { + id?: number; + label?: string; + formula?: string; + type?: string; + status?: 'work' | 'problem' | 'wait' | 'success'; + isInfo?: number; + options?: Options[]; +}; -const columns: TableColumn[] = [ +const data: Row[] = [ { - title: '', - accessor: 'id', - width: 48, - maxWidth: 48, - minWidth: 48, - renderCell: DataCellName, + id: 1, + label: 'Запись инклинометрии', + formula: 'Время замера * Количество', + type: 'Кондуктор', }, { - title: 'Имя', - accessor: 'name', - width: 240, + isInfo: 1, + options: [ + { + label: 'Порты', + value: [ + { label: 'Входящий', value: 'A2-папа' }, + { label: 'Исходящий', value: 'A2-папа' }, + ], + }, + { + label: 'Размеры', + value: [ + { label: 'ширина(мм)', value: '60' }, + { label: 'длинна(мм)', value: '80' }, + ], + }, + ], }, { - title: 'Профессия', - accessor: 'profession', - width: '1fr', + id: 2, + label: 'Шаблонирование при бурении', + formula: 'Интервал/Скорость СПО', + type: 'Труба бурильная', }, { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, + isInfo: 2, + options: [ + { + label: 'Диаметры', + value: [ + { label: 'Внешний', value: '14.7' }, + { label: 'Внутренний', value: '12.7' }, + ], + }, + { + label: 'Нагрузка и моменты', + value: [ + { label: 'Растягивающая нагрузка тела трубы, кН', value: '30' }, + { label: 'Допустимый момент на кручение ЗС,кН*м', value: '10' }, + { + label: 'Допустимый момент на кручение тела трубы, кН*м', + value: '10', + }, + { + label: 'Рекомендуемый момент свинчивания, кН*м', + value: '10', + }, + ], + }, + ], }, ]; -export const TableExampleHoveredControlled = () => { - const onRowMouseEnter = useAction(onRowMouseEnterAction); - const onRowMouseLeave = useAction(onRowMouseLeaveAction); +const isItemInfo = (arg: Row): arg is ItemInfo => + Object.prototype.hasOwnProperty.call(arg, 'isInfo'); + +const isItem = (arg: Row): arg is Item => + Object.prototype.hasOwnProperty.call(arg, 'id'); + +const LabelCell = (props: { + id: number; + label: string; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { id, opened, toggle, label } = props; return ( -
+ + toggle(id)} + /> + } + > + {label} + + ); }; -``` - - - -Для выбора строки укажите в любой ячейке строки атрибут `data-row-active='true'`. - - - - - -```tsx -import { Checkbox } from '@consta/uikit/Checkbox'; -import { atom, AtomMut } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; - -import { DataCell } from '@consta/table/DataCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type ROW = { - id: number; - name: string; - profession: string; - status: string; +const InfoCell = (props: { options: Options[] }) => { + const { options } = props; + return ( + + {options.map((opt) => { + return ( + <> + + {opt.label} + + {opt.value.map((val) => ( + + {val.value} + + {val.label} + + + ))} + + ); + })} + + ); }; -const activeIdsAtom = atom>>({}); +export const TableExampleRenderRow = () => { + const [openedList, setOpenedList] = useState([]); -const DataCellName: TableRenderCell = (props) => { - const [active] = useAtom((ctx) => { - const activeAtom = ctx.spy(activeIdsAtom)[props.row.id]; - return activeAtom ? ctx.spy(activeAtom) : false; - }); + const openedListRef = useMutableRef(openedList); - const onChange = useAction((ctx) => { - const activeIds = ctx.get(activeIdsAtom); - const activeAtom = ctx.get(activeIdsAtom)[props.row.id]; + const rows = useMemo(() => { + return data.filter( + (dataItem) => + Object.prototype.hasOwnProperty.call(dataItem, 'id') || + (isItemInfo(dataItem) && + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.isInfo, + ) !== -1), + ); + }, [openedList]); - if (activeAtom) { - activeAtom(ctx, !ctx.get(activeAtom)); - } else { - activeIdsAtom(ctx, { ...activeIds, [props.row.id]: atom(true) }); + const toggle = useCallback((idx: number) => { + setOpenedList((state) => { + const open = state.findIndex((value) => value === idx) !== -1; + if (open) { + return state.filter((value) => value !== idx); + } + return [...state, idx]; + }); + }, []); + + const renderLabelCell: TableRenderCell = useCallback(({ row }) => { + if (isItem(row)) { + return ( + id === row.id) !== -1} + toggle={toggle} + /> + ); } - }); + if (isItemInfo(row)) { + return ; + } + return null; + }, []); + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Название', + accessor: 'label', + renderCell: renderLabelCell, + colSpan: ({ row }) => (isItemInfo(row) ? 'end' : 1), + minWidth: 300, + }, + { + title: 'Формула', + accessor: 'formula', + minWidth: 200, + }, + { + title: 'Тип', + accessor: 'type', + minWidth: 180, + }, + ], + [], + ); return ( - } - > - {props.row.name} - +
{ + if (isItemInfo(row)) { + return `${row.isInfo}-info`; + } + if (isItem(row)) { + return `${row.id}`; + } + return ''; + }} + /> ); }; - -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - width: 240, - renderCell: DataCellName, - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - }, - { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, - }, -]; - -const rows: ROW[] = [ - { - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; - -export const TableExampleActiveRow = () => ( -
-); ``` -### Состояние загрузки +## Состояние загрузки -Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton`. +Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton. **Пример загрузки данных всей таблицы с помощью `Loader`:** @@ -2496,6 +2121,7 @@ export const TableExampleLoadingDataWithLoader = () => { ```tsx +import { Loader } from '@consta/uikit/Loader'; import { cnMixSpace } from '@consta/uikit/MixSpace'; import { SkeletonBrick } from '@consta/uikit/Skeleton'; import React from 'react'; @@ -2645,6 +2271,7 @@ export const TableExampleLoadingRowWithLoader = () => { ```tsx +import { Loader } from '@consta/uikit/Loader'; import { cnMixSpace } from '@consta/uikit/MixSpace'; import { SkeletonBrick } from '@consta/uikit/Skeleton'; import React from 'react'; @@ -2770,28 +2397,262 @@ const columns: TableColumn[] = [ minWidth: 300, }, { - title: 'Наименование', - accessor: 'label', - minWidth: 300, + title: 'Наименование', + accessor: 'label', + minWidth: 300, + }, +]; + +export const TableExampleLoadingNestedRowWithLoader = () => { + return ( +
row.id} + /> + ); +}; +``` + + + +## Индикатор ячейки и подсказки + +Для отображения индикатора используйте свойство `indicator` у компонента `DataCell`. Для вывода подсказок используйте компонент `Popover`. В примере ниже реализована логика показа подсказок по наведению курсора. + +Пример со всплывающими подсказками: + + + + + +```tsx +import { Example } from '@consta/stand'; +import { Informer } from '@consta/uikit/Informer'; +import { + animateTimeout, + cnMixPopoverAnimate, +} from '@consta/uikit/MixPopoverAnimate'; +import { Popover } from '@consta/uikit/Popover'; +import { useDebounce } from '@consta/uikit/useDebounce'; +import { useFlag } from '@consta/uikit/useFlag'; +import React, { useCallback, useRef } from 'react'; +import { Transition } from 'react-transition-group'; + +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn } from '@consta/table/Table'; + +type Cell = { + data: T; + status?: 'alert' | 'warning'; + statusMessage?: string; +}; + +type Row = { + name: Cell; + profession: Cell; + status: Cell; +}; + +const rows: Row[] = [ + { + name: { + data: 'Антон', + }, + profession: { + data: 'РОЮЛАВТМЯО', + status: 'alert', + statusMessage: 'Неизвестное название диапазона: РОЮЛАВТМЯО.', + }, + status: { + data: 'недоступен', + }, + }, + ..//..//.. +]; + +const titleMap: Record<'alert' | 'warning', string> = { + alert: 'Ошибка', + warning: 'Предупреждение', +}; + +const DataCellWithInformer = ({ + data, + tableRef, + statusMessage, + status, +}: { + data: string; + status?: 'alert' | 'warning'; + statusMessage?: string; + tableRef: React.RefObject; +}) => { + const cellRef = useRef(null); + const popoverContentRef = useRef(null); + const [informerVisible, setInformerVisible] = useFlag(); + + const hoverStateRef = useRef<{ + popover: boolean; + anchor: boolean; + }>({ + popover: false, + anchor: false, + }); + + const mouseEnterController = useDebounce( + useCallback(() => { + (hoverStateRef.current.anchor || hoverStateRef.current.popover) && + setInformerVisible.on(); + }, []), + 200, + ); + + const mouseLeaveController = useDebounce( + useCallback(() => { + !hoverStateRef.current.anchor && + !hoverStateRef.current.popover && + setInformerVisible.off(); + }, []), + 200, + ); + + const anchorOnMouseEnter: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.anchor = true; + mouseEnterController(); + }, []); + + const anchorOnMouseLeave: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.anchor = false; + mouseLeaveController(); + }, []); + + const popoverOnMouseEnter: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.popover = true; + mouseEnterController(); + }, []); + + const popoverOnMouseLeave: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.popover = false; + mouseLeaveController(); + }, []); + + return ( + <> + + {status === 'alert' ? '#ТИПДАННЫХ?' : data} + + {status && ( + + {(animate) => ( + + + + )} + + )} + + ); +}; + +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + renderCell: ({ row, tableRef }) => ( + + ), + }, + { + title: 'Профессия', + accessor: 'profession', + renderCell: ({ row, tableRef }) => ( + + ), + }, + { + title: 'Статус', + accessor: 'status', + renderCell: ({ row, tableRef }) => ( + + ), }, ]; -export const TableExampleLoadingNestedRowWithLoader = () => { +export const TableExampleWithIndicator = () => { return ( -
row.id} - /> +
); }; ``` -### Ключ строки +## Ключ строки Чтобы оптимизировать рендеринг, укажите уникальный ключ строки в `getRowKey`. @@ -2805,9 +2666,157 @@ export const TableExampleLoadingNestedRowWithLoader = () => {
row.uniqueKey} /> ``` -### Фильтрация +## Адаптивная ширина колонок + +Вы можете менять ширину колонок в зависимости от ширины самой таблицы. + + + + + +```tsx +import { Example } from '@consta/stand'; +import { Button } from '@consta/uikit/Button'; +import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; + +import { Table, TableColumn } from '@consta/table/Table'; + +type Row = { name: string; profession: string; status: string }; + +const rows: Row[] = [ + { + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; + +const columnsWidthMap: Record< + 's' | 'm' | 'l', + Record<'name' | 'profession' | 'status', TableColumn> +> = { + s: { + name: { + width: 100, + minWidth: 100, + maxWidth: 100, + }, + profession: { + width: '1fr', + minWidth: 150, + }, + status: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + }, + m: { + name: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + profession: { + width: '1fr', + minWidth: 250, + }, + status: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + }, + l: { + name: { + width: 150, + minWidth: 150, + maxWidth: 150, + }, + profession: { + width: '1fr', + }, + status: { + width: 150, + minWidth: 150, + maxWidth: 150, + }, + }, +}; + +const breakpointsMap = { s: 300, m: 500, l: 760 }; +const breakpointsSequence: (keyof typeof breakpointsMap)[] = ['s', 'm', 'l']; + +export const TableExampleAdaptiveColumns = () => { + const [widthSequence, setWidthSequence] = useState(0); + + const handleWidthChange = useCallback(() => { + setWidthSequence((state) => { + const newState = state + 1; + return newState >= breakpointsSequence.length ? 0 : newState; + }); + }, []); + + const tableRef = useRef(null); + + const point = + getLastPoint( + useBreakpoints({ + ref: tableRef, + map: breakpointsMap, + isActive: true, + }), + ) || 's'; + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Имя', + accessor: 'name', + ...columnsWidthMap[point].name, + }, + { + title: 'Профессия', + accessor: 'profession', + ...columnsWidthMap[point].profession, + }, + { + title: 'Статус', + accessor: 'status', + ...columnsWidthMap[point].status, + }, + ], + [point], + ); + + return ( + <> +
+
; + return ( + +
+ + ); }; ``` -## Пример использования +## Свойства -```tsx -import { Table, TableColumn } from '@consta/table/Table'; +```ts +export type TableRenderHeaderCell = (props: { + title?: string; + index: number; +}) => React.ReactNode | null; -type Row = { name: string; profession: string; status: string }; +export type TableRenderCell = (props: { + row: T; + rowIndex: number; + columnIndex: number; +}) => React.ReactNode | null; -const rows: Row[] = [ - { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; +export type TableColumn = { + title?: string; + width?: + | number + | 'auto' + | '1fr' + | '2fr' + | '3fr' + | '4fr' + | '5fr' + | '6fr' + | '7fr' + | '8fr' + | '9fr' + | '10fr'; + maxWidth?: number; + minWidth?: number; + renderHeaderCell?: TableRenderHeaderCell; + isSeparator?: boolean; + pinned?: TableColumnPropPinned; + renderCell?: TableRenderCell; + colSpan?: TabletColSpan; + accessor?: string; + columns?: TableColumn[]; +}; -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - }, - { - title: 'Профессия', - accessor: 'profession', - }, - { - title: 'Статус', - accessor: 'status', - }, -]; +type TableRowMouseEvent = ( + row: ROW, + props: { e: React.MouseEvent }, +) => void; -
; +type GetRowKey = (row: ROW) => string | number; ``` + +| Свойство | Тип | По умолчанию | Описание | +| ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------ | +| `columns?` | `TableColumn[]` | - | Колонки | +| `rows?` | `ROW[]` | - | Строки | +| `getRowKey?` | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | +| `onRowMouseEnter?` | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | +| `onRowMouseLeave?` | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | +| `onRowClick?` | `TableRowMouseEvent` | - | Событие `onClick` на строке | +| `virtualScroll?` | `boolean` | - | Включение виртуальной прокрутки | +| `stickyHeader?` | `boolean` | - | Зафиксировать шапку сверху | +| `resizable?` | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | +| `zebraStriped?` | `boolean` | - | Окрашивание строк через одну | +| `headerZIndex?` | `number` | `1` | `zIndex` шапки | +| `rowHoverEffect?` | `boolean` | - | Включает эффект наведения на строку | +| `className?` | `string` | - | Дополнительный CSS-класс | +| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | From 5de67d97eb23fb659fbe316d24162c2b09256855 Mon Sep 17 00:00:00 2001 From: alyonurchick1 Date: Wed, 5 Nov 2025 14:52:46 +0300 Subject: [PATCH 4/5] Reapply "docs: fix table" This reverts commit 9f247cbed77ed813fbb8f1354425d207b05fc501. --- .../Table/__stand__/Table.dev.stand.mdx | 3021 ++++++++--------- 1 file changed, 1490 insertions(+), 1531 deletions(-) diff --git a/src/components/Table/__stand__/Table.dev.stand.mdx b/src/components/Table/__stand__/Table.dev.stand.mdx index 28feb65..a3afa68 100644 --- a/src/components/Table/__stand__/Table.dev.stand.mdx +++ b/src/components/Table/__stand__/Table.dev.stand.mdx @@ -32,9 +32,13 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; +- [Обзор](#обзор) +- [Импорт](#импорт) - [Свойства](#свойства) -- [Как формируется таблица](#как-формируется-таблица) -- [Колонка](#колонка) +- [Содержимое](#содержимое) + - [Строки и колонки](#строки-и-колонки) + - [Колонка](#колонка) +- [Внешний вид](#внешний-вид) - [Ширина колонки](#ширина-колонки) - [Разделитель колонок](#разделитель-колонок) - [Закрепление колонок](#закрепление-колонок) @@ -42,75 +46,54 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; - [Представление данных в ячейке](#представление-данных-в-ячейке) - [Представление данных в ячейке в шапке](#представление-данных-в-ячейке-в-шапке) - [Объединение ячеек](#объединение-ячеек) -- [Закрепление шапки](#закрепление-шапки) -- [Виртуальный скролл](#виртуальный-скролл) -- [onScrollToBottom](#onScrollToBottom) -- [Таблица в полоску](#таблица-в-полоску) -- [Выделение строк](#выделение-строк) - - [Наведение на строку](#наведение-на-строку) - - [Выбор строки](#выбор-строки) -- [Управление шириной столбцов](#управление-шириной-столбцов) -- [headerZIndex](#headerzindex) -- [Вложенные строки](#вложенные-строки) -- [Рендер строки](#рендер-строки) -- [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) -- [Ключ строки](#ключ-строки) -- [Адаптивная ширина колонок](#адаптивная-ширина-колонок) -- [Фильтрация](#фильтрация) + - [Закрепление шапки](#закрепление-шапки) + - [Таблица в полоску](#таблица-в-полоску) + - [Управление шириной столбцов](#управление-шириной-столбцов) + - [headerZIndex](#headerzindex) + - [Вложенные строки](#вложенные-строки) + - [Рендер строки](#рендер-строки) + - [Индикатор ячейки и подсказки](#индикатор-ячейки-и-подсказки) + - [Адаптивная ширина колонок](#адаптивная-ширина-колонок) +- [Поведение](#поведение) + - [Виртуальный скролл](#виртуальный-скролл) + - [onScrollToBottom](#onscrolltobottom) + - [Выделение строк](#выделение-строк) + - [Состояние загрузки](#состояние-загрузки) + - [Ключ строки](#ключ-строки) + - [Фильтрация](#фильтрация) +- [Пример использования](#пример-использования) -## Как формируется таблица +## Обзор -Для вывода самой простой таблицы, используйте 2 свойства: - -- `rows` - строки. Тип строки может быть любым массивом объекта `Record`. -- `columns`- колонки. Представляют массив объектов, у которого `accessor` это свойство объекта ваших данных. +Компонент Table предназначен для отображения табличных данных. Он поддерживает настройку колонок, строк, виртуальную прокрутку, закрепление элементов и другие функции для работы с большими наборами данных. - - - +## Импорт ```tsx import { Table, TableColumn } from '@consta/table/Table'; - -type Row = { name: string; profession: string; status: string }; - -const rows: Row[] = [ - { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; - -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - }, - { - title: 'Профессия', - accessor: 'profession', - }, - { - title: 'Статус', - accessor: 'status', - }, -]; - -export const TableExampleSimple = () =>
; ``` - - ## Свойства +| Свойство | Тип | По умолчанию | Описание | +| -------------------------------------------- | ----------------------------- | ----------------- | ------------------------------------------------------------ | +| [`columns?`](#колонка) | `TableColumn[]` | - | Колонки | +| [`rows?`](#строки-и-колонки) | `ROW[]` | - | Строки | +| [`getRowKey?`](#ключ-строки) | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | +| [`onRowMouseEnter?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | +| [`onRowMouseLeave?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | +| [`onRowClick?`](#выделение-строк) | `TableRowMouseEvent` | - | Событие `onClick` на строке | +| [`virtualScroll?`](#виртуальный-скролл) | `boolean` | - | Включение виртуальной прокрутки | +| [`stickyHeader?`](#закрепление-шапки) | `boolean` | - | Зафиксировать шапку сверху | +| [`resizable?`](#управление-шириной-столбцов) | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | +| [`zebraStriped?`](#таблица-в-полоску) | `boolean` | - | Окрашивание строк через одну | +| [`headerZIndex?`](#headerzindex) | `number` | `1` | `zIndex` шапки | +| [`rowHoverEffect?`](#выделение-строк) | `boolean` | - | Включает эффект наведения на строку | +| `className?` | `string` | - | Дополнительный CSS-класс | +| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | + ```ts export type TableRenderHeaderCell = (props: { title?: string; @@ -157,24 +140,58 @@ type TableRowMouseEvent = ( type GetRowKey = (row: ROW) => string | number; ``` -| Свойство | Тип | По умолчанию | Описание | -| ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------ | -| `columns?` | `TableColumn[]` | - | Колонки | -| `rows?` | `ROW[]` | - | Строки | -| `getRowKey?` | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | -| `onRowMouseEnter?` | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | -| `onRowMouseLeave?` | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | -| `onRowClick?` | `TableRowMouseEvent` | - | Событие `onClick` на строке | -| `virtualScroll?` | `boolean` | - | Включение виртуальной прокрутки | -| `stickyHeader?` | `boolean` | - | Зафиксировать шапку сверху | -| `resizable?` | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | -| `zebraStriped?` | `boolean` | - | Окрашивание строк через одну | -| `headerZIndex?` | `number` | `1` | `zIndex` шапки | -| `rowHoverEffect?` | `boolean` | - | Включает эффект наведения на строку | -| `className?` | `string` | - | Дополнительный CSS-класс | -| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | - -## Колонка +## Содержимое + +### Строки и колонки + +Для вывода самой простой таблицы, используйте 2 свойства: + +- `rows` - строки. Тип строки может быть любым массивом объекта `Record`. +- `columns`- колонки. Представляют массив объектов, у которого `accessor` это свойство объекта ваших данных. + + + + + +```tsx +import { Table, TableColumn } from '@consta/table/Table'; + +type Row = { name: string; profession: string; status: string }; + +const rows: Row[] = [ + { + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; + +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + }, + { + title: 'Профессия', + accessor: 'profession', + }, + { + title: 'Статус', + accessor: 'status', + }, +]; + +export const TableExampleSimple = () =>
; +``` + + + +### Колонка Колонки формируются с помощью CSS-свойств `display: grid` и `grid-template-columns`. @@ -220,7 +237,9 @@ export type TableColumn = { | `columns` | Вложенные колонки | | [`colSpan`](#объединение-ячеек) | Функция для вывода количества занятых колонок ячейкой | -## Ширина колонки +## Внешний вид + +### Ширина колонки Для настройки ширины колонок используйте `width`, `maxWidth`, `minWidth`, где `width` — это желаемая ширина колонки. Если все колонки будут помещаться в таблицу без скролла, она останется неизменной. Чтобы таблица не уменьшала или не увеличивала колонку вне желаемых размеров, ограничьте ее, используя `maxWidth` и `minWidth`. @@ -320,7 +339,9 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleWidth = () =>
; +export const TableExampleSeparator = () => ( +
+); ``` @@ -509,7 +530,7 @@ export const TableExampleGroupColumns = () => ( ### Представление данных в ячейке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`DataCell`](##LIBS.LIB.STAND/lib:table/stand:components-datacell-stable). Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `DataCell`. Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. @@ -600,9 +621,7 @@ const columns: TableColumn[] = [ ]; export const TableExampleRenderCell = () => ( - -
- +
); ``` @@ -610,7 +629,7 @@ export const TableExampleRenderCell = () => ( ### Представление данных в ячейке в шапке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`HeaderDataCell`](##LIBS.LIB.STAND/lib:table/stand:components-headerdatacell-stable). +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `HeaderDataCell`. Вы можете в колонке использовать свойство `renderHeaderCell` и выводить данные, как вам потребуется. @@ -712,7 +731,6 @@ export const TableExampleRenderHeaderCell = () => ( import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; import { IconArrowRight } from '@consta/icons/IconArrowRight'; import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; -import { Example } from '@consta/stand'; import { Button } from '@consta/uikit/Button'; import { useMutableRef } from '@consta/uikit/useMutableRef'; import React, { useCallback, useMemo, useState } from 'react'; @@ -916,35 +934,28 @@ export const TableExampleColSpan = () => { -## Закрепление шапки +### Закрепление шапки Чтобы закрепить шапку, используйте свойство `stickyHeader` и ограничьте высоту таблицы в свойстве `maxHeight`. + + ```tsx
``` - - -## Виртуальный скролл - -Если в таблице много данных, включите свойство `virtualScroll` и ограничьте высоту таблицы для лучшей производительности. Может принимать два значения: - -- `boolean` – будет включен виртуальный скролл по горизонтали и вертикали, -- `[boolean, boolean]` – первый элемент определяет, включается ли виртуальный скролл по горизонтали, второй – по вертикали. - - - -Чтобы избежать скачков строки при включении виртуального скролла, задайте одинаковую высоту ячеек в строке. +### Таблица в полоску - +Для отображения таблицы в полоску используйте `zebraStriped`. + + ```tsx
{ columns={columns} stickyHeader virtualScroll + zebraStriped /> ``` - - -## onScrollToBottom - -Свойство `onScrollToBottom` позволяет отслеживать, когда пользователь достиг конца таблицы. Полезно для дозагрузки данных в момент достижения конца таблицы. +### Управление шириной столбцов - +Чтобы дать пользователю возможность управлять шириной столбцов, используйте свойство `resizable`. Уменьшение и увеличение будет в пределах `minWidth` и `maxWidth` у колонки. -```tsx -const TableExampleOnScrollToBottom = () => { - const [isScrollToBottom, setIsScrollToBottom] = useState(false); + - return ( - <> - - {isScrollToBottom ? 'Вы проскроллилили до конца' : 'Скролльте вниз'} - -
setIsScrollToBottom(true)} - /> - - ); -}; -``` +Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. - + - +Закрепленные колонки (`pinned`) не могут изменять размер. -## Таблица в полоску +Есть 2 режима работы: -Для отображения таблицы в полоску используйте `zebraStriped`. +- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. -```tsx -
-``` - - - - - -## Выделение строк + -Строки могут иметь состояние: +```tsx +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; -- Наведение на строку – [`hover`](#наведение-на-строку), -- Выбор строки - [`active`](#выбор-строки) +type ROW = { + athlete: string; + age: number | null; + country: string; +}; - +const columns: TableColumn[] = [ + { + title: 'Имя', + width: 'auto', + accessor: 'athlete', + // Запретили менять ширину у колонки + minWidth: 200, + maxWidth: 200, + }, + { + title: 'Страна', + accessor: 'country', + width: 'auto', + minWidth: 140, + }, + { + title: 'Возраст', + accessor: 'age', + width: 'auto', + minWidth: 100, + }, +]; -Чтобы не рендерить всю таблицу при каждом изменении данных в ячейке, используйте [стейт-менеджер](https://www.reatom.dev/blog/what-is-state-manager/) или [контекст](https://react.dev/reference/react/createContext). +export const TableExampleResizableInside = () => ( +
+); +``` - + -Пример с наведением и выбором строки: +- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. - + ```tsx -import { Example } from '@consta/stand'; -import { action, atom, AtomMut } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; - -import { DataCell } from '@consta/table/DataCell'; -import { DataNumberingCell } from '@consta/table/DataNumberingCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - -// Types +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; type ROW = { - id: number; - name: string; - profession: string; - status: string; - hover: AtomMut; - active: AtomMut; -}; - -// Atoms -const rowsAtom = atom[]>([ - atom({ - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - hover: atom(false), - active: atom(false), - }), - atom({ - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - hover: atom(false), - active: atom(false), - }), -]); - -// Actions - -const onRowClickAction = action<[AtomMut]>((ctx, rowAtom) => { - const row = ctx.get(rowAtom); - row.active(ctx, !ctx.get(row.active)); -}); - -const onRowMouseEnterAction = action<[AtomMut]>((ctx, rowAtom) => { - ctx.get(rowAtom).hover(ctx, true); -}); - -const onRowMouseLeaveAction = action<[AtomMut]>((ctx, rowAtom) => { - ctx.get(rowAtom).hover(ctx, false); -}); - -const DataCellName: TableRenderCell> = (props) => { - const [row] = useAtom(props.row); - const [active] = useAtom(row.active); - const [hover] = useAtom(row.hover); - - return ( - - {row.id} - - ); -}; - -const createDataCellOther = ( - accessor: Exclude, -) => { - const Component: TableRenderCell> = (props) => { - const [row] = useAtom(props.row); - - return {row[accessor]}; - }; - - return Component; -}; - -const columns: TableColumn>[] = [ - { - title: '', - accessor: 'id', - width: 48, - maxWidth: 48, - minWidth: 48, - renderCell: DataCellName, - }, - { - title: 'Имя', - accessor: 'name', - width: 240, - renderCell: createDataCellOther('name'), - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - renderCell: createDataCellOther('profession'), - }, - { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, - renderCell: createDataCellOther('status'), - }, -]; - -export const TableExampleActiveRowWithNumbering = () => { - const onRowClick = useAction(onRowClickAction); - const onRowMouseEnter = useAction(onRowMouseEnterAction); - const onRowMouseLeave = useAction(onRowMouseLeaveAction); - const [rows] = useAtom(rowsAtom); - - return ( - -
- - ); -}; -``` - - - -### Наведение на строку - -Есть 2 способа задать состояние `hover` строкам: - -- Автоматически, с помощью Свойства `rowHoverEffect`. В этом случае все строки будут подсвечиваться при наведении мыши. - -Пример автоматического способа: - - - - - -```tsx -import React from 'react'; - -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; - -type ROW = { - athlete: string; - age: number | null; - country: string; - year: number; - date: string; - sport: string; - gold: number; - silver: number; - bronze: number; - total: number; + athlete: string; + age: number | null; + country: string; }; const columns: TableColumn[] = [ @@ -1200,140 +1057,244 @@ const columns: TableColumn[] = [ title: 'Имя', width: 'auto', accessor: 'athlete', + minWidth: 200, }, { title: 'Страна', accessor: 'country', width: 'auto', + minWidth: 140, + pinned: 'left', }, { title: 'Возраст', accessor: 'age', + width: 'auto', minWidth: 100, }, + { + title: 'Медали', + columns: [ + { + title: 'Бронза', + accessor: 'bronze', + width: 'auto', + minWidth: 100, + }, + { + title: 'Серебро', + accessor: 'silver', + width: 'auto', + minWidth: 100, + }, + { + title: 'Золото', + accessor: 'gold', + width: 'auto', + minWidth: 100, + }, + { + title: 'Всего', + accessor: 'total', + width: 'auto', + minWidth: 100, + }, + ], + }, + { + title: 'Год', + accessor: 'year', + width: 'auto', + minWidth: 140, + pinned: 'right', + }, ]; -export const TableExampleRowHoverEffect = () => ( +export const TableExampleResizableOutside = () => (
); ``` -- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. +### headerZIndex -Пример подконтрольного способа: +Свойство `headerZIndex` влияет на `z-index` следующих элементов: - +- Шапка таблицы. +- Ячейки в закрепленных колонках `z-index: calc(var(--table-header-z-index) - 1)`. +- Ячейки в закрепленных колонках в шапке `z-index: calc(var(--table-header-z-index) + 1)`. +- Ресайзеры колонок `z-index: calc(var(--table-header-z-index) + 10)`. - + -```tsx -import { action, atom } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; +Если вы используете всплывающие окна (`Popover`, `Tooltip`), привязывайте их `z-index` к переменной `--table-header-z-index`. +Например: `z-index: calc(var(--table-header-z-index) + 1)`. Таким образом плавающий элемент будет находится под шапкой таблицы. -import { DataNumberingCell } from '@consta/table/DataNumberingCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; + -// Types +### Вложенные строки -type ROW = { - id: number; - name: string; - profession: string; - status: string; -}; +Реализовать вложенные строки возможно через `renderCell` у колонки, взяв компонент `DataCell` и назначив ему свойство `level`. -const rows: ROW[] = [ - { - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', - }, - { - id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', - }, -]; + -// Atoms + -const hoverIdAtom = atom(undefined); +```tsx +import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; +import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Button } from '@consta/uikit/Button'; +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import React, { useCallback, useMemo, useState } from 'react'; -// Actions +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; +import { range } from '##/utils/array'; -const onRowMouseEnterAction = action<[ROW]>((ctx, row) => - hoverIdAtom(ctx, row.id), -); +type ROW = { + idx: number; + col1: string; + col2: string; + col3: string; + parent: number | undefined; + level: number; +}; -const onRowMouseLeaveAction = action<[ROW]>((ctx, row) => { - const hoverId = ctx.get(hoverIdAtom); - if (hoverId === row.id) { - hoverIdAtom(ctx, undefined); - } +const IconArrow = withAnimateSwitcherHOC({ + startIcon: IconArrowRight, + startDirection: 0, + endDirection: 90, }); -const DataCellName: TableRenderCell = (props) => { - const [hover] = useAtom((ctx) => { - const hoverId = ctx.spy(hoverIdAtom); - return hoverId === props.row.id; - }); +const getDataCell = (idx: number): ROW => { + const parent = Math.floor(idx / 10) * 10; - return ( - {props.row.id} - ); + return { + idx, + col1: `Данные 1 - ${idx}`, + col2: `Данные 2 - ${idx}`, + col3: `Данные 3 - ${idx}`, + parent: parent === idx ? undefined : parent, + level: parent === idx ? 0 : 1, + }; }; -const columns: TableColumn[] = [ - { - title: '', - accessor: 'id', - width: 48, - maxWidth: 48, - minWidth: 48, - renderCell: DataCellName, - }, - { - title: 'Имя', - accessor: 'name', - width: 240, - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - }, - { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, - }, -]; +const data = range(100000).map(getDataCell); -export const TableExampleHoveredControlled = () => { - const onRowMouseEnter = useAction(onRowMouseEnterAction); - const onRowMouseLeave = useAction(onRowMouseLeaveAction); +const DataCellCol1 = (props: { + row: { + col1: string; + parent: number | undefined; + idx: number; + level: number; + }; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { + row: { col1, parent, idx, level }, + opened, + toggle, + } = props; + + return ( + + toggle(idx)} + /> + ) : undefined + } + > + {col1} + + + ); +}; + +export const TableExampleNestedRows = () => { + const [openedList, setOpenedList] = useState([]); + + const openedListRef = useMutableRef(openedList); + + const rows = useMemo(() => { + return data.filter( + (dataItem) => + dataItem.parent === undefined || + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.parent, + ) !== -1, + ); + }, [openedList]); + + const toggle = useCallback((idx: number) => { + setOpenedList((state) => { + const open = state.findIndex((value) => value === idx) !== -1; + if (open) { + return state.filter((value) => value !== idx); + } + return [...state, idx]; + }); + }, []); + + const renderCellCol1: TableRenderCell = useCallback( + (props) => ( + item === props.row.idx) !== + -1 + } + /> + ), + [], + ); + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Колонка - 1', + accessor: 'col1', + renderCell: renderCellCol1, + }, + { + title: 'Колонка - 2', + accessor: 'col2', + }, + { + title: 'Колонка - 3', + accessor: 'col3', + }, + ], + [], + ); return (
row.idx} /> ); }; @@ -1341,699 +1302,849 @@ export const TableExampleHoveredControlled = () => { -### Выбор строки - -Для выбора строки укажите в любой ячейке строки атрибут `data-row-active='true'`. +### Рендер строки -Пример с использованием стейт-менеджера: +Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. - + ```tsx -import { Example } from '@consta/stand'; -import { Checkbox } from '@consta/uikit/Checkbox'; -import { atom, AtomMut } from '@reatom/core'; -import { useAction, useAtom } from '@reatom/npm-react'; -import React from 'react'; +import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; +import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; +import { Button } from '@consta/uikit/Button'; +import { Grid, GridItem } from '@consta/uikit/Grid'; +import { cnMixSpace } from '@consta/uikit/MixSpace'; +import { Text } from '@consta/uikit/Text'; +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import React, { useCallback, useMemo, useState } from 'react'; import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type ROW = { - id: number; - name: string; - profession: string; - status: string; -}; - -const activeIdsAtom = atom>>({}); +const IconArrow = withAnimateSwitcherHOC({ + startIcon: IconArrowRight, + startDirection: 0, + endDirection: 90, +}); -const DataCellName: TableRenderCell = (props) => { - const [active] = useAtom((ctx) => { - const activeAtom = ctx.spy(activeIdsAtom)[props.row.id]; - return activeAtom ? ctx.spy(activeAtom) : false; - }); +type Option = { label: string; value: string }; +type Options = { label: string; value: Option[] }; - const onChange = useAction((ctx) => { - const activeIds = ctx.get(activeIdsAtom); - const activeAtom = ctx.get(activeIdsAtom)[props.row.id]; +type Item = { + id: number; + label: string; + formula: string; + type: string; +}; - if (activeAtom) { - activeAtom(ctx, !ctx.get(activeAtom)); - } else { - activeIdsAtom(ctx, { ...activeIds, [props.row.id]: atom(true) }); - } - }); +type ItemInfo = { + isInfo: number; + options: Options[]; +}; - return ( - } - > - {props.row.name} - - ); +type Row = { + id?: number; + label?: string; + formula?: string; + type?: string; + status?: 'work' | 'problem' | 'wait' | 'success'; + isInfo?: number; + options?: Options[]; }; -const columns: TableColumn[] = [ - { - title: 'Имя', - accessor: 'name', - width: 240, - renderCell: DataCellName, - }, - { - title: 'Профессия', - accessor: 'profession', - width: '1fr', - }, +const data: Row[] = [ { - title: 'Статус', - accessor: 'status', - width: '1fr', - minWidth: 150, + id: 1, + label: 'Запись инклинометрии', + formula: 'Время замера * Количество', + type: 'Кондуктор', }, -]; - -const rows: ROW[] = [ { - id: 1, - name: 'Антон Григорьев', - profession: 'Строитель, который построил дом', - status: 'недоступен', + isInfo: 1, + options: [ + { + label: 'Порты', + value: [ + { label: 'Входящий', value: 'A2-папа' }, + { label: 'Исходящий', value: 'A2-папа' }, + ], + }, + { + label: 'Размеры', + value: [ + { label: 'ширина(мм)', value: '60' }, + { label: 'длинна(мм)', value: '80' }, + ], + }, + ], }, { id: 2, - name: 'Василий Пупкин', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', + label: 'Шаблонирование при бурении', + formula: 'Интервал/Скорость СПО', + type: 'Труба бурильная', }, -]; - -export const TableExampleActiveRow = () => ( - -
- -); -``` - - - -## Управление шириной столбцов - -Чтобы дать пользователю возможность управлять шириной столбцов, используйте свойство `resizable`. Уменьшение и увеличение будет в пределах `minWidth` и `maxWidth` у колонки. + { + isInfo: 2, + options: [ + { + label: 'Диаметры', + value: [ + { label: 'Внешний', value: '14.7' }, + { label: 'Внутренний', value: '12.7' }, + ], + }, + { + label: 'Нагрузка и моменты', + value: [ + { label: 'Растягивающая нагрузка тела трубы, кН', value: '30' }, + { label: 'Допустимый момент на кручение ЗС,кН*м', value: '10' }, + { + label: 'Допустимый момент на кручение тела трубы, кН*м', + value: '10', + }, + { + label: 'Рекомендуемый момент свинчивания, кН*м', + value: '10', + }, + ], + }, + ], + }, +]; - +const isItemInfo = (arg: Row): arg is ItemInfo => + Object.prototype.hasOwnProperty.call(arg, 'isInfo'); -Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. +const isItem = (arg: Row): arg is Item => + Object.prototype.hasOwnProperty.call(arg, 'id'); - +const LabelCell = (props: { + id: number; + label: string; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { id, opened, toggle, label } = props; -Закрепленные колонки (`pinned`) не могут изменять размер. + return ( + + toggle(id)} + /> + } + > + {label} + + + ); +}; -Есть 2 режима работы: +const InfoCell = (props: { options: Options[] }) => { + const { options } = props; + return ( + + {options.map((opt) => { + return ( + <> + + {opt.label} + + {opt.value.map((val) => ( + + {val.value} + + {val.label} + + + ))} + + ); + })} + + ); +}; -- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. +export const TableExampleRenderRow = () => { + const [openedList, setOpenedList] = useState([]); + + const openedListRef = useMutableRef(openedList); + + const rows = useMemo(() => { + return data.filter( + (dataItem) => + Object.prototype.hasOwnProperty.call(dataItem, 'id') || + (isItemInfo(dataItem) && + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.isInfo, + ) !== -1), + ); + }, [openedList]); + + const toggle = useCallback((idx: number) => { + setOpenedList((state) => { + const open = state.findIndex((value) => value === idx) !== -1; + if (open) { + return state.filter((value) => value !== idx); + } + return [...state, idx]; + }); + }, []); + + const renderLabelCell: TableRenderCell = useCallback(({ row }) => { + if (isItem(row)) { + return ( + id === row.id) !== -1} + toggle={toggle} + /> + ); + } + if (isItemInfo(row)) { + return ; + } + return null; + }, []); + + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Название', + accessor: 'label', + renderCell: renderLabelCell, + colSpan: ({ row }) => (isItemInfo(row) ? 'end' : 1), + minWidth: 300, + }, + { + title: 'Формула', + accessor: 'formula', + minWidth: 200, + }, + { + title: 'Тип', + accessor: 'type', + minWidth: 180, + }, + ], + [], + ); + + return ( +
{ + if (isItemInfo(row)) { + return `${row.isInfo}-info`; + } + if (isItem(row)) { + return `${row.id}`; + } + return ''; + }} + /> + ); +}; +``` + + + +### Индикатор ячейки и подсказки + +Для отображения индикатора используйте свойство `indicator` у компонента `DataCell`. Для вывода подсказок используйте компонент `Popover`. В примере ниже реализована логика показа подсказок по наведению курсора. - + + +```tsx +import { Informer } from '@consta/uikit/Informer'; +import { + animateTimeout, + cnMixPopoverAnimate, +} from '@consta/uikit/MixPopoverAnimate'; +import { Popover } from '@consta/uikit/Popover'; +import { useDebounce } from '@consta/uikit/useDebounce'; +import { useFlag } from '@consta/uikit/useFlag'; +import React, { useCallback, useRef } from 'react'; +import { Transition } from 'react-transition-group'; + +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn } from '@consta/table/Table'; + +type Cell = { + data: T; + status?: 'alert' | 'warning'; + statusMessage?: string; +}; + +type Row = { + name: Cell; + profession: Cell; + status: Cell; +}; + +const rows: Row[] = [ + { + name: { + data: 'Антон', + }, + profession: { + data: 'РОЮЛАВТМЯО', + status: 'alert', + statusMessage: 'Неизвестное название диапазона: РОЮЛАВТМЯО.', + }, + status: { + data: 'недоступен', + }, + }, + // ... +]; + +const titleMap: Record<'alert' | 'warning', string> = { + alert: 'Ошибка', + warning: 'Предупреждение', +}; + +const DataCellWithInformer = ({ + data, + tableRef, + statusMessage, + status, +}: { + data: string; + status?: 'alert' | 'warning'; + statusMessage?: string; + tableRef: React.RefObject; +}) => { + const cellRef = useRef(null); + const popoverContentRef = useRef(null); + const [informerVisible, setInformerVisible] = useFlag(); + + const hoverStateRef = useRef<{ + popover: boolean; + anchor: boolean; + }>({ + popover: false, + anchor: false, + }); + + const mouseEnterController = useDebounce( + useCallback(() => { + (hoverStateRef.current.anchor || hoverStateRef.current.popover) && + setInformerVisible.on(); + }, []), + 200, + ); + + const mouseLeaveController = useDebounce( + useCallback(() => { + !hoverStateRef.current.anchor && + !hoverStateRef.current.popover && + setInformerVisible.off(); + }, []), + 200, + ); + + const anchorOnMouseEnter: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.anchor = true; + mouseEnterController(); + }, []); + + const anchorOnMouseLeave: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.anchor = false; + mouseLeaveController(); + }, []); -```tsx -import { Example } from '@consta/stand'; -import React from 'react'; + const popoverOnMouseEnter: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.popover = true; + mouseEnterController(); + }, []); -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + const popoverOnMouseLeave: React.MouseEventHandler = + useCallback((e) => { + hoverStateRef.current.popover = false; + mouseLeaveController(); + }, []); -type ROW = { - athlete: string; - age: number | null; - country: string; + return ( + <> + + {status === 'alert' ? '#ТИПДАННЫХ?' : data} + + {status && ( + + {(animate) => ( + + + + )} + + )} + + ); }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { title: 'Имя', - width: 'auto', - accessor: 'athlete', - // Запретили менять ширину у колонки - minWidth: 200, - maxWidth: 200, + accessor: 'name', + renderCell: ({ row, tableRef }) => ( + + ), }, { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, + title: 'Профессия', + accessor: 'profession', + renderCell: ({ row, tableRef }) => ( + + ), }, { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, + title: 'Статус', + accessor: 'status', + renderCell: ({ row, tableRef }) => ( + + ), }, ]; -export const TableExampleResizable = () => ( -
-); +export const TableExampleWithIndicator = () => { + return ( +
+ ); +}; ``` -- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. +### Адаптивная ширина колонок + +Вы можете менять ширину колонок в зависимости от ширины самой таблицы. - + ```tsx -import { Example } from '@consta/stand'; -import React from 'react'; +import { Button } from '@consta/uikit/Button'; +import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; -type ROW = { - athlete: string; - age: number | null; - country: string; -}; +type Row = { name: string; profession: string; status: string }; -const columns: TableColumn[] = [ +const rows: Row[] = [ { - title: 'Имя', - width: 'auto', - accessor: 'athlete', - minWidth: 200, + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', }, { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, - pinned: 'left', + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', }, - { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, +]; + +const columnsWidthMap: Record< + 's' | 'm' | 'l', + Record<'name' | 'profession' | 'status', TableColumn> +> = { + s: { + name: { + width: 100, + minWidth: 100, + maxWidth: 100, + }, + profession: { + width: '1fr', + minWidth: 150, + }, + status: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, }, - { - title: 'Медали', - columns: [ - { - title: 'Бронза', - accessor: 'bronze', - width: 'auto', - minWidth: 100, - }, + m: { + name: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + profession: { + width: '1fr', + minWidth: 250, + }, + status: { + width: 120, + minWidth: 120, + maxWidth: 120, + }, + }, + l: { + name: { + width: 150, + minWidth: 150, + maxWidth: 150, + }, + profession: { + width: '1fr', + }, + status: { + width: 150, + minWidth: 150, + maxWidth: 150, + }, + }, +}; + +const breakpointsMap = { s: 300, m: 500, l: 760 }; +const breakpointsSequence: (keyof typeof breakpointsMap)[] = ['s', 'm', 'l']; + +export const TableExampleAdaptiveColumns = () => { + const [widthSequence, setWidthSequence] = useState(0); + + const handleWidthChange = useCallback(() => { + setWidthSequence((state) => { + const newState = state + 1; + return newState >= breakpointsSequence.length ? 0 : newState; + }); + }, []); + + const tableRef = useRef(null); + + const point = + getLastPoint( + useBreakpoints({ + ref: tableRef, + map: breakpointsMap, + isActive: true, + }), + ) || 's'; + + const columns: TableColumn[] = useMemo( + () => [ { - title: 'Серебро', - accessor: 'silver', - width: 'auto', - minWidth: 100, + title: 'Имя', + accessor: 'name', + ...columnsWidthMap[point].name, }, { - title: 'Золото', - accessor: 'gold', - width: 'auto', - minWidth: 100, + title: 'Профессия', + accessor: 'profession', + ...columnsWidthMap[point].profession, }, { - title: 'Всего', - accessor: 'total', - width: 'auto', - minWidth: 100, + title: 'Статус', + accessor: 'status', + ...columnsWidthMap[point].status, }, ], - }, - { - title: 'Год', - accessor: 'year', - width: 'auto', - minWidth: 140, - pinned: 'right', - }, -]; + [point], + ); -export const TableExampleResizableOutside = () => ( -
-); + return ( + <> +
+
+``` -type ROW = { - idx: number; - col1: string; - col2: string; - col3: string; - parent: number | undefined; - level: number; -}; + -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); +### onScrollToBottom -const getDataCell = (idx: number): ROW => { - const parent = Math.floor(idx / 10) * 10; +Свойство `onScrollToBottom` позволяет отслеживать, когда пользователь достиг конца таблицы. Полезно для дозагрузки данных в момент достижения конца таблицы. - return { - idx, - col1: `Данные 1 - ${idx}`, - col2: `Данные 2 - ${idx}`, - col3: `Данные 3 - ${idx}`, - parent: parent === idx ? undefined : parent, - level: parent === idx ? 0 : 1, - }; -}; + -const data = range(100000).map(getDataCell); + -const DataCellCol1 = (props: { - row: { - col1: string; - parent: number | undefined; - idx: number; - level: number; - }; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { - row: { col1, parent, idx, level }, - opened, - toggle, - } = props; +```tsx +const TableExampleOnScrollToBottom = () => { + const [isScrollToBottom, setIsScrollToBottom] = useState(false); return ( - - toggle(idx)} - /> - ) : undefined - } - > - {col1} - - + <> + + {isScrollToBottom ? 'Вы проскроллили до конца' : 'Скролльте вниз'} + +
setIsScrollToBottom(true)} + /> + ); }; +``` -export const TableExampleNestedRows = () => { - const [openedList, setOpenedList] = useState([]); - - const openedListRef = useMutableRef(openedList); - - const rows = useMemo(() => { - return data.filter( - (dataItem) => - dataItem.parent === undefined || - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.parent, - ) !== -1, - ); - }, [openedList]); + - const toggle = useCallback((idx: number) => { - setOpenedList((state) => { - const open = state.findIndex((value) => value === idx) !== -1; - if (open) { - return state.filter((value) => value !== idx); - } - return [...state, idx]; - }); - }, []); +### Выделение строк - const renderCellCol1: TableRenderCell = useCallback( - (props) => ( - item === props.row.idx) !== - -1 - } - /> - ), - [], - ); +Строки могут иметь состояние: - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Колонка - 1', - accessor: 'col1', - renderCell: renderCellCol1, - }, - { - title: 'Колонка - 2', - accessor: 'col2', - }, - { - title: 'Колонка - 3', - accessor: 'col3', - }, - ], - [], - ); +- Наведение на строку – `hover`, +- Выбор строки - `active` - return ( -
row.idx} - /> - ); -}; -``` + - +Чтобы не рендерить всю таблицу при каждом изменении данных в ячейке, используйте стейт-менеджер или контекст. -## Рендер строки + -Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. +Пример с наведением и выбором строки: - + ```tsx -import { AnimateIconSwitcherProvider } from '@consta/icons/AnimateIconSwitcherProvider'; -import { IconArrowRight } from '@consta/icons/IconArrowRight'; -import { withAnimateSwitcherHOC } from '@consta/icons/withAnimateSwitcherHOC'; -import { Example } from '@consta/stand'; -import { Button } from '@consta/uikit/Button'; -import { Grid, GridItem } from '@consta/uikit/Grid'; -import { cnMixSpace } from '@consta/uikit/MixSpace'; -import { Text } from '@consta/uikit/Text'; -import { useMutableRef } from '@consta/uikit/useMutableRef'; -import React, { useCallback, useMemo, useState } from 'react'; +import { action, atom, AtomMut } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; +import React from 'react'; import { DataCell } from '@consta/table/DataCell'; +import { DataNumberingCell } from '@consta/table/DataNumberingCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); - -type Option = { label: string; value: string }; -type Options = { label: string; value: Option[] }; +// Types -type Item = { +type ROW = { id: number; - label: string; - formula: string; - type: string; -}; - -type ItemInfo = { - isInfo: number; - options: Options[]; -}; - -type Row = { - id?: number; - label?: string; - formula?: string; - type?: string; - status?: 'work' | 'problem' | 'wait' | 'success'; - isInfo?: number; - options?: Options[]; + name: string; + profession: string; + status: string; + hover: AtomMut; + active: AtomMut; }; -const data: Row[] = [ - { +// Atoms +const rowsAtom = atom[]>([ + atom({ id: 1, - label: 'Запись инклинометрии', - formula: 'Время замера * Количество', - type: 'Кондуктор', - }, - { - isInfo: 1, - options: [ - { - label: 'Порты', - value: [ - { label: 'Входящий', value: 'A2-папа' }, - { label: 'Исходящий', value: 'A2-папа' }, - ], - }, - { - label: 'Размеры', - value: [ - { label: 'ширина(мм)', value: '60' }, - { label: 'длинна(мм)', value: '80' }, - ], - }, - ], - }, - { + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + hover: atom(false), + active: atom(false), + }), + atom({ id: 2, - label: 'Шаблонирование при бурении', - formula: 'Интервал/Скорость СПО', - type: 'Труба бурильная', - }, - { - isInfo: 2, - options: [ - { - label: 'Диаметры', - value: [ - { label: 'Внешний', value: '14.7' }, - { label: 'Внутренний', value: '12.7' }, - ], - }, - { - label: 'Нагрузка и моменты', - value: [ - { label: 'Растягивающая нагрузка тела трубы, кН', value: '30' }, - { label: 'Допустимый момент на кручение ЗС,кН*м', value: '10' }, - { - label: 'Допустимый момент на кручение тела трубы, кН*м', - value: '10', - }, - { - label: 'Рекомендуемый момент свинчивания, кН*м', - value: '10', - }, - ], - }, - ], - }, -]; + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + hover: atom(false), + active: atom(false), + }), +]); -const isItemInfo = (arg: Row): arg is ItemInfo => - Object.prototype.hasOwnProperty.call(arg, 'isInfo'); +// Actions -const isItem = (arg: Row): arg is Item => - Object.prototype.hasOwnProperty.call(arg, 'id'); +const onRowClickAction = action<[AtomMut]>((ctx, rowAtom) => { + const row = ctx.get(rowAtom); + row.active(ctx, !ctx.get(row.active)); +}); -const LabelCell = (props: { - id: number; - label: string; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { id, opened, toggle, label } = props; +const onRowMouseEnterAction = action<[AtomMut]>((ctx, rowAtom) => { + ctx.get(rowAtom).hover(ctx, true); +}); - return ( - - toggle(id)} - /> - } - > - {label} - - - ); -}; +const onRowMouseLeaveAction = action<[AtomMut]>((ctx, rowAtom) => { + ctx.get(rowAtom).hover(ctx, false); +}); + +const DataCellName: TableRenderCell> = (props) => { + const [row] = useAtom(props.row); + const [active] = useAtom(row.active); + const [hover] = useAtom(row.hover); -const InfoCell = (props: { options: Options[] }) => { - const { options } = props; return ( - - {options.map((opt) => { - return ( - <> - - {opt.label} - - {opt.value.map((val) => ( - - {val.value} - - {val.label} - - - ))} - - ); - })} - + + {row.id} + ); }; -export const TableExampleRenderRow = () => { - const [openedList, setOpenedList] = useState([]); - - const openedListRef = useMutableRef(openedList); +const createDataCellOther = ( + accessor: Exclude, +) => { + const Component: TableRenderCell> = (props) => { + const [row] = useAtom(props.row); - const rows = useMemo(() => { - return data.filter( - (dataItem) => - Object.prototype.hasOwnProperty.call(dataItem, 'id') || - (isItemInfo(dataItem) && - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.isInfo, - ) !== -1), - ); - }, [openedList]); + return {row[accessor]}; + }; - const toggle = useCallback((idx: number) => { - setOpenedList((state) => { - const open = state.findIndex((value) => value === idx) !== -1; - if (open) { - return state.filter((value) => value !== idx); - } - return [...state, idx]; - }); - }, []); + return Component; +}; - const renderLabelCell: TableRenderCell = useCallback(({ row }) => { - if (isItem(row)) { - return ( - id === row.id) !== -1} - toggle={toggle} - /> - ); - } - if (isItemInfo(row)) { - return ; - } - return null; - }, []); +const columns: TableColumn>[] = [ + { + title: '', + accessor: 'id', + width: 48, + maxWidth: 48, + minWidth: 48, + renderCell: DataCellName, + }, + { + title: 'Имя', + accessor: 'name', + width: 240, + renderCell: createDataCellOther('name'), + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + renderCell: createDataCellOther('profession'), + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + renderCell: createDataCellOther('status'), + }, +]; - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Название', - accessor: 'label', - renderCell: renderLabelCell, - colSpan: ({ row }) => (isItemInfo(row) ? 'end' : 1), - minWidth: 300, - }, - { - title: 'Формула', - accessor: 'formula', - minWidth: 200, - }, - { - title: 'Тип', - accessor: 'type', - minWidth: 180, - }, - ], - [], - ); +export const TableExampleActiveRowWithNumbering = () => { + const onRowClick = useAction(onRowClickAction); + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); + const [rows] = useAtom(rowsAtom); return (
{ - if (isItemInfo(row)) { - return `${row.isInfo}-info`; - } - if (isItem(row)) { - return `${row.id}`; - } - return ''; - }} + zebraStriped + onRowClick={onRowClick} + onRowMouseEnter={onRowMouseEnter} + onRowMouseLeave={onRowMouseLeave} /> ); }; @@ -2041,72 +2152,170 @@ export const TableExampleRenderRow = () => { -## Состояние загрузки +Есть 2 способа задать состояние `hover` строкам: -Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton. +- Автоматически, с помощью свойства `rowHoverEffect`. В этом случае все строки будут подсвечиваться при наведении мыши. -**Пример загрузки данных всей таблицы с помощью `Loader`:** + + + + +```tsx +import React from 'react'; + +import { Table, TableColumn } from '@consta/table/Table'; +import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + +type ROW = { + athlete: string; + age: number | null; + country: string; + year: number; + date: string; + sport: string; + gold: number; + silver: number; + bronze: number; + total: number; +}; + +const columns: TableColumn[] = [ + { + title: 'Имя', + width: 'auto', + accessor: 'athlete', + }, + { + title: 'Страна', + accessor: 'country', + width: 'auto', + }, + { + title: 'Возраст', + accessor: 'age', + minWidth: 100, + }, +]; + +export const TableExampleRowHoverEffect = () => ( +
+); +``` + + + +- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. - + ```tsx -import { Loader } from '@consta/uikit/Loader'; +import { action, atom } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; import React from 'react'; -import { DataCell } from '@consta/table/DataCell'; +import { DataNumberingCell } from '@consta/table/DataNumberingCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type Item = { +// Types + +type ROW = { id: number; - label: string; + name: string; + profession: string; + status: string; }; -type Loader = { - isLoader: true; -}; +const rows: ROW[] = [ + { + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; -type Row = Item | Loader; +// Atoms -const data: Row[] = [{ isLoader: true }]; +const hoverIdAtom = atom(undefined); -const isLoader = (arg: Row): arg is Loader => - Object.prototype.hasOwnProperty.call(arg, 'isLoader'); +// Actions -const renderIdCell: TableRenderCell = ({ row }) => { - if (isLoader(row)) { - return ( - - - - ); +const onRowMouseEnterAction = action<[ROW]>((ctx, row) => + hoverIdAtom(ctx, row.id), +); + +const onRowMouseLeaveAction = action<[ROW]>((ctx, row) => { + const hoverId = ctx.get(hoverIdAtom); + if (hoverId === row.id) { + hoverIdAtom(ctx, undefined); } - return {row.id}; +}); + +const DataCellName: TableRenderCell = (props) => { + const [hover] = useAtom((ctx) => { + const hoverId = ctx.spy(hoverIdAtom); + return hoverId === props.row.id; + }); + + return ( + {props.row.id} + ); }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { - title: 'Номер', + title: '', accessor: 'id', - renderCell: renderIdCell, - colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), - minWidth: 300, + width: 48, + maxWidth: 48, + minWidth: 48, + renderCell: DataCellName, }, { - title: 'Наименование', - accessor: 'label', - minWidth: 300, + title: 'Имя', + accessor: 'name', + width: 240, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, }, ]; -export const TableExampleLoadingDataWithLoader = () => { +export const TableExampleHoveredControlled = () => { + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); + return (
); }; @@ -2114,86 +2323,108 @@ export const TableExampleLoadingDataWithLoader = () => { -**Пример загрузки данных всей таблицы с помощью `Skeleton`:** +Для выбора строки укажите в любой ячейке строки атрибут `data-row-active='true'`. - + ```tsx -import { Loader } from '@consta/uikit/Loader'; -import { cnMixSpace } from '@consta/uikit/MixSpace'; -import { SkeletonBrick } from '@consta/uikit/Skeleton'; +import { Checkbox } from '@consta/uikit/Checkbox'; +import { atom, AtomMut } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; import React from 'react'; import { DataCell } from '@consta/table/DataCell'; import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type Item = { +type ROW = { id: number; - label: string; + name: string; + profession: string; + status: string; }; -type Loader = { - isLoader: true; +const activeIdsAtom = atom>>({}); + +const DataCellName: TableRenderCell = (props) => { + const [active] = useAtom((ctx) => { + const activeAtom = ctx.spy(activeIdsAtom)[props.row.id]; + return activeAtom ? ctx.spy(activeAtom) : false; + }); + + const onChange = useAction((ctx) => { + const activeIds = ctx.get(activeIdsAtom); + const activeAtom = ctx.get(activeIdsAtom)[props.row.id]; + + if (activeAtom) { + activeAtom(ctx, !ctx.get(activeAtom)); + } else { + activeIdsAtom(ctx, { ...activeIds, [props.row.id]: atom(true) }); + } + }); + + return ( + } + > + {props.row.name} + + ); }; -type Row = Item | Loader; - -const data: Row[] = [ - { isLoader: true }, - { isLoader: true }, - { isLoader: true }, +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + width: 240, + renderCell: DataCellName, + }, + { + title: 'Профессия', + accessor: 'profession', + width: '1fr', + }, + { + title: 'Статус', + accessor: 'status', + width: '1fr', + minWidth: 150, + }, ]; -const isLoader = (arg: Row): arg is Loader => - Object.prototype.hasOwnProperty.call(arg, 'isLoader'); - -const renderIdCell: TableRenderCell = ({ row }) => { - if (isLoader(row)) { - return ( -
- -
- ); - } - return {row.id}; -}; - -const columns: TableColumn[] = [ +const rows: ROW[] = [ { - title: 'Номер', - accessor: 'id', - renderCell: renderIdCell, - colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), - minWidth: 300, + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', }, { - title: 'Наименование', - accessor: 'label', - minWidth: 300, + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', }, ]; -export const TableExampleLoadingDataWithSkeleton = () => { - return ( -
- ); -}; +export const TableExampleActiveRow = () => ( +
+); ``` -**Пример подгрузки строки с помощью `Loader`:** +### Состояние загрузки + +Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton`. + +**Пример загрузки данных всей таблицы с помощью `Loader`:** - + ```tsx import { Loader } from '@consta/uikit/Loader'; @@ -2213,12 +2444,7 @@ type Loader = { type Row = Item | Loader; -const data: Row[] = [ - { id: 1, label: 'Item 1' }, - { id: 2, label: 'Item 2' }, - { id: 3, label: 'Item 3' }, - { isLoader: true }, -]; +const data: Row[] = [{ isLoader: true }]; const isLoader = (arg: Row): arg is Loader => Object.prototype.hasOwnProperty.call(arg, 'isLoader'); @@ -2249,14 +2475,13 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleLoadingRowWithLoader = () => { +export const TableExampleLoadingDataWithLoader = () => { return (
(isLoader(row) ? `loader` : `${row.id}`)} /> ); }; @@ -2264,14 +2489,13 @@ export const TableExampleLoadingRowWithLoader = () => { -**Пример подгрузки строки с помощью `Skeleton`:** +**Пример загрузки данных всей таблицы с помощью `Skeleton`:** - + ```tsx -import { Loader } from '@consta/uikit/Loader'; import { cnMixSpace } from '@consta/uikit/MixSpace'; import { SkeletonBrick } from '@consta/uikit/Skeleton'; import React from 'react'; @@ -2291,9 +2515,8 @@ type Loader = { type Row = Item | Loader; const data: Row[] = [ - { id: 1, label: 'Item 1' }, - { id: 2, label: 'Item 2' }, - { id: 3, label: 'Item 3' }, + { isLoader: true }, + { isLoader: true }, { isLoader: true }, ]; @@ -2326,91 +2549,13 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleLoadingRowWithSkeleton = () => { - return ( -
(isLoader(row) ? `loader` : `${row.id}`)} - /> - ); -}; -``` - - - -**Пример подгрузки вложенной строки с помощью `Loader`:** - - - - - -```tsx -import { IconArrowRight } from '@consta/icons/IconArrowRight'; - -import { Button } from '@consta/uikit/Button'; -import React from 'react'; - -import { DataCell } from '@consta/table/DataCell'; -import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - -type Item = { - id: number; - label: string; - loading?: boolean; -}; - -type Row = Item; - -const data: Row[] = [ - { id: 1, label: 'Item 1' }, - { id: 2, label: 'Item 2' }, - { id: 3, label: 'Item 3' }, - { id: 4, label: 'Item 4', loading: true }, - { id: 5, label: 'Item 5' }, - { id: 6, label: 'Item 6' }, - { id: 7, label: 'Item 7' }, -]; - -const renderIdCell: TableRenderCell = ({ row }) => ( - - } - > - {row.id} - -); - -const columns: TableColumn[] = [ - { - title: 'Номер', - accessor: 'id', - renderCell: renderIdCell, - minWidth: 300, - }, - { - title: 'Наименование', - accessor: 'label', - minWidth: 300, - }, -]; - -export const TableExampleLoadingNestedRowWithLoader = () => { +export const TableExampleLoadingDataWithSkeleton = () => { return (
row.id} /> ); }; @@ -2418,405 +2563,251 @@ export const TableExampleLoadingNestedRowWithLoader = () => { -## Индикатор ячейки и подсказки - -Для отображения индикатора используйте свойство `indicator` у компонента `DataCell`. Для вывода подсказок используйте компонент `Popover`. В примере ниже реализована логика показа подсказок по наведению курсора. - -Пример со всплывающими подсказками: +**Пример подгрузки строки с помощью `Loader`:** - - -```tsx -import { Example } from '@consta/stand'; -import { Informer } from '@consta/uikit/Informer'; -import { - animateTimeout, - cnMixPopoverAnimate, -} from '@consta/uikit/MixPopoverAnimate'; -import { Popover } from '@consta/uikit/Popover'; -import { useDebounce } from '@consta/uikit/useDebounce'; -import { useFlag } from '@consta/uikit/useFlag'; -import React, { useCallback, useRef } from 'react'; -import { Transition } from 'react-transition-group'; - -import { DataCell } from '@consta/table/DataCell'; -import { Table, TableColumn } from '@consta/table/Table'; - -type Cell = { - data: T; - status?: 'alert' | 'warning'; - statusMessage?: string; -}; - -type Row = { - name: Cell; - profession: Cell; - status: Cell; -}; - -const rows: Row[] = [ - { - name: { - data: 'Антон', - }, - profession: { - data: 'РОЮЛАВТМЯО', - status: 'alert', - statusMessage: 'Неизвестное название диапазона: РОЮЛАВТМЯО.', - }, - status: { - data: 'недоступен', - }, - }, - ..//..//.. -]; - -const titleMap: Record<'alert' | 'warning', string> = { - alert: 'Ошибка', - warning: 'Предупреждение', -}; - -const DataCellWithInformer = ({ - data, - tableRef, - statusMessage, - status, -}: { - data: string; - status?: 'alert' | 'warning'; - statusMessage?: string; - tableRef: React.RefObject; -}) => { - const cellRef = useRef(null); - const popoverContentRef = useRef(null); - const [informerVisible, setInformerVisible] = useFlag(); + - const hoverStateRef = useRef<{ - popover: boolean; - anchor: boolean; - }>({ - popover: false, - anchor: false, - }); +```tsx +import { Loader } from '@consta/uikit/Loader'; +import React from 'react'; - const mouseEnterController = useDebounce( - useCallback(() => { - (hoverStateRef.current.anchor || hoverStateRef.current.popover) && - setInformerVisible.on(); - }, []), - 200, - ); +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; - const mouseLeaveController = useDebounce( - useCallback(() => { - !hoverStateRef.current.anchor && - !hoverStateRef.current.popover && - setInformerVisible.off(); - }, []), - 200, - ); +type Item = { + id: number; + label: string; +}; - const anchorOnMouseEnter: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.anchor = true; - mouseEnterController(); - }, []); +type Loader = { + isLoader: true; +}; - const anchorOnMouseLeave: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.anchor = false; - mouseLeaveController(); - }, []); +type Row = Item | Loader; - const popoverOnMouseEnter: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.popover = true; - mouseEnterController(); - }, []); +const data: Row[] = [ + { id: 1, label: 'Item 1' }, + { id: 2, label: 'Item 2' }, + { id: 3, label: 'Item 3' }, + { isLoader: true }, +]; - const popoverOnMouseLeave: React.MouseEventHandler = - useCallback((e) => { - hoverStateRef.current.popover = false; - mouseLeaveController(); - }, []); +const isLoader = (arg: Row): arg is Loader => + Object.prototype.hasOwnProperty.call(arg, 'isLoader'); - return ( - <> - - {status === 'alert' ? '#ТИПДАННЫХ?' : data} +const renderIdCell: TableRenderCell = ({ row }) => { + if (isLoader(row)) { + return ( + + - {status && ( - - {(animate) => ( - - - - )} - - )} - - ); + ); + } + return {row.id}; }; const columns: TableColumn[] = [ { - title: 'Имя', - accessor: 'name', - renderCell: ({ row, tableRef }) => ( - - ), - }, - { - title: 'Профессия', - accessor: 'profession', - renderCell: ({ row, tableRef }) => ( - - ), + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), + minWidth: 300, }, { - title: 'Статус', - accessor: 'status', - renderCell: ({ row, tableRef }) => ( - - ), + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -export const TableExampleWithIndicator = () => { +export const TableExampleLoadingRowWithLoader = () => { return ( -
+
(isLoader(row) ? `loader` : `${row.id}`)} + /> ); }; ``` -## Ключ строки +**Пример подгрузки строки с помощью `Skeleton`:** -Чтобы оптимизировать рендеринг, укажите уникальный ключ строки в `getRowKey`. + - + -Если не указывать ключ строки, компонент попытается взять `row.id`. В случае, если и там ничего нет — `index`. При этом ключи ячеек будут брать ключ строки и добавлять к нему `accessor`. +```tsx +import { cnMixSpace } from '@consta/uikit/MixSpace'; +import { SkeletonBrick } from '@consta/uikit/Skeleton'; +import React from 'react'; - +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -```tsx -
row.uniqueKey} /> +type Item = { + id: number; + label: string; +}; + +type Loader = { + isLoader: true; +}; + +type Row = Item | Loader; + +const data: Row[] = [ + { id: 1, label: 'Item 1' }, + { id: 2, label: 'Item 2' }, + { id: 3, label: 'Item 3' }, + { isLoader: true }, +]; + +const isLoader = (arg: Row): arg is Loader => + Object.prototype.hasOwnProperty.call(arg, 'isLoader'); + +const renderIdCell: TableRenderCell = ({ row }) => { + if (isLoader(row)) { + return ( +
+ +
+ ); + } + return {row.id}; +}; + +const columns: TableColumn[] = [ + { + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), + minWidth: 300, + }, + { + title: 'Наименование', + accessor: 'label', + minWidth: 300, + }, +]; + +export const TableExampleLoadingRowWithSkeleton = () => { + return ( +
(isLoader(row) ? `loader` : `${row.id}`)} + /> + ); +}; ``` -## Адаптивная ширина колонок + -Вы можете менять ширину колонок в зависимости от ширины самой таблицы. +**Пример подгрузки вложенной строки с помощью `Loader`:** - + ```tsx -import { Example } from '@consta/stand'; +import { IconArrowRight } from '@consta/icons/IconArrowRight'; + import { Button } from '@consta/uikit/Button'; -import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React from 'react'; -import { Table, TableColumn } from '@consta/table/Table'; +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type Row = { name: string; profession: string; status: string }; +type Item = { + id: number; + label: string; + loading?: boolean; +}; -const rows: Row[] = [ +type Row = Item; + +const data: Row[] = [ + { id: 1, label: 'Item 1' }, + { id: 2, label: 'Item 2' }, + { id: 3, label: 'Item 3' }, + { id: 4, label: 'Item 4', loading: true }, + { id: 5, label: 'Item 5' }, + { id: 6, label: 'Item 6' }, + { id: 7, label: 'Item 7' }, +]; + +const renderIdCell: TableRenderCell = ({ row }) => ( + + } + > + {row.id} + +); + +const columns: TableColumn[] = [ { - name: 'Антон', - profession: 'Строитель, который построил дом', - status: 'недоступен', + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + minWidth: 300, }, { - name: 'Василий', - profession: 'Отвечает на вопросы, хотя его не спросили', - status: 'на связи', + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -const columnsWidthMap: Record< - 's' | 'm' | 'l', - Record<'name' | 'profession' | 'status', TableColumn> -> = { - s: { - name: { - width: 100, - minWidth: 100, - maxWidth: 100, - }, - profession: { - width: '1fr', - minWidth: 150, - }, - status: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - }, - m: { - name: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - profession: { - width: '1fr', - minWidth: 250, - }, - status: { - width: 120, - minWidth: 120, - maxWidth: 120, - }, - }, - l: { - name: { - width: 150, - minWidth: 150, - maxWidth: 150, - }, - profession: { - width: '1fr', - }, - status: { - width: 150, - minWidth: 150, - maxWidth: 150, - }, - }, +export const TableExampleLoadingNestedRowWithLoader = () => { + return ( +
row.id} + /> + ); }; +``` -const breakpointsMap = { s: 300, m: 500, l: 760 }; -const breakpointsSequence: (keyof typeof breakpointsMap)[] = ['s', 'm', 'l']; + -export const TableExampleAdaptiveColumns = () => { - const [widthSequence, setWidthSequence] = useState(0); +### Ключ строки - const handleWidthChange = useCallback(() => { - setWidthSequence((state) => { - const newState = state + 1; - return newState >= breakpointsSequence.length ? 0 : newState; - }); - }, []); +Чтобы оптимизировать рендеринг, укажите уникальный ключ строки в `getRowKey`. - const tableRef = useRef(null); + - const point = - getLastPoint( - useBreakpoints({ - ref: tableRef, - map: breakpointsMap, - isActive: true, - }), - ) || 's'; +Если не указывать ключ строки, компонент попытается взять `row.id`. В случае, если и там ничего нет — `index`. При этом ключи ячеек будут брать ключ строки и добавлять к нему `accessor`. - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Имя', - accessor: 'name', - ...columnsWidthMap[point].name, - }, - { - title: 'Профессия', - accessor: 'profession', - ...columnsWidthMap[point].profession, - }, - { - title: 'Статус', - accessor: 'status', - ...columnsWidthMap[point].status, - }, - ], - [point], - ); + - return ( - <> -
-
row.uniqueKey} /> ``` - - -## Фильтрация +### Фильтрация -Для фильтрации используйте в `renderHeaderCell` компонент [`FlatSelect`](##LIBS.LIB.STAND/lib:uikit/stand:components-flatselect-stable) +Для фильтрации используйте в `renderHeaderCell` компонент `FlatSelect`. @@ -2824,7 +2815,6 @@ export const TableExampleAdaptiveColumns = () => { ```tsx import { IconFunnel } from '@consta/icons/IconFunnel'; -import { Example } from '@consta/stand'; import { Button } from '@consta/uikit/Button'; import { FlatSelect } from '@consta/uikit/FlatSelect'; import { atom } from '@reatom/core'; @@ -2969,77 +2959,46 @@ const columns: TableColumn[] = [ export const TableExampleFilter = () => { const [rows] = useAtom(rowsAtom); - return ( - -
- - ); + return
; }; ``` -## Свойства +## Пример использования -```ts -export type TableRenderHeaderCell = (props: { - title?: string; - index: number; -}) => React.ReactNode | null; +```tsx +import { Table, TableColumn } from '@consta/table/Table'; -export type TableRenderCell = (props: { - row: T; - rowIndex: number; - columnIndex: number; -}) => React.ReactNode | null; +type Row = { name: string; profession: string; status: string }; -export type TableColumn = { - title?: string; - width?: - | number - | 'auto' - | '1fr' - | '2fr' - | '3fr' - | '4fr' - | '5fr' - | '6fr' - | '7fr' - | '8fr' - | '9fr' - | '10fr'; - maxWidth?: number; - minWidth?: number; - renderHeaderCell?: TableRenderHeaderCell; - isSeparator?: boolean; - pinned?: TableColumnPropPinned; - renderCell?: TableRenderCell; - colSpan?: TabletColSpan; - accessor?: string; - columns?: TableColumn[]; -}; +const rows: Row[] = [ + { + name: 'Антон', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; -type TableRowMouseEvent = ( - row: ROW, - props: { e: React.MouseEvent }, -) => void; +const columns: TableColumn[] = [ + { + title: 'Имя', + accessor: 'name', + }, + { + title: 'Профессия', + accessor: 'profession', + }, + { + title: 'Статус', + accessor: 'status', + }, +]; -type GetRowKey = (row: ROW) => string | number; +
; ``` - -| Свойство | Тип | По умолчанию | Описание | -| ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------ | -| `columns?` | `TableColumn[]` | - | Колонки | -| `rows?` | `ROW[]` | - | Строки | -| `getRowKey?` | `GetRowKey` | `(row) => row.id` | Функция получения ключа, если ключ не найден берется `index` | -| `onRowMouseEnter?` | `TableRowMouseEvent` | - | Событие `onMouseEnter` на строке | -| `onRowMouseLeave?` | `TableRowMouseEvent` | - | Событие `onMouseLeave` на строке | -| `onRowClick?` | `TableRowMouseEvent` | - | Событие `onClick` на строке | -| `virtualScroll?` | `boolean` | - | Включение виртуальной прокрутки | -| `stickyHeader?` | `boolean` | - | Зафиксировать шапку сверху | -| `resizable?` | `'inside'` | `'outside'` | - | Включение возможности изменять ширину колонок | -| `zebraStriped?` | `boolean` | - | Окрашивание строк через одну | -| `headerZIndex?` | `number` | `1` | `zIndex` шапки | -| `rowHoverEffect?` | `boolean` | - | Включает эффект наведения на строку | -| `className?` | `string` | - | Дополнительный CSS-класс | -| `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | From c9520fb3edbdead57718c0e58b27084c3f608bdf Mon Sep 17 00:00:00 2001 From: gizeasy Date: Thu, 6 Nov 2025 12:23:45 +0300 Subject: [PATCH 5/5] Update Table.dev.stand.mdx --- src/components/Table/__stand__/Table.dev.stand.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Table/__stand__/Table.dev.stand.mdx b/src/components/Table/__stand__/Table.dev.stand.mdx index 8dec616..3d624ee 100644 --- a/src/components/Table/__stand__/Table.dev.stand.mdx +++ b/src/components/Table/__stand__/Table.dev.stand.mdx @@ -1562,8 +1562,6 @@ export const TableExampleRenderRow = () => { -### Индикатор ячейки и подсказки - ## Состояние загрузки Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton`.