diff --git a/src/components/Table/__stand__/Table.dev.stand.mdx b/src/components/Table/__stand__/Table.dev.stand.mdx index 8ad09b2..3d624ee 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, @@ -31,9 +32,13 @@ import { MdxTabs, MdxMenu, MdxInformer } from '@consta/stand'; +- [Обзор](#обзор) +- [Импорт](#импорт) - [Свойства](#свойства) -- [Как формируется таблица](#как-формируется-таблица) -- [Колонка](#колонка) +- [Содержимое](#содержимое) + - [Строки и колонки](#строки-и-колонки) + - [Колонка](#колонка) +- [Внешний вид](#внешний-вид) - [Ширина колонки](#ширина-колонки) - [Разделитель колонок](#разделитель-колонок) - [Закрепление колонок](#закрепление-колонок) @@ -41,74 +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; @@ -155,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`. @@ -218,7 +237,9 @@ export type TableColumn = { | `columns` | Вложенные колонки | | [`colSpan`](#объединение-ячеек) | Функция для вывода количества занятых колонок ячейкой | -## Ширина колонки +## Внешний вид + +### Ширина колонки Для настройки ширины колонок используйте `width`, `maxWidth`, `minWidth`, где `width` — это желаемая ширина колонки. Если все колонки будут помещаться в таблицу без скролла, она останется неизменной. Чтобы таблица не уменьшала или не увеличивала колонку вне желаемых размеров, ограничьте ее, используя `maxWidth` и `minWidth`. @@ -318,7 +339,9 @@ const columns: TableColumn[] = [ }, ]; -export const TableExampleWidth = () =>
; +export const TableExampleSeparator = () => ( +
+); ``` @@ -507,7 +530,7 @@ export const TableExampleGroupColumns = () => ( ### Представление данных в ячейке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`DataCell`](##LIBS.LIB.STAND/lib:table/stand:components-datacell-stable). Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `DataCell`. Вы можете в колонке использовать свойство `renderCell` и выводить данные, как вам потребуется. @@ -598,9 +621,7 @@ const columns: TableColumn[] = [ ]; export const TableExampleRenderCell = () => ( - -
- +
); ``` @@ -608,7 +629,7 @@ export const TableExampleRenderCell = () => ( ### Представление данных в ячейке в шапке -По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент [`HeaderDataCell`](##LIBS.LIB.STAND/lib:table/stand:components-headerdatacell-stable). +По умолчанию компонент таблицы пытается привести данные к строке и выводит их через компонент `HeaderDataCell`. Вы можете в колонке использовать свойство `renderHeaderCell` и выводить данные, как вам потребуется. @@ -710,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'; @@ -914,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[] = [ @@ -1198,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} /> ); }; @@ -1339,438 +1302,482 @@ 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', + }, + ], + }, + ], + }, +]; -Чтобы запретить изменение ширины у определенной колонки, установите у `minWidth` и `maxWidth` равное значение. +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'); -Закрепленные колонки (`pinned`) не могут изменять размер. +const LabelCell = (props: { + id: number; + label: string; + opened: boolean | undefined; + toggle: (idx: number) => void; +}) => { + const { id, opened, toggle, label } = props; -Есть 2 режима работы: + return ( + + toggle(id)} + /> + } + > + {label} + + + ); +}; -- `inside`. При `resizable = 'inside'` таблица будет стараться уместить весь контент в ширину контейнера таблицы. То есть при увеличении ширины одного столбца будет уменьшаться ширина другого, и наоборот. Рекомендуется использовать этот режим, когда столбцов мало и они все помещаются в таблицу. +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([]); - + const openedListRef = useMutableRef(openedList); -```tsx -import { Example } from '@consta/stand'; -import React from 'react'; + const rows = useMemo(() => { + return data.filter( + (dataItem) => + Object.prototype.hasOwnProperty.call(dataItem, 'id') || + (isItemInfo(dataItem) && + openedList.findIndex( + (openedListItem) => openedListItem === dataItem.isInfo, + ) !== -1), + ); + }, [openedList]); -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; + 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]; + }); + }, []); -type ROW = { - athlete: string; - age: number | null; - country: string; -}; + 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: 'Имя', - width: 'auto', - accessor: 'athlete', - // Запретили менять ширину у колонки - minWidth: 200, - maxWidth: 200, - }, - { - title: 'Страна', - accessor: 'country', - width: 'auto', - minWidth: 140, - }, - { - title: 'Возраст', - accessor: 'age', - width: 'auto', - minWidth: 100, - }, -]; + 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 TableExampleResizable = () => ( -
-); + return ( +
{ + if (isItemInfo(row)) { + return `${row.isInfo}-info`; + } + if (isItem(row)) { + return `${row.id}`; + } + return ''; + }} + /> + ); +}; ``` -- `outside`. При `resizable = 'outside'` таблица будет расширятся при увеличении колонки. Рекомендуется использовать этот режим, когда столбцов много и они не помещаются в таблицу. +## Состояние загрузки + +Загрузку можно отобразить двумя видами с помощью `Loader` или `Skeleton`. + +**Пример загрузки данных всей таблицы с помощью `Loader`:** - + ```tsx -import { Example } from '@consta/stand'; +import { Loader } from '@consta/uikit/Loader'; import React from 'react'; -import { Table, TableColumn } from '@consta/table/Table'; -import rows from '@consta/table/Table/__mocks__/olympic-winners.json'; +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; -type ROW = { - athlete: string; - age: number | null; - country: string; +type Item = { + id: number; + label: string; }; -const columns: TableColumn[] = [ +type Loader = { + isLoader: true; +}; + +type Row = Item | Loader; + +const data: Row[] = [{ 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: 'Имя', - 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: 'id', + renderCell: renderIdCell, + colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), + minWidth: 300, }, { - title: 'Год', - accessor: 'year', - width: 'auto', - minWidth: 140, - pinned: 'right', + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -export const TableExampleResizableOutside = () => ( -
-); +export const TableExampleLoadingDataWithLoader = () => { + return ( +
+ ); +}; ``` -## 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`. +**Пример загрузки данных всей таблицы с помощью `Skeleton`:** - + ```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 { Loader } from '@consta/uikit/Loader'; +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'; -import { range } from '##/utils/array'; -type ROW = { - idx: number; - col1: string; - col2: string; - col3: string; - parent: number | undefined; - level: number; +type Item = { + id: number; + label: string; }; -const IconArrow = withAnimateSwitcherHOC({ - startIcon: IconArrowRight, - startDirection: 0, - endDirection: 90, -}); +type Loader = { + isLoader: true; +}; -const getDataCell = (idx: number): ROW => { - const parent = Math.floor(idx / 10) * 10; +type Row = Item | Loader; - return { - idx, - col1: `Данные 1 - ${idx}`, - col2: `Данные 2 - ${idx}`, - col3: `Данные 3 - ${idx}`, - parent: parent === idx ? undefined : parent, - level: parent === idx ? 0 : 1, - }; -}; +const data: Row[] = [ + { isLoader: true }, + { isLoader: true }, + { isLoader: true }, +]; -const data = range(100000).map(getDataCell); +const isLoader = (arg: Row): arg is Loader => + Object.prototype.hasOwnProperty.call(arg, 'isLoader'); -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 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 TableExampleLoadingDataWithSkeleton = () => { return ( - - toggle(idx)} - /> - ) : undefined - } - > - {col1} - - +
); }; +``` -export const TableExampleNestedRows = () => { - const [openedList, setOpenedList] = useState([]); + - const openedListRef = useMutableRef(openedList); +**Пример подгрузки строки с помощью `Loader`:** - 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 - } - /> - ), - [], - ); +```tsx +import { Loader } from '@consta/uikit/Loader'; +import React from 'react'; - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Колонка - 1', - accessor: 'col1', - renderCell: renderCellCol1, - }, - { - title: 'Колонка - 2', - accessor: 'col2', - }, - { - title: 'Колонка - 3', - accessor: 'col3', - }, - ], - [], - ); +import { DataCell } from '@consta/table/DataCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; + +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 TableExampleLoadingRowWithLoader = () => { return (
row.idx} + getRowKey={(row) => (isLoader(row) ? `loader` : `${row.id}`)} /> ); }; @@ -1778,260 +1785,916 @@ export const TableExampleNestedRows = () => { -## Рендер строки - -Чтобы вывести всю строку с собственным представлением, укажите у первого столбца `colSpan = () => 'end'`, и `renderCell`. +**Пример подгрузки строки с помощью `Skeleton`:** - + ```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 { Loader } from '@consta/uikit/Loader'; 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 { SkeletonBrick } from '@consta/uikit/Skeleton'; +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 = { id: number; label: string; - formula: string; - type: string; }; -type ItemInfo = { - isInfo: number; - options: Options[]; +type Loader = { + isLoader: true; }; -type Row = { - id?: number; - label?: string; - formula?: string; - type?: string; - status?: 'work' | 'problem' | 'wait' | 'success'; - isInfo?: number; - options?: Options[]; -}; +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[] = [ { - id: 1, - label: 'Запись инклинометрии', - formula: 'Время замера * Количество', - type: 'Кондуктор', - }, - { - isInfo: 1, - options: [ - { - label: 'Порты', - value: [ - { label: 'Входящий', value: 'A2-папа' }, - { label: 'Исходящий', value: 'A2-папа' }, - ], - }, - { - label: 'Размеры', - value: [ - { label: 'ширина(мм)', value: '60' }, - { label: 'длинна(мм)', value: '80' }, - ], - }, - ], - }, - { - id: 2, - label: 'Шаблонирование при бурении', - formula: 'Интервал/Скорость СПО', - type: 'Труба бурильная', + title: 'Номер', + accessor: 'id', + renderCell: renderIdCell, + colSpan: ({ row }) => (isLoader(row) ? 'end' : 1), + minWidth: 300, }, { - 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', - }, - ], - }, - ], + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -const isItemInfo = (arg: Row): arg is ItemInfo => - Object.prototype.hasOwnProperty.call(arg, 'isInfo'); +export const TableExampleLoadingRowWithSkeleton = () => { + return ( +
(isLoader(row) ? `loader` : `${row.id}`)} + /> + ); +}; +``` -const isItem = (arg: Row): arg is Item => - Object.prototype.hasOwnProperty.call(arg, 'id'); + -const LabelCell = (props: { +В состоянии загрузки вы можете показывать не только единую строку-заглушку на всю ширину таблицы. При необходимости `Skeleton` можно отрисовывать отдельно в каждой ячейке. + +**Пример подгрузки вложенной строки с помощью `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; - opened: boolean | undefined; - toggle: (idx: number) => void; -}) => { - const { id, opened, toggle, label } = props; - - return ( - - toggle(id)} - /> - } - > - {label} - - - ); + loading?: boolean; }; -const InfoCell = (props: { options: Options[] }) => { - const { options } = props; - return ( - - {options.map((opt) => { - return ( - <> - - {opt.label} - - {opt.value.map((val) => ( - - {val.value} - - {val.label} - - - ))} - - ); - })} - +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 = () => { + return ( +
row.id} + /> + ); +}; +``` + + + +## Индикатор ячейки и подсказки + +Для отображения индикатора используйте свойство `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(); + }, []); + + 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, + }, + { + 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', + }, + { + title: 'Возраст', + accessor: 'age', + minWidth: 100, + }, +]; + +export const TableExampleRowHoverEffect = () => ( +
+); +``` + + + +- Подконтрольный. Для этого нужно у любой ячейки в строке указать атрибут `data-row-hover='true'`. В этом режиме вы можете выбирать, у каких строк включить этот эффект а у каких нет. + + + + + +```tsx +import { action, atom } from '@reatom/core'; +import { useAction, useAtom } from '@reatom/npm-react'; +import React from 'react'; + +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; +}; -export const TableExampleRenderRow = () => { - const [openedList, setOpenedList] = useState([]); +const rows: ROW[] = [ + { + id: 1, + name: 'Антон Григорьев', + profession: 'Строитель, который построил дом', + status: 'недоступен', + }, + { + id: 2, + name: 'Василий Пупкин', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, +]; - const openedListRef = useMutableRef(openedList); +// Atoms - const rows = useMemo(() => { - return data.filter( - (dataItem) => - Object.prototype.hasOwnProperty.call(dataItem, 'id') || - (isItemInfo(dataItem) && - openedList.findIndex( - (openedListItem) => openedListItem === dataItem.isInfo, - ) !== -1), - ); - }, [openedList]); +const hoverIdAtom = atom(undefined); - 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]; - }); - }, []); +// Actions - const renderLabelCell: TableRenderCell = useCallback(({ row }) => { - if (isItem(row)) { - return ( - id === row.id) !== -1} - toggle={toggle} - /> - ); - } - if (isItemInfo(row)) { - return ; - } - return null; - }, []); +const onRowMouseEnterAction = action<[ROW]>((ctx, row) => + hoverIdAtom(ctx, row.id), +); - 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, - }, - ], - [], +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 ( + {props.row.id} ); +}; + +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, + }, +]; + +export const TableExampleHoveredControlled = () => { + const onRowMouseEnter = useAction(onRowMouseEnterAction); + const onRowMouseLeave = useAction(onRowMouseLeaveAction); return (
{ - if (isItemInfo(row)) { - return `${row.isInfo}-info`; - } - if (isItem(row)) { - return `${row.id}`; - } - return ''; - }} + zebraStriped + onRowMouseEnter={onRowMouseEnter} + onRowMouseLeave={onRowMouseLeave} + rowHoverEffect /> ); }; @@ -2039,7 +2702,100 @@ export const TableExampleRenderRow = () => { -## Состояние загрузки +Для выбора строки укажите в любой ячейке строки атрибут `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 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} + + ); +}; + +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`. @@ -2119,7 +2875,6 @@ 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'; @@ -2269,7 +3024,6 @@ 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'; @@ -2339,8 +3093,6 @@ export const TableExampleLoadingRowWithSkeleton = () => { -В состоянии загрузки вы можете показывать не только единую строку-заглушку на всю ширину таблицы. При необходимости `Skeleton` можно отрисовывать отдельно в каждой ячейке. - **Пример подгрузки вложенной строки с помощью `Loader`:** @@ -2396,263 +3148,29 @@ const columns: TableColumn[] = [ renderCell: renderIdCell, 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 }) => ( - - ), + { + title: 'Наименование', + accessor: 'label', + minWidth: 300, }, ]; -export const TableExampleWithIndicator = () => { +export const TableExampleLoadingNestedRowWithLoader = () => { return ( -
+
row.id} + /> ); }; ``` -## Ключ строки +### Ключ строки Чтобы оптимизировать рендеринг, укажите уникальный ключ строки в `getRowKey`. @@ -2666,215 +3184,200 @@ export const TableExampleWithIndicator = () => {
row.uniqueKey} /> ``` -## Адаптивная ширина колонок +### Фильтрация -Вы можете менять ширину колонок в зависимости от ширины самой таблицы. +Для фильтрации используйте в `renderHeaderCell` компонент `FlatSelect`. - + ```tsx -import { Example } from '@consta/stand'; +import { IconFunnel } from '@consta/icons/IconFunnel'; import { Button } from '@consta/uikit/Button'; -import { getLastPoint, useBreakpoints } from '@consta/uikit/useBreakpoints'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { FlatSelect } from '@consta/uikit/FlatSelect'; +import { atom } from '@reatom/core'; +import { useAtom } from '@reatom/npm-react'; +import React, { useRef } from 'react'; -import { Table, TableColumn } from '@consta/table/Table'; - -type Row = { name: string; profession: string; status: string }; +import { HeaderDataCell } from '@consta/table/HeaderDataCell'; +import { Table, TableColumn, TableRenderHeaderCell } from '@consta/table/Table'; -const rows: Row[] = [ +const dataAtom = atom([ { - name: 'Антон', + name: 'Антон Григорьев', profession: 'Строитель, который построил дом', status: 'недоступен', }, { - name: 'Василий', + 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']; +const statusFilterValueAtom = atom([]); +const nameFilterValueAtom = atom([]); -export const TableExampleAdaptiveColumns = () => { - const [widthSequence, setWidthSequence] = useState(0); +const rowsAtom = atom((ctx) => { + const data = ctx.spy(dataAtom); + const statusFilterValue = ctx.spy(statusFilterValueAtom); + const nameFilterValue = ctx.spy(nameFilterValueAtom); - const handleWidthChange = useCallback(() => { - setWidthSequence((state) => { - const newState = state + 1; - return newState >= breakpointsSequence.length ? 0 : newState; - }); - }, []); + 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) + ); + }); +}); - const tableRef = useRef(null); +type ROW = { + name: string; + profession: string; + status: string; +}; - const point = - getLastPoint( - useBreakpoints({ - ref: tableRef, - map: breakpointsMap, - isActive: true, - }), - ) || 's'; +const nameFilterItems = ['Антон Григорьев', 'Василий Пупкин']; +const statusFilterItems = ['недоступен', 'на связи']; - const columns: TableColumn[] = useMemo( - () => [ - { - title: 'Имя', - accessor: 'name', - ...columnsWidthMap[point].name, - }, - { - title: 'Профессия', - accessor: 'profession', - ...columnsWidthMap[point].profession, - }, - { - title: 'Статус', - accessor: 'status', - ...columnsWidthMap[point].status, - }, - ], - [point], +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 }} /> -
; +}; ``` -## Свойства +## Пример использования -```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-элемент | 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"