From b1cdffe73e1ad1b2214bc08d3cd3bcc7f82645b2 Mon Sep 17 00:00:00 2001 From: Inomdzhon Mirdzhamolov Date: Tue, 20 Feb 2024 20:09:43 +0300 Subject: [PATCH 1/2] feat: create table components --- packages/vkui/src/components/Table/Readme.md | 124 ++++++++++++++ .../src/components/Table/Table.module.css | 7 + .../src/components/Table/Table.stories.tsx | 152 ++++++++++++++++++ .../vkui/src/components/Table/Table.test.tsx | 35 ++++ packages/vkui/src/components/Table/Table.tsx | 22 +++ .../vkui/src/components/Table/TableContext.ts | 11 ++ .../vkui/src/components/Table/constants.ts | 1 + packages/vkui/src/components/Table/types.ts | 20 +++ .../vkui/src/components/TableBody/Readme.md | 0 .../TableBody/TableBody.stories.tsx | 14 ++ .../components/TableBody/TableBody.test.tsx | 35 ++++ .../src/components/TableBody/TableBody.tsx | 18 +++ .../vkui/src/components/TableCell/Readme.md | 0 .../components/TableCell/TableCell.module.css | 94 +++++++++++ .../TableCell/TableCell.stories.tsx | 14 ++ .../components/TableCell/TableCell.test.tsx | 64 ++++++++ .../src/components/TableCell/TableCell.tsx | 111 +++++++++++++ .../vkui/src/components/TableFooter/Readme.md | 0 .../TableFooter/TableFooter.stories.tsx | 14 ++ .../TableFooter/TableFooter.test.tsx | 35 ++++ .../components/TableFooter/TableFooter.tsx | 27 ++++ .../vkui/src/components/TableHeader/Readme.md | 0 .../TableHeader/TableHeader.stories.tsx | 14 ++ .../TableHeader/TableHeader.test.tsx | 35 ++++ .../components/TableHeader/TableHeader.tsx | 27 ++++ .../vkui/src/components/TableRow/Readme.md | 0 .../components/TableRow/TableRow.module.css | 5 + .../components/TableRow/TableRow.stories.tsx | 14 ++ .../src/components/TableRow/TableRow.test.tsx | 35 ++++ .../vkui/src/components/TableRow/TableRow.tsx | 17 ++ packages/vkui/src/index.ts | 18 +++ styleguide/config.js | 7 +- styleguide/setup.js | 3 +- 33 files changed, 971 insertions(+), 2 deletions(-) create mode 100644 packages/vkui/src/components/Table/Readme.md create mode 100644 packages/vkui/src/components/Table/Table.module.css create mode 100644 packages/vkui/src/components/Table/Table.stories.tsx create mode 100644 packages/vkui/src/components/Table/Table.test.tsx create mode 100644 packages/vkui/src/components/Table/Table.tsx create mode 100644 packages/vkui/src/components/Table/TableContext.ts create mode 100644 packages/vkui/src/components/Table/constants.ts create mode 100644 packages/vkui/src/components/Table/types.ts create mode 100644 packages/vkui/src/components/TableBody/Readme.md create mode 100644 packages/vkui/src/components/TableBody/TableBody.stories.tsx create mode 100644 packages/vkui/src/components/TableBody/TableBody.test.tsx create mode 100644 packages/vkui/src/components/TableBody/TableBody.tsx create mode 100644 packages/vkui/src/components/TableCell/Readme.md create mode 100644 packages/vkui/src/components/TableCell/TableCell.module.css create mode 100644 packages/vkui/src/components/TableCell/TableCell.stories.tsx create mode 100644 packages/vkui/src/components/TableCell/TableCell.test.tsx create mode 100644 packages/vkui/src/components/TableCell/TableCell.tsx create mode 100644 packages/vkui/src/components/TableFooter/Readme.md create mode 100644 packages/vkui/src/components/TableFooter/TableFooter.stories.tsx create mode 100644 packages/vkui/src/components/TableFooter/TableFooter.test.tsx create mode 100644 packages/vkui/src/components/TableFooter/TableFooter.tsx create mode 100644 packages/vkui/src/components/TableHeader/Readme.md create mode 100644 packages/vkui/src/components/TableHeader/TableHeader.stories.tsx create mode 100644 packages/vkui/src/components/TableHeader/TableHeader.test.tsx create mode 100644 packages/vkui/src/components/TableHeader/TableHeader.tsx create mode 100644 packages/vkui/src/components/TableRow/Readme.md create mode 100644 packages/vkui/src/components/TableRow/TableRow.module.css create mode 100644 packages/vkui/src/components/TableRow/TableRow.stories.tsx create mode 100644 packages/vkui/src/components/TableRow/TableRow.test.tsx create mode 100644 packages/vkui/src/components/TableRow/TableRow.tsx diff --git a/packages/vkui/src/components/Table/Readme.md b/packages/vkui/src/components/Table/Readme.md new file mode 100644 index 0000000000..c4efe2db56 --- /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 0000000000..013a020bde --- /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 0000000000..4e6e734bf1 --- /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 0000000000..1c2a8a4724 --- /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 0000000000..f0d6365dc8 --- /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 0000000000..476639d311 --- /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 0000000000..a273c2e175 --- /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 0000000000..25837e1098 --- /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 0000000000..e69de29bb2 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 0000000000..0958396bbd --- /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 0000000000..d14dfdbc94 --- /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 0000000000..e3f9166ba6 --- /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 0000000000..e69de29bb2 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 0000000000..d60e0bdef3 --- /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 0000000000..3c461d606f --- /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 0000000000..2ee97b70a9 --- /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 0000000000..669f844e2f --- /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 0000000000..e69de29bb2 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 0000000000..06f068ce1e --- /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 0000000000..2d0a7d5178 --- /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 0000000000..e39a39f498 --- /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 0000000000..e69de29bb2 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 0000000000..fe262e2237 --- /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 0000000000..b96f180090 --- /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 0000000000..524cd1bf04 --- /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 0000000000..e69de29bb2 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 0000000000..79789b14bf --- /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 0000000000..7fc4a4a194 --- /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 0000000000..07249a27eb --- /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 0000000000..ee50f303cf --- /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 088ed85c63..5b049258a6 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -245,6 +245,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 d5be99e1a8..c83415b677 100644 --- a/styleguide/config.js +++ b/styleguide/config.js @@ -260,7 +260,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 0ce2199905..1313896f59 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, @@ -44,6 +44,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; From 2130b1e85be83ed3bd5a9c08d5e79e58cb5dea9c Mon Sep 17 00:00:00 2001 From: Inomdzhon Mirdzhamolov Date: Fri, 15 Mar 2024 17:38:09 +0300 Subject: [PATCH 2/2] feat(Table): fix styles; add mock example; add zebra stripes --- .../src/components/Table/Table.stories.tsx | 168 +++++++++++++++++- packages/vkui/src/components/Table/Table.tsx | 9 +- packages/vkui/src/components/Table/types.ts | 1 + .../components/TableCell/TableCell.module.css | 25 ++- .../src/components/TableCell/TableCell.tsx | 118 +++++++----- .../src/components/TableHeaderLabel/Readme.md | 0 .../TableHeaderLabel.module.css | 18 ++ .../TableHeaderLabel.stories.tsx | 14 ++ .../TableHeaderLabel.test.tsx | 38 ++++ .../TableHeaderLabel/TableHeaderLabel.tsx | 49 +++++ packages/vkui/src/index.ts | 2 + packages/vkui/src/lib/utils.ts | 5 + packages/vkui/src/storybook/Placeholder.css | 26 +++ packages/vkui/src/storybook/Placeholder.tsx | 13 ++ .../vkui/src/storybook/assets/chessSquare.png | Bin 0 -> 284 bytes 15 files changed, 425 insertions(+), 61 deletions(-) create mode 100644 packages/vkui/src/components/TableHeaderLabel/Readme.md create mode 100644 packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.module.css create mode 100644 packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.stories.tsx create mode 100644 packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.test.tsx create mode 100644 packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.tsx create mode 100644 packages/vkui/src/storybook/Placeholder.css create mode 100644 packages/vkui/src/storybook/Placeholder.tsx create mode 100644 packages/vkui/src/storybook/assets/chessSquare.png diff --git a/packages/vkui/src/components/Table/Table.stories.tsx b/packages/vkui/src/components/Table/Table.stories.tsx index 4e6e734bf1..af7ba63176 100644 --- a/packages/vkui/src/components/Table/Table.stories.tsx +++ b/packages/vkui/src/components/Table/Table.stories.tsx @@ -1,17 +1,24 @@ import * as React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { getRandomInt, getRandomString } from '@vkontakte/vkjs'; +import { Placeholder } from '../../storybook/Placeholder'; import { DisableCartesianParam } from '../../storybook/constants'; -import { getAvatarUrl } from '../../testing/mock'; +import { getAvatarUrl, getRandomUser } from '../../testing/mock'; import { Badge } from '../Badge/Badge'; +import { Checkbox } from '../Checkbox/Checkbox'; 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 { TableHeaderLabel } from '../TableHeaderLabel/TableHeaderLabel'; import { TableRow } from '../TableRow/TableRow'; import { Caption } from '../Typography/Caption/Caption'; +import { Footnote } from '../Typography/Footnote/Footnote'; +import { Headline } from '../Typography/Headline/Headline'; import { Subhead } from '../Typography/Subhead/Subhead'; +import { Text } from '../Typography/Text/Text'; +import { UsersStack } from '../UsersStack/UsersStack'; import { Table, type TableProps } from './Table'; const story: Meta = { @@ -22,6 +29,154 @@ const story: Meta = { export default story; +type Story = StoryObj; + +export const Mock: Story = { + render: function Mock({ padding, enableZebraStripes, ...restProps }) { + const someUser = React.useMemo(() => getRandomUser(), []); + const [sort, setSort] = React.useState<'ascending' | 'descending'>('ascending'); + const [rows, setRows] = React.useState(() => + new Array(30).fill(undefined).map((_, index) => { + return { + id: index, + checked: false, + }; + }), + ); + const rowSelected = React.useMemo( + () => rows.reduce((acc, row) => (row.checked ? acc + 1 : acc), 0), + [rows], + ); + + const handleClickToSelectAllCells = (event: React.ChangeEvent) => { + setRows((prevRows) => + prevRows.map((row) => { + row.checked = event.target.checked; + return row; + }), + ); + }; + + const handleCellSelected = (event: React.ChangeEvent) => { + const idRaw = event.target.dataset.id; + if (typeof idRaw === 'string') { + const id = Number(idRaw); + setRows((prevRows) => + prevRows.map((row, index) => { + if (id === index) { + row.checked = event.target.checked; + } + return row; + }), + ); + } + }; + + const handleClickToSort = () => { + if (sort === 'descending') { + setRows((prevRows) => prevRows.reverse()); + setSort('ascending'); + } else { + setRows((prevRows) => prevRows.reverse()); + setSort('descending'); + } + }; + + return ( + + + + + 0 && rowSelected < rows.length} + onChange={handleClickToSelectAllCells} + /> + + + + Text + + + + Text + + + Text + + + Text + + + Text + + + + + {rows.map((row) => ( + + + + + +
+ Text +
+ + + + {row.id === 0 ? ( + + Следующим людям понравилось: {someUser.first_name} {someUser.last_name} + + ) : ( + + )} + + + Text ({row.id}) + + + Text ({row.id}) + + + Text ({row.id}) + + + ))} + + + + + + Всего + + + + + Text + + + Text + + + +
Subscription
+ ); + }, +}; + const numberFormatter = new Intl.NumberFormat('ru-RU'); const currencyFormatter = new Intl.NumberFormat('ru-RU', { @@ -43,7 +198,12 @@ const rows = new Array(30).fill(undefined).map(() => { const getTotalSpent = () => rows.reduce((total, { spent }) => total + spent, 0); -const styleContainer = { display: 'flex', alignItems: 'center', gap: 8 }; +const styleContainer: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: 8, +}; const Status = ({ status = 'active', ...restProps }) => { switch (status) { @@ -66,9 +226,7 @@ const Status = ({ status = 'active', ...restProps }) => { } }; -type Story = StoryObj; - -export const Playground: Story = { +export const Example: Story = { render(props) { return (
diff --git a/packages/vkui/src/components/Table/Table.tsx b/packages/vkui/src/components/Table/Table.tsx index f0d6365dc8..52fda34da3 100644 --- a/packages/vkui/src/components/Table/Table.tsx +++ b/packages/vkui/src/components/Table/Table.tsx @@ -11,9 +11,14 @@ export interface TableProps React.TableHTMLAttributes, HasRootRef {} -export const Table = ({ padding = DEFAULT_TABLE_PADDING, children, ...restProps }: TableProps) => { +export const Table = ({ + padding = DEFAULT_TABLE_PADDING, + enableZebraStripes, + children, + ...restProps +}: TableProps) => { return ( - + {children} diff --git a/packages/vkui/src/components/Table/types.ts b/packages/vkui/src/components/Table/types.ts index 25837e1098..c093eb6a3c 100644 --- a/packages/vkui/src/components/Table/types.ts +++ b/packages/vkui/src/components/Table/types.ts @@ -2,6 +2,7 @@ import type { LiteralUnion } from '../../types'; export type TableContextProps = { padding?: LiteralUnion<'xs' | 's' | 'm' | 'l', number | string>; + enableZebraStripes?: boolean; }; export type TableSectionType = 'header' | 'body' | 'footer'; diff --git a/packages/vkui/src/components/TableCell/TableCell.module.css b/packages/vkui/src/components/TableCell/TableCell.module.css index d60e0bdef3..25daf612c5 100644 --- a/packages/vkui/src/components/TableCell/TableCell.module.css +++ b/packages/vkui/src/components/TableCell/TableCell.module.css @@ -24,9 +24,16 @@ .TableCell--section-header, .TableCell--section-footer { + color: var(--vkui--color_text_subhead); background-color: var(--vkui--color_background_secondary); } +/* stylelint-disable selector-max-type */ +tr:nth-child(even) .TableCell--zebra-stripes { + background-color: var(--vkui--color_background_tertiary); +} +/* stylelint-enable selector-max-type */ + .TableCell--padding-m { padding-block-start: 8px; padding-block-end: calc(8px - var(--vkui--size_border--regular)); @@ -61,34 +68,42 @@ 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; + border-start-start-radius: var(--vkui--size_border_radius--regular); } 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; + border-start-end-radius: var(--vkui--size_border_radius--regular); } 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; + border-end-start-radius: var(--vkui--size_border_radius--regular); } 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; + border-end-end-radius: var(--vkui--size_border_radius--regular); } /* * Удаляем разделитель у ячейках в конце таблицы. * - * Т.к. должен быть объявлен в конце, то тут не указываем его тут. + * Т.к. должен быть объявлен только в начале, то тут не указываем его тут. */ table > tr:last-child > .TableCell, tbody > tr:last-child > .TableCell, tfoot > tr:last-child > .TableCell { border-block-end: unset; } + +/* + * Для нужно выставить разделить сверху, т.к. он может быть sticky. + */ +tfoot > tr:last-child > .TableCell { + border-block-start: var(--vkui--size_border--regular) solid + var(--vkui--color_separator_primary_alpha); +} diff --git a/packages/vkui/src/components/TableCell/TableCell.tsx b/packages/vkui/src/components/TableCell/TableCell.tsx index 669f844e2f..cd8e06f2a9 100644 --- a/packages/vkui/src/components/TableCell/TableCell.tsx +++ b/packages/vkui/src/components/TableCell/TableCell.tsx @@ -4,20 +4,14 @@ 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 type { TableContextProps, 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 { + noPadding?: boolean; /** * Переключить ячейку в режим заголовка (тег ``). * @@ -27,12 +21,74 @@ export interface TableCellProps extends TableCellBaseProps, HasRootRef { + const { padding = DEFAULT_TABLE_PADDING, enableZebraStripes } = React.useContext(TableContext); + const [paddingClassName, style] = resolvePaddingProps(padding, styleProp, noPadding); + + const tableSectionContext = React.useContext(TableSectionContext); + const { contextSectionType, Component, scope, classNamesByContext } = + resolvePropsByTableSectionContext(asHeader ? 'th' : 'td', scopeProp, tableSectionContext); + + return ( + + {children} + + ); +}; + +const paddingClassNames = { + xs: styles['TableCell--padding-xs'], + s: styles['TableCell--padding-s'], + m: styles['TableCell--padding-m'], + l: styles['TableCell--padding-l'], +}; + +function resolvePaddingProps( + padding: TableContextProps['padding'], + style?: React.CSSProperties, + noPadding?: boolean, +): [undefined | string, undefined | React.CSSProperties] { + if (!noPadding && padding) { + if (paddingClassNames.hasOwnProperty(padding)) { + return [paddingClassNames[padding], style]; + } else if (style && style.padding === undefined) { + style.padding = padding; + } + } + + return [undefined, style]; +} + +function resolvePropsByTableSectionContext( Component: 'th' | 'td', scopeProp: string | undefined, tableSectionTypeProps: TableSectionContextProps | undefined, -) => { +) { const defaultProps = { + contextSectionType: null, Component, // у отсутствует атрибут `scope` (см. https://html.spec.whatwg.org/multipage/tables.html#the-td-element) scope: Component === 'td' ? undefined : scopeProp, @@ -54,6 +110,7 @@ const resolvePropsByTableSectionContext = ( if (tableSectionTypeProps.type === 'header') { classNamesByContext.push(styles['TableCell--section-header']); return { + contextSectionType: 'header', Component: 'th' as const, scope: scopeProp ? scopeProp : 'col', classNamesByContext, @@ -62,6 +119,7 @@ const resolvePropsByTableSectionContext = ( classNamesByContext.push(styles['TableCell--section-footer']); return { + contextSectionType: 'footer', Component: defaultProps.Component, scope: defaultProps.scope, classNamesByContext, @@ -70,42 +128,4 @@ const resolvePropsByTableSectionContext = ( 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/TableHeaderLabel/Readme.md b/packages/vkui/src/components/TableHeaderLabel/Readme.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.module.css b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.module.css new file mode 100644 index 0000000000..912297d5d3 --- /dev/null +++ b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.module.css @@ -0,0 +1,18 @@ +.TableHeaderLabel { + display: inline-flex; + align-items: center; +} + +.TableHeaderLabel__sortIcon { + margin-inline-end: 4px; +} + +.TableHeaderLabel__titleIcon { + cursor: help; + margin-inline-start: 4px; +} + +.TableHeaderLabel__sortIcon--desc { + transform: rotate(180deg); + transform-origin: center center; +} diff --git a/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.stories.tsx b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.stories.tsx new file mode 100644 index 0000000000..9edf597a08 --- /dev/null +++ b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.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 { TableHeaderLabel, type TableHeaderLabelProps } from './TableHeaderLabel'; + +const story: Meta = { + title: 'Layout/TableHeaderLabel', + component: TableHeaderLabel, + parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, + decorators: [withVKUILayout], +}; + +export default story; diff --git a/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.test.tsx b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.test.tsx new file mode 100644 index 0000000000..2e8f30d8ff --- /dev/null +++ b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.test.tsx @@ -0,0 +1,38 @@ +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/TableRow'; +import { TableHeaderLabel } from './TableHeaderLabel'; + +describe(TableHeaderLabel, () => { + baselineComponent((props) => ( + + + + + Header + + Header 1 + + + + + + Column 1 + + Column 2 + + + + + Footer 1 + Footer 2 + + +
+ )); +}); diff --git a/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.tsx b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.tsx new file mode 100644 index 0000000000..c45f884a9c --- /dev/null +++ b/packages/vkui/src/components/TableHeaderLabel/TableHeaderLabel.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Icon12ArrowUp, Icon12Help } from '@vkontakte/icons'; +import { classNames, hasReactNode } from '@vkontakte/vkjs'; +import { preventEventBubbling } from '../../lib/utils'; +import type { StateProps } from '../Clickable/useState'; +import { Tappable, type TappableProps } from '../Tappable/Tappable'; +import type { StateProps as TappableStateProps } from '../Tappable/state'; +import { Tooltip } from '../Tooltip/Tooltip'; +import styles from './TableHeaderLabel.module.css'; + +export interface TableHeaderLabelProps + extends Omit { + sort?: undefined | 'ascending' | 'descending'; + helpText?: React.ReactNode; +} + +export const TableHeaderLabel = ({ + sort, + children, + helpText, + ...restProps +}: TableHeaderLabelProps) => { + return ( + + {sort ? ( + + ) : null} + {children} + {hasReactNode(helpText) ? ( + + + + ) : null} + + ); +}; diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index 5b049258a6..642889056f 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -257,6 +257,8 @@ 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 { TableHeaderLabel } from './components/TableHeaderLabel/TableHeaderLabel'; +export type { TableHeaderLabelProps } from './components/TableHeaderLabel/TableHeaderLabel'; export { TableHeader } from './components/TableHeader/TableHeader'; export type { TableHeaderProps } from './components/TableHeader/TableHeader'; export { TableBody } from './components/TableBody/TableBody'; diff --git a/packages/vkui/src/lib/utils.ts b/packages/vkui/src/lib/utils.ts index d24cc45c23..9af62c6d51 100644 --- a/packages/vkui/src/lib/utils.ts +++ b/packages/vkui/src/lib/utils.ts @@ -42,6 +42,11 @@ export function multiRef(...refs: Array | undefined>): React.Ref export const stopPropagation = (event: T) => event.stopPropagation(); +export const preventEventBubbling = (event: T) => { + event.preventDefault(); + event.stopPropagation(); +}; + export function addClassNameToElement(element: HTMLElement, className: string) { const elementClassName = element.getAttribute('class') || ''; const updatedClassName = `${elementClassName}${elementClassName ? ' ' : ''}${className}`; diff --git a/packages/vkui/src/storybook/Placeholder.css b/packages/vkui/src/storybook/Placeholder.css new file mode 100644 index 0000000000..048c4a14bf --- /dev/null +++ b/packages/vkui/src/storybook/Placeholder.css @@ -0,0 +1,26 @@ +.Placeholder { + position: relative; + display: flex; + align-items: center; + justify-content: center; + inline-size: 100%; + block-size: 100%; + text-align: center; +} + +.Placeholder::before { + content: ''; + position: absolute; + inset: 0; + opacity: 0.05; + border-radius: 10px; + background-size: contain; + background-image: url('./assets/chessSquare.png'); + overflow: hidden; +} + +.Placeholder__text { + position: relative; + max-inline-size: 120px; + color: var(--vkui--color_text_secondary); +} diff --git a/packages/vkui/src/storybook/Placeholder.tsx b/packages/vkui/src/storybook/Placeholder.tsx new file mode 100644 index 0000000000..dec7ad6af7 --- /dev/null +++ b/packages/vkui/src/storybook/Placeholder.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { Caption } from '../components/Typography/Caption/Caption'; +import './Placeholder.css'; + +export const Placeholder = () => { + return ( +
+ + Поместите сюда любой компонент + +
+ ); +}; diff --git a/packages/vkui/src/storybook/assets/chessSquare.png b/packages/vkui/src/storybook/assets/chessSquare.png new file mode 100644 index 0000000000000000000000000000000000000000..63aa6a4d3df52007b2cb4cded0502477256b58fa GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k3C%+Ln`9l-ge|`ao}(X-0^>5 zHQz_|#D~S}RXFyqax71~*YbLA$?rFL9d?r+dpB@hU|QK(%%8--5!5)T@lI391fEHb zMipF&b`AU*tS?Mv)E?;ex3E9&-nNEiMT6PpOH3QtlNq1QRAOsLSmVfZpdndbKAwT2 zNr9uNDO1IbO}RChy-YhtKLH3QC{)}%Y-z`qz`&N^5P3F*BUV~dfW;AncqA7v