diff --git a/packages/vkui/src/components/Table/Readme.md b/packages/vkui/src/components/Table/Readme.md new file mode 100644 index 00000000000..c4efe2db562 --- /dev/null +++ b/packages/vkui/src/components/Table/Readme.md @@ -0,0 +1,124 @@ +```jsx { "props": { "layout": false, "adaptivity": true } } +const numberFormatter = new Intl.NumberFormat('ru-RU'); + +const currencyFormatter = new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0, +}); + +const rows = new Array(30).fill(undefined).map(() => { + return { + name: getRandomString(getRandomInt(10, 20)), + status: getRandomInt(0, 1) === 1 ? 'active' : 'disabled', + budget: getRandomInt(1000, 100000), + spent: getRandomInt(10, 10000), + result: getRandomInt(5, 150), + costOfResult: getRandomInt(50, 100), + }; +}); + +const getTotalSpent = () => rows.reduce((total, { spent }) => total + spent, 0); + +const styleContainer = { display: 'flex', alignItems: 'center', gap: 8 }; + +const Status = ({ status = 'active', ...restProps }) => { + switch (status) { + case 'active': + return ( +
+ + Действующий +
+ ); + case 'disabled': + return ( +
+ + Неактивный +
+ ); + default: + return null; + } +}; + +const Example = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + {rows.map((row) => ( + + +
+ {' '} + {row.name} +
+
+ + + + + + {numberFormatter.format(row.budget)}
+ + + + {currencyFormatter.format(row.spent)} + + + {row.result} + + + {currencyFormatter.format(row.costOfResult)}{' '} + + + + ))} + + + + + + + + + + + + + + + + + + + +
Название объявленияСтатусБюджетПотраченоРезультатЦена за рез-тдневнойустановкиустановкиВсего {rows.length} объявлений{currencyFormatter.format(getTotalSpent())}ср. 134ср. 123
+ ); +}; + +; +``` diff --git a/packages/vkui/src/components/Table/Table.module.css b/packages/vkui/src/components/Table/Table.module.css new file mode 100644 index 00000000000..013a020bde3 --- /dev/null +++ b/packages/vkui/src/components/Table/Table.module.css @@ -0,0 +1,7 @@ +.Table { + position: relative; + inline-size: 100%; + border-collapse: separate; + border-spacing: 0; + min-inline-size: 700px; +} diff --git a/packages/vkui/src/components/Table/Table.stories.tsx b/packages/vkui/src/components/Table/Table.stories.tsx new file mode 100644 index 00000000000..4e6e734bf16 --- /dev/null +++ b/packages/vkui/src/components/Table/Table.stories.tsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { getRandomInt, getRandomString } from '@vkontakte/vkjs'; +import { DisableCartesianParam } from '../../storybook/constants'; +import { getAvatarUrl } from '../../testing/mock'; +import { Badge } from '../Badge/Badge'; +import { Image } from '../Image/Image'; +import { TableBody } from '../TableBody/TableBody'; +import { TableCell } from '../TableCell/TableCell'; +import { TableFooter } from '../TableFooter/TableFooter'; +import { TableHeader } from '../TableHeader/TableHeader'; +import { TableRow } from '../TableRow/TableRow'; +import { Caption } from '../Typography/Caption/Caption'; +import { Subhead } from '../Typography/Subhead/Subhead'; +import { Table, type TableProps } from './Table'; + +const story: Meta = { + title: 'Layout/Table', + component: Table, + parameters: { layout: 'fullscreen', ...DisableCartesianParam }, +}; + +export default story; + +const numberFormatter = new Intl.NumberFormat('ru-RU'); + +const currencyFormatter = new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0, +}); + +const rows = new Array(30).fill(undefined).map(() => { + return { + name: getRandomString(getRandomInt(10, 20)), + status: getRandomInt(0, 1) === 1 ? 'active' : 'disabled', + budget: getRandomInt(1000, 100000), + spent: getRandomInt(10, 10000), + result: getRandomInt(5, 150), + costOfResult: getRandomInt(50, 100), + }; +}); + +const getTotalSpent = () => rows.reduce((total, { spent }) => total + spent, 0); + +const styleContainer = { display: 'flex', alignItems: 'center', gap: 8 }; + +const Status = ({ status = 'active', ...restProps }) => { + switch (status) { + case 'active': + return ( +
+ + Действующий +
+ ); + case 'disabled': + return ( +
+ + Неактивный +
+ ); + default: + return null; + } +}; + +type Story = StoryObj; + +export const Playground: Story = { + render(props) { + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + {rows.map((row) => ( + + +
+ {' '} + {row.name} +
+
+ + + + + + {numberFormatter.format(row.budget)}
+ + + + {currencyFormatter.format(row.spent)} + + + {row.result} + + + {currencyFormatter.format(row.costOfResult)}{' '} + + + + ))} + + + + + + + + + + + + + + + + + + + +
Название объявленияСтатусБюджетПотраченоРезультатЦена за рез-тдневнойустановкиустановкиВсего {rows.length} объявлений{currencyFormatter.format(getTotalSpent())}ср. 134ср. 123
+
+
+ ); + }, +}; diff --git a/packages/vkui/src/components/Table/Table.test.tsx b/packages/vkui/src/components/Table/Table.test.tsx new file mode 100644 index 00000000000..1c2a8a47242 --- /dev/null +++ b/packages/vkui/src/components/Table/Table.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { baselineComponent } from '../../testing/utils'; +import { TableBody } from '../TableBody/TableBody'; +import { TableCell } from '../TableCell/TableCell'; +import { TableFooter } from '../TableFooter/TableFooter'; +import { TableHeader } from '../TableHeader/TableHeader'; +import { TableRow } from '../TableRow/TableRow'; +import { Table } from './Table'; + +describe(Table, () => { + baselineComponent((props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); +}); diff --git a/packages/vkui/src/components/Table/Table.tsx b/packages/vkui/src/components/Table/Table.tsx new file mode 100644 index 00000000000..f0d6365dc87 --- /dev/null +++ b/packages/vkui/src/components/Table/Table.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import type { HasRootRef } from '../../types'; +import { RootComponent } from '../RootComponent/RootComponent'; +import { TableContext } from './TableContext'; +import { DEFAULT_TABLE_PADDING } from './constants'; +import type { TableContextProps } from './types'; +import styles from './Table.module.css'; + +export interface TableProps + extends TableContextProps, + React.TableHTMLAttributes, + HasRootRef {} + +export const Table = ({ padding = DEFAULT_TABLE_PADDING, children, ...restProps }: TableProps) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/vkui/src/components/Table/TableContext.ts b/packages/vkui/src/components/Table/TableContext.ts new file mode 100644 index 00000000000..476639d3116 --- /dev/null +++ b/packages/vkui/src/components/Table/TableContext.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { DEFAULT_TABLE_PADDING } from './constants'; +import type { TableContextProps, TableSectionContextProps } from './types'; + +export const TableContext = React.createContext({ + padding: DEFAULT_TABLE_PADDING, +}); + +export const TableSectionContext = React.createContext( + undefined, +); diff --git a/packages/vkui/src/components/Table/constants.ts b/packages/vkui/src/components/Table/constants.ts new file mode 100644 index 00000000000..a273c2e1750 --- /dev/null +++ b/packages/vkui/src/components/Table/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_TABLE_PADDING = 'm'; diff --git a/packages/vkui/src/components/Table/types.ts b/packages/vkui/src/components/Table/types.ts new file mode 100644 index 00000000000..25837e10985 --- /dev/null +++ b/packages/vkui/src/components/Table/types.ts @@ -0,0 +1,20 @@ +import type { LiteralUnion } from '../../types'; + +export type TableContextProps = { + padding?: LiteralUnion<'xs' | 's' | 'm' | 'l', number | string>; +}; + +export type TableSectionType = 'header' | 'body' | 'footer'; + +export type TableSectionContextProps = + | { + type: 'header'; + isSticky: boolean; + } + | { + type: 'body'; + } + | { + type: 'footer'; + isSticky: boolean; + }; diff --git a/packages/vkui/src/components/TableBody/Readme.md b/packages/vkui/src/components/TableBody/Readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/vkui/src/components/TableBody/TableBody.stories.tsx b/packages/vkui/src/components/TableBody/TableBody.stories.tsx new file mode 100644 index 00000000000..0958396bbdd --- /dev/null +++ b/packages/vkui/src/components/TableBody/TableBody.stories.tsx @@ -0,0 +1,14 @@ +// import * as React from 'react'; +import type { Meta } from '@storybook/react'; +import { withVKUILayout } from '../../storybook/VKUIDecorators'; +import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants'; +import { TableBody, type TableBodyProps } from './TableBody'; + +const story: Meta = { + title: 'Layout/TableBody', + component: TableBody, + parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, + decorators: [withVKUILayout], +}; + +export default story; diff --git a/packages/vkui/src/components/TableBody/TableBody.test.tsx b/packages/vkui/src/components/TableBody/TableBody.test.tsx new file mode 100644 index 00000000000..d14dfdbc94e --- /dev/null +++ b/packages/vkui/src/components/TableBody/TableBody.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { baselineComponent } from '../../testing/utils'; +import { Table } from '../Table/Table'; +import { TableCell } from '../TableCell/TableCell'; +import { TableFooter } from '../TableFooter/TableFooter'; +import { TableHeader } from '../TableHeader/TableHeader'; +import { TableRow } from '../TableRow/TableRow'; +import { TableBody } from './TableBody'; + +describe(TableBody, () => { + baselineComponent((props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); +}); diff --git a/packages/vkui/src/components/TableBody/TableBody.tsx b/packages/vkui/src/components/TableBody/TableBody.tsx new file mode 100644 index 00000000000..e3f9166ba66 --- /dev/null +++ b/packages/vkui/src/components/TableBody/TableBody.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { HasRootRef } from '../../types'; +import { RootComponent } from '../RootComponent/RootComponent'; +import { TableSectionContext } from '../Table/TableContext'; + +export interface TableBodyProps + extends React.HTMLAttributes, + HasRootRef {} + +export const TableBody = ({ children, ...restProps }: TableBodyProps) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/vkui/src/components/TableCell/Readme.md b/packages/vkui/src/components/TableCell/Readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/vkui/src/components/TableCell/TableCell.module.css b/packages/vkui/src/components/TableCell/TableCell.module.css new file mode 100644 index 00000000000..23e1f7206f8 --- /dev/null +++ b/packages/vkui/src/components/TableCell/TableCell.module.css @@ -0,0 +1,94 @@ +.TableCell { + vertical-align: inherit; + border-block-end: var(--vkui--size_border--regular) solid + var(--vkui--color_separator_primary_alpha); + background-color: var(--vkui--color_background_content); + box-sizing: border-box; +} + +.TableCell--sticky { + /* Навешиваем sticky тут, т.к. он работает нормально только на ячейках (см. https://caniuse.com/?search=sticky). */ + position: sticky; + backface-visibility: hidden; +} + +.TableCell--section-header.TableCell--sticky { + z-index: 2; + inset-block-start: 0; +} + +.TableCell--section-footer.TableCell--sticky { + z-index: 1; + inset-block-end: 0; +} + +.TableCell--section-header, +.TableCell--section-footer { + background-color: var(--vkui--color_background_secondary); +} + +.TableCell--padding-m { + padding-block-start: 8px; + padding-block-end: calc(8px - var(--vkui--size_border--regular)); + padding-inline: 12px; + block-size: 48px; +} + +.TableCell--align-left { + text-align: start; +} + +.TableCell--align-center { + text-align: center; +} + +.TableCell--align-right { + text-align: end; +} + +.TableCell--align-justify { + text-align: justify; +} + +/** + * Выставляем скругления в нужных ячейках. + * + * > Note: и – уникальные элементы и располагаются в начале и в конце таблицы, + * > соответственно, поэтому нет нужны указывать псевдоклассы в отличие от . + */ + +/* stylelint-disable selector-max-type */ +table > tr:first-of-type > .TableCell:first-child, /* тут нужен именно :first-of-type, т.к. может быть до */ +thead > tr:first-child > .TableCell:first-child, +tbody:first-child > :first-child > .TableCell:first-child { + border-start-start-radius: 8px; +} + +table > tr:first-of-type > .TableCell:last-child, /* тут нужен именно :first-of-type, т.к. может быть до */ +thead > tr:first-child > .TableCell:last-child, +tbody:first-child > :first-child > .TableCell:last-child { + border-start-end-radius: 8px; +} + +table > tr:last-child > .TableCell:first-child, /* тут нужен именно :first-child, а не :first-of-type */ +tbody:last-child > :last-child > .TableCell:first-child, +tfoot > tr:last-child > .TableCell:first-child { + border-end-start-radius: 8px; +} + +table > tr:last-child > .TableCell:last-child, /* тут нужен именно :first-child, а не :first-of-type */ +tbody:last-child > :last-child > .TableCell:last-child, +tfoot > :last-child > .TableCell:last-child { + border-end-end-radius: 8px; +} + +/* + * Удаляем разделитель у ячейках в конце таблицы. + * + * Т.к. должен быть объявлен в конце, то тут не указываем его тут. + */ +table > tr:last-child > .TableCell, +tbody > tr:last-child > .TableCell, +tfoot > tr:last-child > .TableCell { + border-block-end: unset; +} diff --git a/packages/vkui/src/components/TableCell/TableCell.stories.tsx b/packages/vkui/src/components/TableCell/TableCell.stories.tsx new file mode 100644 index 00000000000..3c461d606f7 --- /dev/null +++ b/packages/vkui/src/components/TableCell/TableCell.stories.tsx @@ -0,0 +1,14 @@ +// import * as React from 'react'; +import type { Meta } from '@storybook/react'; +import { withVKUILayout } from '../../storybook/VKUIDecorators'; +import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants'; +import { TableCell, type TableCellProps } from './TableCell'; + +const story: Meta = { + title: 'Layout/TableCell', + component: TableCell, + parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, + decorators: [withVKUILayout], +}; + +export default story; diff --git a/packages/vkui/src/components/TableCell/TableCell.test.tsx b/packages/vkui/src/components/TableCell/TableCell.test.tsx new file mode 100644 index 00000000000..2ee97b70a9f --- /dev/null +++ b/packages/vkui/src/components/TableCell/TableCell.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { baselineComponent } from '../../testing/utils'; +import { Table } from '../Table/Table'; +import { TableBody } from '../TableBody/TableBody'; +import { TableFooter } from '../TableFooter/TableFooter'; +import { TableHeader } from '../TableHeader/TableHeader'; +import { TableRow } from '../TableRow/TableRow'; +import { TableCell } from './TableCell'; + +describe(TableCell, () => { + baselineComponent((props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); + + baselineComponent( + (props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ ), + undefined, + 'baseline (with asHeader prop)', + ); +}); diff --git a/packages/vkui/src/components/TableCell/TableCell.tsx b/packages/vkui/src/components/TableCell/TableCell.tsx new file mode 100644 index 00000000000..669f844e2f2 --- /dev/null +++ b/packages/vkui/src/components/TableCell/TableCell.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import { classNames } from '@vkontakte/vkjs'; +import type { HasRootRef } from '../../types'; +import { RootComponent } from '../RootComponent/RootComponent'; +import { TableContext, TableSectionContext } from '../Table/TableContext'; +import { DEFAULT_TABLE_PADDING } from '../Table/constants'; +import type { TableSectionContextProps } from '../Table/types'; +import styles from './TableCell.module.css'; + +const paddingClassNames = { + xs: styles['TableCell--padding-xs'], + s: styles['TableCell--padding-s'], + m: styles['TableCell--padding-m'], + l: styles['TableCell--padding-l'], +}; + +export type TableCellBaseProps = React.ThHTMLAttributes & + React.TdHTMLAttributes; + +export interface TableCellProps extends TableCellBaseProps, HasRootRef { + /** + * Переключить ячейку в режим заголовка (тег ``). + * + * > Note: при оборачивании в [TableHeader](https://vkcom.github.io/VKUI/#/TableHeader) заголовок + * > автоматически становится `asHeader={true}`. + */ + asHeader?: boolean; +} + +const resolvePropsByTableSectionContext = ( + Component: 'th' | 'td', + scopeProp: string | undefined, + tableSectionTypeProps: TableSectionContextProps | undefined, +) => { + const defaultProps = { + Component, + // у отсутствует атрибут `scope` (см. https://html.spec.whatwg.org/multipage/tables.html#the-td-element) + scope: Component === 'td' ? undefined : scopeProp, + classNamesByContext: [], + }; + + if (!tableSectionTypeProps) { + return defaultProps; + } + + switch (tableSectionTypeProps.type) { + case 'header': + case 'footer': { + /* Навешиваем sticky тут, т.к. он работает нормально только на ячейках (см. https://caniuse.com/?search=sticky). */ + const classNamesByContext = tableSectionTypeProps.isSticky + ? [styles['TableCell--sticky']] + : []; + + if (tableSectionTypeProps.type === 'header') { + classNamesByContext.push(styles['TableCell--section-header']); + return { + Component: 'th' as const, + scope: scopeProp ? scopeProp : 'col', + classNamesByContext, + }; + } + + classNamesByContext.push(styles['TableCell--section-footer']); + return { + Component: defaultProps.Component, + scope: defaultProps.scope, + classNamesByContext, + }; + } + default: + return defaultProps; + } +}; + +export const TableCell = ({ + asHeader, + scope: scopeProp, + align = 'left', + className, + children, + ...restProps +}: TableCellProps) => { + const { padding = DEFAULT_TABLE_PADDING } = React.useContext(TableContext); + const tableSectionContext = React.useContext(TableSectionContext); + const { Component, scope, classNamesByContext } = resolvePropsByTableSectionContext( + asHeader ? 'th' : 'td', + scopeProp, + tableSectionContext, + ); + + const isUnionTypePadding = paddingClassNames.hasOwnProperty(padding); + + return ( + + {children} + + ); +}; diff --git a/packages/vkui/src/components/TableFooter/Readme.md b/packages/vkui/src/components/TableFooter/Readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/vkui/src/components/TableFooter/TableFooter.stories.tsx b/packages/vkui/src/components/TableFooter/TableFooter.stories.tsx new file mode 100644 index 00000000000..06f068ce1e3 --- /dev/null +++ b/packages/vkui/src/components/TableFooter/TableFooter.stories.tsx @@ -0,0 +1,14 @@ +// import * as React from 'react'; +import type { Meta } from '@storybook/react'; +import { withVKUILayout } from '../../storybook/VKUIDecorators'; +import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants'; +import { TableFooter, type TableFooterProps } from './TableFooter'; + +const story: Meta = { + title: 'Layout/TableFooter', + component: TableFooter, + parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, + decorators: [withVKUILayout], +}; + +export default story; diff --git a/packages/vkui/src/components/TableFooter/TableFooter.test.tsx b/packages/vkui/src/components/TableFooter/TableFooter.test.tsx new file mode 100644 index 00000000000..2d0a7d5178d --- /dev/null +++ b/packages/vkui/src/components/TableFooter/TableFooter.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { baselineComponent } from '../../testing/utils'; +import { Table } from '../Table/Table'; +import { TableBody } from '../TableBody/TableBody'; +import { TableCell } from '../TableCell/TableCell'; +import { TableHeader } from '../TableHeader/TableHeader'; +import { TableRow } from '../TableRow/TableRow'; +import { TableFooter } from './TableFooter'; + +describe(TableFooter, () => { + baselineComponent((props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); +}); diff --git a/packages/vkui/src/components/TableFooter/TableFooter.tsx b/packages/vkui/src/components/TableFooter/TableFooter.tsx new file mode 100644 index 00000000000..e39a39f4988 --- /dev/null +++ b/packages/vkui/src/components/TableFooter/TableFooter.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import type { HasRootRef } from '../../types'; +import { RootComponent } from '../RootComponent/RootComponent'; +import { TableSectionContext } from '../Table/TableContext'; + +export interface TableFooterProps + extends React.HTMLAttributes, + HasRootRef { + /** + * Включает прилипания всех переданных в компонент строк. + * + * > Note: в старых браузерах `position: sticky` с таблицами работает стабильно, если вешать его + * > на ячейки (``/``), поэтому свойство пока выставляется на [TableCell](#/TableCell), чтобы + * > удовлетворять `.browserlistrc` библиотеки (см. https://caniuse.com/?search=sticky). + */ + isSticky?: boolean; +} + +export const TableFooter = ({ isSticky = false, children, ...restProps }: TableFooterProps) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/vkui/src/components/TableHeader/Readme.md b/packages/vkui/src/components/TableHeader/Readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/vkui/src/components/TableHeader/TableHeader.stories.tsx b/packages/vkui/src/components/TableHeader/TableHeader.stories.tsx new file mode 100644 index 00000000000..fe262e22374 --- /dev/null +++ b/packages/vkui/src/components/TableHeader/TableHeader.stories.tsx @@ -0,0 +1,14 @@ +// import * as React from 'react'; +import type { Meta } from '@storybook/react'; +import { withVKUILayout } from '../../storybook/VKUIDecorators'; +import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants'; +import { TableHeader, type TableHeaderProps } from './TableHeader'; + +const story: Meta = { + title: 'Layout/TableHeader', + component: TableHeader, + parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, + decorators: [withVKUILayout], +}; + +export default story; diff --git a/packages/vkui/src/components/TableHeader/TableHeader.test.tsx b/packages/vkui/src/components/TableHeader/TableHeader.test.tsx new file mode 100644 index 00000000000..b96f1800908 --- /dev/null +++ b/packages/vkui/src/components/TableHeader/TableHeader.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { baselineComponent } from '../../testing/utils'; +import { Table } from '../Table/Table'; +import { TableBody } from '../TableBody/TableBody'; +import { TableCell } from '../TableCell/TableCell'; +import { TableFooter } from '../TableFooter/TableFooter'; +import { TableRow } from '../TableRow/TableRow'; +import { TableHeader } from './TableHeader'; + +describe(TableHeader, () => { + baselineComponent((props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); +}); diff --git a/packages/vkui/src/components/TableHeader/TableHeader.tsx b/packages/vkui/src/components/TableHeader/TableHeader.tsx new file mode 100644 index 00000000000..524cd1bf047 --- /dev/null +++ b/packages/vkui/src/components/TableHeader/TableHeader.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import type { HasRootRef } from '../../types'; +import { RootComponent } from '../RootComponent/RootComponent'; +import { TableSectionContext } from '../Table/TableContext'; + +export interface TableHeaderProps + extends React.HTMLAttributes, + HasRootRef { + /** + * Включает прилипания всех переданных в компонент строк. + * + * > Note: в старых браузерах `position: sticky` с таблицами работает стабильно, если вешать его + * > на ячейки (``/``), поэтому свойство пока выставляется на [TableCell](#/TableCell), чтобы + * > удовлетворять `.browserlistrc` библиотеки (см. https://caniuse.com/?search=sticky). + */ + isSticky?: boolean; +} + +export const TableHeader = ({ isSticky = false, children, ...restProps }: TableHeaderProps) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/vkui/src/components/TableRow/Readme.md b/packages/vkui/src/components/TableRow/Readme.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/vkui/src/components/TableRow/TableRow.module.css b/packages/vkui/src/components/TableRow/TableRow.module.css new file mode 100644 index 00000000000..79789b14bfe --- /dev/null +++ b/packages/vkui/src/components/TableRow/TableRow.module.css @@ -0,0 +1,5 @@ +.TableRow { + color: inherit; + vertical-align: middle; + outline: 0; +} diff --git a/packages/vkui/src/components/TableRow/TableRow.stories.tsx b/packages/vkui/src/components/TableRow/TableRow.stories.tsx new file mode 100644 index 00000000000..7fc4a4a1945 --- /dev/null +++ b/packages/vkui/src/components/TableRow/TableRow.stories.tsx @@ -0,0 +1,14 @@ +// import * as React from 'react'; +import type { Meta } from '@storybook/react'; +import { withVKUILayout } from '../../storybook/VKUIDecorators'; +import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants'; +import { TableRow, type TableRowProps } from './TableRow'; + +const story: Meta = { + title: 'Layout/TableRow', + component: TableRow, + parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, + decorators: [withVKUILayout], +}; + +export default story; diff --git a/packages/vkui/src/components/TableRow/TableRow.test.tsx b/packages/vkui/src/components/TableRow/TableRow.test.tsx new file mode 100644 index 00000000000..07249a27eb9 --- /dev/null +++ b/packages/vkui/src/components/TableRow/TableRow.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { baselineComponent } from '../../testing/utils'; +import { Table } from '../Table/Table'; +import { TableBody } from '../TableBody/TableBody'; +import { TableCell } from '../TableCell/TableCell'; +import { TableFooter } from '../TableFooter/TableFooter'; +import { TableHeader } from '../TableHeader/TableHeader'; +import { TableRow } from './TableRow'; + +describe(TableRow, () => { + baselineComponent((props) => ( + + + + Header 1 + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); +}); diff --git a/packages/vkui/src/components/TableRow/TableRow.tsx b/packages/vkui/src/components/TableRow/TableRow.tsx new file mode 100644 index 00000000000..ee50f303cfa --- /dev/null +++ b/packages/vkui/src/components/TableRow/TableRow.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import type { HasComponent, HasRootRef } from '../../types'; +import { RootComponent } from '../RootComponent/RootComponent'; +import styles from './TableRow.module.css'; + +export interface TableRowProps + extends React.HTMLAttributes, + HasRootRef, + HasComponent {} + +export const TableRow = ({ Component = 'tr', children, ...restProps }: TableRowProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index e72ac09ec5f..18afd431bcd 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -236,6 +236,24 @@ export type { PaginationProps } from './components/Pagination/Pagination'; export { Accordion } from './components/Accordion/Accordion'; export type { AccordionProps } from './components/Accordion/Accordion'; export type { AccordionSummaryProps } from './components/Accordion/AccordionSummary'; +export { Table } from './components/Table/Table'; +export type { TableProps } from './components/Table/Table'; +export { TableContext, TableSectionContext } from './components/Table/TableContext'; +export type { + TableContextProps, + TableSectionType, + TableSectionContextProps, +} from './components/Table/types'; +export { TableRow } from './components/TableRow/TableRow'; +export type { TableRowProps } from './components/TableRow/TableRow'; +export { TableCell } from './components/TableCell/TableCell'; +export type { TableCellProps } from './components/TableCell/TableCell'; +export { TableHeader } from './components/TableHeader/TableHeader'; +export type { TableHeaderProps } from './components/TableHeader/TableHeader'; +export { TableBody } from './components/TableBody/TableBody'; +export type { TableBodyProps } from './components/TableBody/TableBody'; +export { TableFooter } from './components/TableFooter/TableFooter'; +export type { TableFooterProps } from './components/TableFooter/TableFooter'; /** * Forms diff --git a/styleguide/config.js b/styleguide/config.js index c81131b598b..28eb1d0ac65 100644 --- a/styleguide/config.js +++ b/styleguide/config.js @@ -259,7 +259,12 @@ const baseConfig = { `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/ModalCardBase/ModalCardBase.tsx`, `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/Pagination/Pagination.tsx`, `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/AdaptiveIconRenderer/AdaptiveIconRenderer.tsx`, - `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/ScrollArrow/ScrollArrow.tsx`, + `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/Table/Table.tsx`, + `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/TableRow/TableRow.tsx`, + `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/TableCell/TableCell.tsx`, + `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/TableHeader/TableHeader.tsx`, + `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/TableBody/TableBody.tsx`, + `../${VKUI_PACKAGE.PATHS.COMPONENTS_DIR}/TableFooter/TableFooter.tsx`, ], }, { diff --git a/styleguide/setup.js b/styleguide/setup.js index 3a88ab4c766..055be963740 100644 --- a/styleguide/setup.js +++ b/styleguide/setup.js @@ -4,7 +4,7 @@ import '../packages/vkui/src/styles/common.css'; import { useRef, useState } from 'react'; import * as Icons from '@vkontakte/icons'; -import { noop } from '@vkontakte/vkjs'; +import { getRandomString, noop } from '@vkontakte/vkjs'; import { IconExampleForBadgeBasedOnImageBaseSize, IconExampleForFallbackBasedOnImageBaseSize, @@ -53,6 +53,7 @@ window.importantCountries = importantCountries; window.getRandomInt = getRandomInt; window.getRandomUser = getRandomUser; window.getRandomUsers = getRandomUsers; +window.getRandomString = getRandomString; window.getAllUsers = getAllUsers; window.importantCountries = importantCountries; window.getAvatarUrl = getAvatarUrl;