From 5b138053837b7e8706982af5bbf519e134ba9283 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Mon, 22 Dec 2025 15:19:42 +0300 Subject: [PATCH 01/11] init --- .husky/pre-push | 2 +- package.json | 2 +- src/components/DataCell/DataCell.css | 10 ++ src/components/DataCell/DataCell.tsx | 19 +++- .../DataCell/__stand__/DataCell.dev.stand.mdx | 1 + .../DataCell/__stand__/DataCell.variants.tsx | 2 + src/components/Table/TableCell/TableCell.css | 5 + .../Table/TableRowCell/TableRowCell.tsx | 2 + .../TableExampleSimple/TableExampleSimple.tsx | 35 ++++++- .../TextFieldCell/TextFieldCell.css | 41 ++++++++ .../TextFieldCell/TextFieldCell.tsx | 95 +++++++++++++++++++ src/components/TextFieldCell/index.ts | 1 + yarn.lock | 8 +- 13 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 src/components/TextFieldCell/TextFieldCell.css create mode 100644 src/components/TextFieldCell/TextFieldCell.tsx create mode 100644 src/components/TextFieldCell/index.ts diff --git a/.husky/pre-push b/.husky/pre-push index ae06106..dd56e3a 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -yarn run pre-push \ No newline at end of file +# yarn run pre-push \ No newline at end of file diff --git a/package.json b/package.json index 735bfd3..ce912fa 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "react-refresh": "^0.11.0", "react-router5": "^8.0.1", "react-test-renderer": "^18.0.0", - "react-textarea-autosize": "^8.5.3", + "react-textarea-autosize": "^8.5.9", "react-transition-group": "^4.4.5", "resolve": "^1.20.0", "resolve-url-loader": "^4.0.0", diff --git a/src/components/DataCell/DataCell.css b/src/components/DataCell/DataCell.css index 770a960..1b715f3 100644 --- a/src/components/DataCell/DataCell.css +++ b/src/components/DataCell/DataCell.css @@ -59,6 +59,16 @@ overflow: hidden; } } + + &-Text { + &_lineClamp { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: var(--table-data-cell-line-clamp); + -webkit-box-orient: vertical; + } + } } .MixFlex.ct--DataCell { diff --git a/src/components/DataCell/DataCell.tsx b/src/components/DataCell/DataCell.tsx index a9f9da4..eb1676e 100644 --- a/src/components/DataCell/DataCell.tsx +++ b/src/components/DataCell/DataCell.tsx @@ -21,6 +21,7 @@ export type DataCellProps = { size?: 'm' | 's'; indicator?: 'alert' | 'warning'; truncate?: boolean; + lineClamp?: number; } & JSX.IntrinsicElements['div']; const renderContentSlot = ( @@ -47,16 +48,20 @@ const renderChildren = ( view: DataCellProps['view'], size: 'm' | 's', truncate: boolean, + lineClamp?: number, ) => { if (isString(children) || isNumber(children)) { return renderContentSlot( {children} , @@ -79,6 +84,7 @@ export const DataCell = forwardRef( indicator, style, truncate = false, + lineClamp, ...otherProps } = props; const level = levelProp < 0 ? 0 : levelProp; @@ -98,8 +104,10 @@ export const DataCell = forwardRef( const childrenSlots = children ? [ ...(Array.isArray(children) - ? children.map((item) => renderChildren(item, view, size, truncate)) - : [renderChildren(children, view, size, truncate)]), + ? children.map((item) => + renderChildren(item, view, size, truncate, lineClamp), + ) + : [renderChildren(children, view, size, truncate, lineClamp)]), ] : []; @@ -122,6 +130,7 @@ export const DataCell = forwardRef( ['--table-data-cell-indicator-color' as string]: indicator ? `var(--color-bg-${indicator})` : undefined, + ['--table-data-cell-line-clamp' as string]: lineClamp || undefined, }} ref={ref} > diff --git a/src/components/DataCell/__stand__/DataCell.dev.stand.mdx b/src/components/DataCell/__stand__/DataCell.dev.stand.mdx index 6d69045..65575fe 100644 --- a/src/components/DataCell/__stand__/DataCell.dev.stand.mdx +++ b/src/components/DataCell/__stand__/DataCell.dev.stand.mdx @@ -37,6 +37,7 @@ import { DataCell } from '@consta/table/DataCell'; | [`view?`](#вид) | `'primary'` | `'alert'` | `'success'` | `'warning'` | - | Слот для контролов | | [`level?`](#уровень-вложенности) | `number` | `0` | Уровень вложенности | | `truncate?` | `boolean` | - | Заменяет переполненный текст на многоточие | +| `lineClamp?` | `number` | - | Обрезка текста по строкам | | `className?` | `string` | - | Дополнительный CSS-класс | | `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | diff --git a/src/components/DataCell/__stand__/DataCell.variants.tsx b/src/components/DataCell/__stand__/DataCell.variants.tsx index 8dfd4fd..32c9a3b 100644 --- a/src/components/DataCell/__stand__/DataCell.variants.tsx +++ b/src/components/DataCell/__stand__/DataCell.variants.tsx @@ -23,6 +23,7 @@ const Variants = () => { const text = useText('text', 'Значение ячейки'); const size = useSelect('size', ['m', 's'], 'm'); + const lineClamp = useNumber('lineClamp', 0); const view = useSelect( 'view', ['primary', 'alert', 'success', 'warning'], @@ -82,6 +83,7 @@ const Variants = () => { icon={widthIcon ? IconPhone : undefined} control={controlMap[control || 'without control']} indicator={indicator} + lineClamp={lineClamp} > {children} diff --git a/src/components/Table/TableCell/TableCell.css b/src/components/Table/TableCell/TableCell.css index c0fe6e3..82c2ca8 100644 --- a/src/components/Table/TableCell/TableCell.css +++ b/src/components/Table/TableCell/TableCell.css @@ -48,4 +48,9 @@ &_up { z-index: 1; } + + &:has([data-cell-edit-mode='true']) { + box-shadow: 1px 1px 0px 0px var(--color-control-bg-border-focus) inset, + -1px -1px 0px 0px var(--color-control-bg-border-focus) inset; + } } diff --git a/src/components/Table/TableRowCell/TableRowCell.tsx b/src/components/Table/TableRowCell/TableRowCell.tsx index 4ce2423..629fcf7 100644 --- a/src/components/Table/TableRowCell/TableRowCell.tsx +++ b/src/components/Table/TableRowCell/TableRowCell.tsx @@ -102,6 +102,8 @@ const TableRowCellRender = ( ? `var(--table-column-sticky-right-offset-${index})` : undefined, gridColumn: `${index + 1} / span ${miss > 0 ? miss + 1 : 1}`, + ['--table-cell-grid-column-index' as string]: index, + ['--table-cell-grid-row-index' as string]: rowIndex, }} > {isNotNil(RenderCell) ? ( diff --git a/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx b/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx index b91ea8f..b9724ef 100644 --- a/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx +++ b/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx @@ -1,7 +1,8 @@ import { Example } from '@consta/stand'; -import React from 'react'; +import React, { useState } from 'react'; -import { Table, TableColumn } from '##/components/Table'; +import { Table, TableColumn, TableRenderCell } from '##/components/Table'; +import { TextFieldCell } from '##/components/TextFieldCell'; type Row = { name: string; profession: string; status: string }; @@ -16,19 +17,49 @@ const rows: Row[] = [ profession: 'Отвечает на вопросы, хотя его не спросили', status: 'на связи', }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, + { + name: 'Василий', + profession: 'Отвечает на вопросы, хотя его не спросили', + status: 'на связи', + }, ]; +const RenderCell: TableRenderCell = (row) => { + const [value, setValue] = useState([row.row.name]); + return ( + value?.join(', ')} + // rows={3} + // rows="auto" + size="s" + /> + ); +}; + const columns: TableColumn[] = [ { title: 'Имя', accessor: 'name', + renderCell: RenderCell, }, { title: 'Профессия', accessor: 'profession', + renderCell: RenderCell, }, { title: 'Статус', + renderCell: RenderCell, accessor: 'status', }, ]; diff --git a/src/components/TextFieldCell/TextFieldCell.css b/src/components/TextFieldCell/TextFieldCell.css new file mode 100644 index 0000000..a7126a3 --- /dev/null +++ b/src/components/TextFieldCell/TextFieldCell.css @@ -0,0 +1,41 @@ +.ct--TextFieldCell { + width: 100%; + min-height: 100%; + + &[data-cell-edit-mode='true'] { + --table-cell-edit-grid-column-index: var(--table-cell-grid-column-index); + --table-cell-edit-grid-row-index: var(--table-cell-grid-row-index); + } + + &-Field { + width: 100%; + flex: 1; + &_size { + &_s { + --text-filed-cell-vertical-margin: 0.5px; + } + &_m { + --text-filed-cell-vertical-margin: 3px; + } + } + margin-top: var(--text-filed-cell-vertical-margin); + margin-bottom: var(--text-filed-cell-vertical-margin); + line-height: 1.5em; + } + + &_editMode { + & .ct--DataCell-Slots { + flex: 1; + } + + & .ct--DataCell-ContentSlot { + flex: 1; + } + } + + & .TextFieldTypeTextArea-TextArea { + line-height: var(--line-height-text-m); + } + + /* box-shadow: 12px 12px 2px 1px rgb(0 0 255 / 0.2); */ +} diff --git a/src/components/TextFieldCell/TextFieldCell.tsx b/src/components/TextFieldCell/TextFieldCell.tsx new file mode 100644 index 0000000..1b5e043 --- /dev/null +++ b/src/components/TextFieldCell/TextFieldCell.tsx @@ -0,0 +1,95 @@ +import './TextFieldCell.css'; + +import { useClickOutside } from '@consta/uikit/__internal__/src/hooks/useClickOutside'; +import { cnMixFlex } from '@consta/uikit/MixFlex'; +import { cnMixSpace } from '@consta/uikit/MixSpace'; +import { Text } from '@consta/uikit/Text'; +import { + TextField, + TextFieldProps, + TextFieldPropValue, +} from '@consta/uikit/TextFieldCanary'; +import { useFlag } from '@consta/uikit/useFlag'; +import { useForkRef } from '@consta/uikit/useForkRef'; +import React, { forwardRef, useEffect, useRef } from 'react'; + +import { DataCell } from '##/components/DataCell'; +import { cn } from '##/utils/bem'; + +const cnTextFieldCell = cn('TextFieldCell'); + +export type TextFieldCellProps = TextFieldProps & { + size?: 's' | 'm'; + lineClamp?: number; + readModeRender?: (value?: TextFieldPropValue) => React.ReactNode; +}; + +const readModeRenderDefault = ( + value?: TextFieldPropValue, +) => value; + +export type TextFieldCellComponent = ( + props: TextFieldCellProps, +) => React.ReactNode | null; + +const TextFieldCellRender = ( + props: TextFieldCellProps, + refRoot: React.Ref, +) => { + const { + size = 'm', + lineClamp, + type, + value, + readModeRender = readModeRenderDefault, + ...restProps + } = props; + const [editMode, setEditMode] = useFlag(false); + const refTextField = useRef(null); + const ref = useForkRef([refTextField, refRoot]); + const inputRef = useRef(null); + + useClickOutside({ + isActive: editMode, + ignoreClicksInsideRefs: [refTextField], + handler: setEditMode.off, + }); + + // useEffect(() => { + // if (editMode) { + // inputRef.current?.focus(); + // } + // }, [editMode]); + + // добавить переключение режима по esc + + return ( + + {editMode ? ( + + ) : ( + readModeRender(value) + )} + + ); +}; + +export const TextFieldCell = forwardRef( + TextFieldCellRender, +) as TextFieldCellComponent; diff --git a/src/components/TextFieldCell/index.ts b/src/components/TextFieldCell/index.ts new file mode 100644 index 0000000..1bc66df --- /dev/null +++ b/src/components/TextFieldCell/index.ts @@ -0,0 +1 @@ +export * from './TextFieldCell'; diff --git a/yarn.lock b/yarn.lock index eca9aef..230c6fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13826,10 +13826,10 @@ react-test-renderer@^18.0.0: react-shallow-renderer "^16.15.0" scheduler "^0.23.0" -react-textarea-autosize@^8.5.3: - version "8.5.4" - resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.4.tgz#1c568ad838857b6ce86ee2a96e504179305e0bf4" - integrity sha512-eSSjVtRLcLfFwFcariT77t9hcbVJHQV76b51QjQGarQIHml2+gM2lms0n3XrhnDmgK5B+/Z7TmQk5OHNzqYm/A== +react-textarea-autosize@^8.5.9: + version "8.5.9" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz#ab8627b09aa04d8a2f45d5b5cd94c84d1d4a8893" + integrity sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A== dependencies: "@babel/runtime" "^7.20.13" use-composed-ref "^1.3.0" From 0b129f95104842e90977fc36419347b936213cfe Mon Sep 17 00:00:00 2001 From: gizeasy Date: Mon, 29 Dec 2025 09:43:48 +0300 Subject: [PATCH 02/11] fix --- package.json | 4 +- src/components/Collapse/Collapse.css | 7 +- .../CollapseFullscreen/CollapseFullscreen.css | 4 +- .../CollapseExampleFullscreenContainer.css | 10 +- src/components/DataCell/DataCell.css | 23 ++- src/components/Table/TableCell/TableCell.css | 16 +- src/components/Table/TableData/TableData.css | 50 ++--- .../Table/TableResizers/TableResizers.css | 17 +- src/components/Table/TableRow/TableRow.css | 49 +++-- .../TableExampleSimple/TableExampleSimple.tsx | 25 +-- .../TextFieldCell/TextFieldCell.css | 45 +++-- .../TextFieldCell/TextFieldCell.tsx | 188 ++++++++++++++---- .../__stand__/TextFieldCell.dev.stand.mdx | 9 + .../__stand__/TextFieldCell.stand.mdx | 3 + .../__stand__/TextFieldCell.stand.tsx | 12 ++ .../__stand__/TextFieldCell.variants.tsx | 185 +++++++++++++++++ .../TextFieldCellExampleTypes.tsx | 141 +++++++++++++ src/components/Toolbar/Toolbar.css | 13 +- .../Toolbar/ToolbarDivider/ToolbarDivider.css | 2 +- 19 files changed, 645 insertions(+), 158 deletions(-) create mode 100644 src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx create mode 100644 src/components/TextFieldCell/__stand__/TextFieldCell.stand.mdx create mode 100644 src/components/TextFieldCell/__stand__/TextFieldCell.stand.tsx create mode 100644 src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx create mode 100644 src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx diff --git a/package.json b/package.json index ce912fa..1a3df51 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ } ], "peerDependencies": { - "@consta/icons": "^1.1.1", - "@consta/uikit": "^5.26.0", + "@consta/icons": "^1.5.0", + "@consta/uikit": "^5.28.5", "@reatom/core": "3.10.1", "@reatom/npm-react": "3.10.6" }, diff --git a/src/components/Collapse/Collapse.css b/src/components/Collapse/Collapse.css index 875adb2..9ba78f1 100644 --- a/src/components/Collapse/Collapse.css +++ b/src/components/Collapse/Collapse.css @@ -1,22 +1,25 @@ .ct--Collapse { &-Content { display: grid; - grid-template-rows: 0fr; + grid-template-rows: 0; grid-template-columns: 100%; - transition: grid-template-rows 0.2s ease-in-out; + &_expanded { grid-template-rows: 1fr; } } + &-ChildrenWrapper { overflow: hidden; } + &-Title { display: flex; align-items: center; height: var(--control-height-s); } + &-Toolbar.ct--Toolbar { background: var(--color-bg-secondary); } diff --git a/src/components/Collapse/CollapseFullscreen/CollapseFullscreen.css b/src/components/Collapse/CollapseFullscreen/CollapseFullscreen.css index b87d737..805e26e 100644 --- a/src/components/Collapse/CollapseFullscreen/CollapseFullscreen.css +++ b/src/components/Collapse/CollapseFullscreen/CollapseFullscreen.css @@ -1,8 +1,8 @@ .ct--CollapseFullscreen { position: fixed; top: 0; - left: 0; right: 0; + left: 0; bottom: 0; background-color: var(--color-bg-default); @@ -13,9 +13,11 @@ &-ToolbarWrapper { height: var(--collapse-toolbar-height); } + &-ChildrenWrapper { height: calc(100% - var(--collapse-toolbar-height)); } + &-Toolbar.ct--Toolbar { background: var(--color-bg-secondary); } diff --git a/src/components/Collapse/__stand__/examples/CollapseExampleFullscreenContainer/CollapseExampleFullscreenContainer.css b/src/components/Collapse/__stand__/examples/CollapseExampleFullscreenContainer/CollapseExampleFullscreenContainer.css index 3c131d4..47d945c 100644 --- a/src/components/Collapse/__stand__/examples/CollapseExampleFullscreenContainer/CollapseExampleFullscreenContainer.css +++ b/src/components/Collapse/__stand__/examples/CollapseExampleFullscreenContainer/CollapseExampleFullscreenContainer.css @@ -6,14 +6,16 @@ &-Item { height: 360px; + .ct--Collapse-Content { - height: calc( - 100% - + height: + calc( + 100% - calc( var(--control-height-s) + var(--control-border-width) + - var(--space-xs) * 2 + var(--space-xs) * 2 ) - ); + ); } } diff --git a/src/components/DataCell/DataCell.css b/src/components/DataCell/DataCell.css index 1b715f3..a55ebd4 100644 --- a/src/components/DataCell/DataCell.css +++ b/src/components/DataCell/DataCell.css @@ -1,16 +1,18 @@ /* --table-data-cell-level - задается в компоненте */ .ct--DataCell { padding-right: var(--space-s); - padding-left: calc( - var(--space-s) + (var(--table-data-cell-slot-width) + var(--space-2xs)) * + padding-left: + calc( + var(--space-s) + (var(--table-data-cell-slot-width) + var(--space-2xs)) * var(--table-data-cell-level, 0) + var(--table-data-cell-additional-space, calc(0 * 1px)) - ); + ); &_alignmentIndent { - --table-data-cell-additional-space: calc( - var(--table-data-cell-gap) - var(--space-2xs) - ); + --table-data-cell-additional-space: + calc( + var(--table-data-cell-gap) - var(--space-2xs) + ); } &_size { @@ -37,7 +39,8 @@ right: 0; border: var(--space-2xs) solid transparent; border-top: var(--space-2xs) solid var(--table-data-cell-indicator-color); - border-right: var(--space-2xs) solid + border-right: + var(--space-2xs) solid var(--table-data-cell-indicator-color); } } @@ -55,16 +58,20 @@ &-ContentSlot { min-height: var(--table-data-cell-slot-height); + &_truncate { overflow: hidden; } } &-Text { + white-space: pre-wrap; + word-break: break-word; + &_lineClamp { + display: -webkit-box; overflow: hidden; text-overflow: ellipsis; - display: -webkit-box; -webkit-line-clamp: var(--table-data-cell-line-clamp); -webkit-box-orient: vertical; } diff --git a/src/components/Table/TableCell/TableCell.css b/src/components/Table/TableCell/TableCell.css index 82c2ca8..bedd16f 100644 --- a/src/components/Table/TableCell/TableCell.css +++ b/src/components/Table/TableCell/TableCell.css @@ -33,11 +33,12 @@ display: block; width: 100%; height: 100%; - background: color-mix( - in srgb, - rgb(from var(--color-bg-stripe) r g b / 1) 5%, - var(--color-bg-default) - ); + background: + color-mix( + in srgb, + rgb(from var(--color-bg-stripe) r g b / 1) 5%, + var(--color-bg-default) + ); } } @@ -50,7 +51,8 @@ } &:has([data-cell-edit-mode='true']) { - box-shadow: 1px 1px 0px 0px var(--color-control-bg-border-focus) inset, - -1px -1px 0px 0px var(--color-control-bg-border-focus) inset; + box-shadow: + 1px 1px 0 0 var(--color-control-bg-border-focus) inset, + -1px -1px 0 0 var(--color-control-bg-border-focus) inset; } } diff --git a/src/components/Table/TableData/TableData.css b/src/components/Table/TableData/TableData.css index a480eb3..9b89389 100644 --- a/src/components/Table/TableData/TableData.css +++ b/src/components/Table/TableData/TableData.css @@ -1,6 +1,6 @@ .ct--TableData { - display: contents; --table-data-cell-bg: var(--color-bg-default); + display: contents; &-Cell { grid-column: span var(--table-cell-col-span, 1); @@ -16,41 +16,45 @@ display: contents; &_zebraStriped { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-bg-stripe) r g b / 1) 5%, - var(--color-bg-default) - ); + --table-data-cell-bg: + color-mix( + in srgb, + rgb(from var(--color-bg-stripe) r g b / 1) 5%, + var(--color-bg-default) + ); } &:has(> * > [data-row-active='true']) { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-control-bg-primary) r g b / 1) 15%, - var(--color-bg-default) - ); + --table-data-cell-bg: + color-mix( + in srgb, + rgb(from var(--color-control-bg-primary) r g b / 1) 15%, + var(--color-bg-default) + ); } &:has(> * > [data-row-hover='true']), .ct--TableData_rowHoverEffect &:hover { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 9%, - var(--color-bg-default) - ); + --table-data-cell-bg: + color-mix( + in srgb, + rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 9%, + var(--color-bg-default) + ); } &:has(> * > [data-row-hover='true'][data-row-active='true']), .ct--TableData_rowHoverEffect &:hover:has(> * > [data-row-active='true']) { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 3%, + --table-data-cell-bg: color-mix( in srgb, - rgb(from var(--color-control-bg-primary) r g b / 1) 15%, - var(--color-bg-default) - ) - ); + rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 3%, + color-mix( + in srgb, + rgb(from var(--color-control-bg-primary) r g b / 1) 15%, + var(--color-bg-default) + ) + ); } } } diff --git a/src/components/Table/TableResizers/TableResizers.css b/src/components/Table/TableResizers/TableResizers.css index c76b313..9704cc8 100644 --- a/src/components/Table/TableResizers/TableResizers.css +++ b/src/components/Table/TableResizers/TableResizers.css @@ -11,17 +11,19 @@ --inner-line-width: 1px; --fast-transition: 0.15s ease-out; --resizer-width: 4px; - --resizer-top: calc( - (var(--table-header-height) - var(--table-resizer-top-offset)) * -1 - ); + --resizer-top: + calc( + (var(--table-header-height) - var(--table-resizer-top-offset)) * -1 + ); position: absolute; top: var(--resizer-top); right: 0; width: var(--resizer-width); - height: calc( - var(--table-body-height) - var(--table-resizer-top-offset) - + height: + calc( + var(--table-body-height) - var(--table-resizer-top-offset) - var(--table-body-horizontal-scroll-height) - 2px - ); + ); background-color: var(--color-bg-ghost); opacity: 0; cursor: col-resize; @@ -45,10 +47,11 @@ opacity: 1; } } + &-VirtualScrollHelper { position: absolute; top: calc(var(--table-header-height) * -1); - pointer-events: none; width: 100%; + pointer-events: none; } } diff --git a/src/components/Table/TableRow/TableRow.css b/src/components/Table/TableRow/TableRow.css index 60c349a..080861c 100644 --- a/src/components/Table/TableRow/TableRow.css +++ b/src/components/Table/TableRow/TableRow.css @@ -6,6 +6,7 @@ &_left { grid-column: span var(--table-row-offset, 1); } + &_right { grid-column: span var(--table-row-offset, 1); } @@ -13,41 +14,45 @@ } &_zebraStriped { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-bg-stripe) r g b / 1) 5%, - var(--color-bg-default) - ); + --table-data-cell-bg: + color-mix( + in srgb, + rgb(from var(--color-bg-stripe) r g b / 1) 5%, + var(--color-bg-default) + ); } &:has(> * > [data-row-active='true']) { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-control-bg-primary) r g b / 1) 15%, - var(--color-bg-default) - ); + --table-data-cell-bg: + color-mix( + in srgb, + rgb(from var(--color-control-bg-primary) r g b / 1) 15%, + var(--color-bg-default) + ); } &:has(> * > [data-row-hover='true']), .ct--TableData_rowHoverEffect &:hover { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 9%, - var(--color-bg-default) - ); + --table-data-cell-bg: + color-mix( + in srgb, + rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 9%, + var(--color-bg-default) + ); } &:has(> * > [data-row-hover='true'][data-row-active='true']), .ct--TableData_rowHoverEffect &:hover:has(> * > [data-row-active='true']) { - --table-data-cell-bg: color-mix( - in srgb, - rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 3%, + --table-data-cell-bg: color-mix( in srgb, - rgb(from var(--color-control-bg-primary) r g b / 1) 15%, - var(--color-bg-default) - ) - ); + rgb(from var(--color-control-typo-ghost-hover) r g b / 1) 3%, + color-mix( + in srgb, + rgb(from var(--color-control-bg-primary) r g b / 1) 15%, + var(--color-bg-default) + ) + ); } &-Cell { diff --git a/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx b/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx index b9724ef..4db2856 100644 --- a/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx +++ b/src/components/Table/__stand__/examples/TableExampleSimple/TableExampleSimple.tsx @@ -1,8 +1,7 @@ import { Example } from '@consta/stand'; -import React, { useState } from 'react'; +import React from 'react'; -import { Table, TableColumn, TableRenderCell } from '##/components/Table'; -import { TextFieldCell } from '##/components/TextFieldCell'; +import { Table, TableColumn } from '##/components/Table'; type Row = { name: string; profession: string; status: string }; @@ -29,37 +28,17 @@ const rows: Row[] = [ }, ]; -const RenderCell: TableRenderCell = (row) => { - const [value, setValue] = useState([row.row.name]); - return ( - value?.join(', ')} - // rows={3} - // rows="auto" - size="s" - /> - ); -}; - const columns: TableColumn[] = [ { title: 'Имя', accessor: 'name', - renderCell: RenderCell, }, { title: 'Профессия', accessor: 'profession', - renderCell: RenderCell, }, { title: 'Статус', - renderCell: RenderCell, accessor: 'status', }, ]; diff --git a/src/components/TextFieldCell/TextFieldCell.css b/src/components/TextFieldCell/TextFieldCell.css index a7126a3..c45bb12 100644 --- a/src/components/TextFieldCell/TextFieldCell.css +++ b/src/components/TextFieldCell/TextFieldCell.css @@ -1,41 +1,46 @@ .ct--TextFieldCell { width: 100%; - min-height: 100%; + height: 100%; + min-height: var(--text-filed-cell-min-height); - &[data-cell-edit-mode='true'] { - --table-cell-edit-grid-column-index: var(--table-cell-grid-column-index); - --table-cell-edit-grid-row-index: var(--table-cell-grid-row-index); - } + &_size { + &_s { + --text-filed-cell-min-height: var(--space-3xl); + } - &-Field { - width: 100%; - flex: 1; - &_size { - &_s { - --text-filed-cell-vertical-margin: 0.5px; - } - &_m { - --text-filed-cell-vertical-margin: 3px; - } + &_m { + --text-filed-cell-min-height: var(--space-4xl); } - margin-top: var(--text-filed-cell-vertical-margin); - margin-bottom: var(--text-filed-cell-vertical-margin); - line-height: 1.5em; } - &_editMode { + &[data-cell-edit-mode='true'] { & .ct--DataCell-Slots { + overflow: hidden; flex: 1; } & .ct--DataCell-ContentSlot { + overflow: hidden; flex: 1; } } + &-Field { + flex: 1; + + /* flex-grow: strache; */ + align-items: stretch; + width: 100%; + margin-top: 0; + margin-bottom: 0; + line-height: 1.5em; + } + & .TextFieldTypeTextArea-TextArea { line-height: var(--line-height-text-m); } - /* box-shadow: 12px 12px 2px 1px rgb(0 0 255 / 0.2); */ + & .FieldControlLayout-Children { + width: var(--field-control-layout-children-width); + } } diff --git a/src/components/TextFieldCell/TextFieldCell.tsx b/src/components/TextFieldCell/TextFieldCell.tsx index 1b5e043..e9344c3 100644 --- a/src/components/TextFieldCell/TextFieldCell.tsx +++ b/src/components/TextFieldCell/TextFieldCell.tsx @@ -1,9 +1,6 @@ import './TextFieldCell.css'; import { useClickOutside } from '@consta/uikit/__internal__/src/hooks/useClickOutside'; -import { cnMixFlex } from '@consta/uikit/MixFlex'; -import { cnMixSpace } from '@consta/uikit/MixSpace'; -import { Text } from '@consta/uikit/Text'; import { TextField, TextFieldProps, @@ -11,6 +8,7 @@ import { } from '@consta/uikit/TextFieldCanary'; import { useFlag } from '@consta/uikit/useFlag'; import { useForkRef } from '@consta/uikit/useForkRef'; +import { useKeysRef } from '@consta/uikit/useKeysRef'; import React, { forwardRef, useEffect, useRef } from 'react'; import { DataCell } from '##/components/DataCell'; @@ -18,12 +16,31 @@ import { cn } from '##/utils/bem'; const cnTextFieldCell = cn('TextFieldCell'); -export type TextFieldCellProps = TextFieldProps & { +// readonly вид, не понятно какую ячейку можно редактировать а какую нет +// у textarea вертикальные отступы отличаются от input и Text, по этому происходят прыжки текста при смене режима "просмотр/редактирование" +// также у textarea меняется межстрочный интервал в зависимсоти от размера(возможно только в коде) +// замечены проблемы в textarea resize=auto, появляется полоска скролла, а она не нужна +// в textarea resize=auto сделать возможным менять высоту контейнера вручную, при этом будет отключен авто режим +// всегда ли поведение двойного нужно для компонента? или дать переключение режимов на откуп пользователям? + +export type TextFieldCellProps = Omit< + TextFieldProps, + 'view' | 'size' | 'form' | 'status' +> & { size?: 's' | 'm'; lineClamp?: number; readModeRender?: (value?: TextFieldPropValue) => React.ReactNode; + level?: number; + indicator?: 'alert' | 'warning'; + truncate?: boolean; }; +type SplitProps = TextFieldCellProps<'textarea'> & + TextFieldCellProps<'text'> & + TextFieldCellProps<'textarray'> & + TextFieldCellProps<'password'> & + TextFieldCellProps<'number'>; + const readModeRenderDefault = ( value?: TextFieldPropValue, ) => value; @@ -32,60 +49,163 @@ export type TextFieldCellComponent = ( props: TextFieldCellProps, ) => React.ReactNode | null; -const TextFieldCellRender = ( - props: TextFieldCellProps, - refRoot: React.Ref, +const TextFieldCellRender = ( + props: SplitProps, + ref: React.Ref, ) => { const { + className, size = 'm', lineClamp, type, value, readModeRender = readModeRenderDefault, + indicator, + level, + truncate, + defaultValue, + onChange, + id, + name, + disabled, + maxLength, + minLength, + onFocus, + onBlur, + placeholder, + leftSide, + rightSide, + clearButton, + iconClear, + autoComplete, + readOnly, + tabIndex, + ariaLabel, + iconSize, + onClear, + inputRef: inputRefProp, + onKeyUp, + onKeyUpCapture, + onKeyDown, + onKeyDownCapture, + onCopy, + onCopyCapture, + onCut, + onCutCapture, + onPaste, + onPasteCapture, + onWheel, + max, + min, + step, + incrementButtons, + iconShowPassword, + iconHidePassword, + resize, + minRows, + maxRows, + rows, + renderValueItem, + inputValue, + onInputChange, ...restProps } = props; + const refRoot = useRef(null); + const inputRef = useRef(null); + const inputRefForked = useForkRef([inputRef, inputRefProp]); + const [editMode, setEditMode] = useFlag(false); - const refTextField = useRef(null); - const ref = useForkRef([refTextField, refRoot]); - const inputRef = useRef(null); useClickOutside({ isActive: editMode, - ignoreClicksInsideRefs: [refTextField], + ignoreClicksInsideRefs: [refRoot], handler: setEditMode.off, }); - // useEffect(() => { - // if (editMode) { - // inputRef.current?.focus(); - // } - // }, [editMode]); + useKeysRef({ + isActive: editMode, + ref: refRoot, + keys: { + Escape: setEditMode.off, + }, + }); + + useEffect(() => { + if (editMode) { + inputRef.current?.focus(); + } + }, [editMode]); + + const textFiledProps = { + className: cnTextFieldCell('Field'), + view: 'clear', + onDoubleClick: setEditMode.on, + value, + type, + inputRef: inputRefForked, + defaultValue, + onChange, + id, + name, + disabled, + maxLength, + minLength, + onFocus, + onBlur, + placeholder, + leftSide, + rightSide, + clearButton, + iconClear, + autoComplete, + readOnly, + tabIndex, + ariaLabel, + iconSize, + onClear, + onKeyUp, + onKeyUpCapture, + onKeyDown, + onKeyDownCapture, + max, + min, + step, + incrementButtons, + iconShowPassword, + iconHidePassword, + resize, + minRows, + maxRows, + rows, + renderValueItem, + inputValue, + onInputChange, + onCopy, + onCopyCapture, + onCut, + onCutCapture, + onPaste, + onPasteCapture, + onWheel, + size, + } as const; - // добавить переключение режима по esc + // return ( - {editMode ? ( - - ) : ( - readModeRender(value) - )} + {editMode ? : readModeRender(value)} ); }; diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx new file mode 100644 index 0000000..d1c5f4b --- /dev/null +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx @@ -0,0 +1,9 @@ +import { TextFieldCellExampleTypes } from './examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes'; + +# Обзор + +TextFieldCell позволяет вводить текст, числа или массивы строк. Компонент базируется на [`TextField`](##LIBS.LIB.STAND/lib:uikit/stand:components-textfield-canary) и подготовлен для использования в таблицах. Основные свойства компонента идентичны [`TextField`](##LIBS.LIB.STAND/lib:uikit/stand:components-textfield-canary), на этой странице сосредоточимся на свойствах, которые специфичны для `TextFieldCell`. + +# Пример ячеек в таблице + + diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.stand.mdx b/src/components/TextFieldCell/__stand__/TextFieldCell.stand.mdx new file mode 100644 index 0000000..cb60a27 --- /dev/null +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.stand.mdx @@ -0,0 +1,3 @@ +```tsx +import { TextFieldCell } from '@consta/table/TextFieldCell'; +``` diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.stand.tsx b/src/components/TextFieldCell/__stand__/TextFieldCell.stand.tsx new file mode 100644 index 0000000..7aafb49 --- /dev/null +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.stand.tsx @@ -0,0 +1,12 @@ +import { createStand } from '##/stand/standConfig'; + +export default createStand({ + title: 'TextFieldCell', + id: 'TextFieldCell', + group: 'components', + description: 'Компонент текстового поля для таблицы', + version: '0.8.0', + status: 'stable', + alias: ['ячейка', 'input', 'текстовое поле'], + order: 10, +}); diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx new file mode 100644 index 0000000..a20720f --- /dev/null +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx @@ -0,0 +1,185 @@ +import { IconPhoto } from '@consta/icons/IconPhoto'; +import { useBoolean, useNumber, useSelect, useText } from '@consta/stand'; +import React, { useCallback, useState } from 'react'; + +import { Table, TableColumn, TableRenderCell } from '##/components/Table'; + +import { TextFieldCell } from '..'; + +type Row = { data: string }; + +const rows: Row[] = [{ data: 'Двойной клик' }]; + +const resizeMap = { + true: true, + false: false, + auto: 'auto', +} as const; + +const getStep = ( + type: string | undefined, + withStepArray: boolean, + step: number | undefined, +) => { + if (type !== 'number') { + return undefined; + } + + if (withStepArray) { + return [10, 50, 100]; + } + + return step; +}; + +const RenderCell: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.data); + const [valueArray, setValueArray] = useState([row.row.data]); + const [inputValue, setInputValue] = useState(null); + const onChangeValueArray = useCallback((value: string[] | null) => { + setValueArray(value); + setInputValue(null); + }, []); + + const type = useSelect( + 'type', + ['text', 'number', 'password', 'textarea', 'textarray'], + 'text', + ); + const resize = + useSelect( + 'resize', + ['false', 'true', 'auto'], + 'false', + type === 'textarea', + ) || 'false'; + + const size = useSelect('size', ['s', 'm'], 'm') || 'm'; + const placeholder = useText('placeholder', 'placeholder'); + + const rows = useNumber('rows', 3, type === 'textarea' && resize !== 'auto'); + const minRows = useNumber( + 'minRows', + 1, + type === 'textarea' && resize === 'auto', + ); + const maxRows = useNumber( + 'maxRows', + 5, + type === 'textarea' && resize === 'auto', + ); + + const step = useNumber('step', 1, type === 'number'); + const withStepArray = useBoolean('withStepArray', false, type === 'number'); + const incrementButtons = useBoolean( + 'incrementButtons', + true, + type === 'number', + ); + const min = useNumber('min', 0, type === 'number'); + const max = useNumber('max', 150, type === 'number'); + + const disabled = useBoolean('disabled', false); + const clearButton = useBoolean('clearButton', true); + const maxLength = useNumber('maxLength', 1000, type !== 'number'); + + const leftSideType = useSelect('leftSideType', ['icon', 'text']); + const leftSideText = useText('leftSideText', 'from'); + const rightSideType = useSelect('rightSideType', ['icon', 'text']); + const rightSideText = useText('rightSideText', 'm²'); + + const leftSideSelect = { + text: leftSideText, + icon: IconPhoto, + }; + + const rightSideSelect = { + text: rightSideText, + icon: IconPhoto, + }; + + const leftSide = leftSideType && leftSideSelect[leftSideType]; + const rightSide = rightSideType && rightSideSelect[rightSideType]; + + const props = { + size, + placeholder, + type, + disabled, + clearButton, + maxLength, + leftSide, + rightSide, + }; + + if (type === 'textarray') { + return ( + value?.join(', ')} + onInputChange={setInputValue} + inputValue={inputValue} + /> + ); + } + + if (type === 'textarea') { + return ( + + ); + } + + if (type === 'number') { + return ( + + ); + } + + return ( + + ); +}; + +const Variants = () => { + const columns: TableColumn[] = [ + { title: 'Изменяемые данные', accessor: 'data', renderCell: RenderCell }, + ]; + + return ( +
+ + + ); +}; + +export default Variants; diff --git a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx new file mode 100644 index 0000000..a5612b2 --- /dev/null +++ b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx @@ -0,0 +1,141 @@ +import { Example } from '@consta/stand'; +import React, { useCallback, useState } from 'react'; + +import { Table, TableColumn, TableRenderCell } from '##/components/Table'; +import { TextFieldCell } from '##/components/TextFieldCell'; + +type Row = { + text: string; + textarea: string; + textareaAutosize: string; + number: string; + textArray: string[]; +}; + +const rows: Row[] = [ + { + text: 'value1', + textarea: 'value2', + textareaAutosize: 'value3', + number: 'value4', + textArray: ['value5', 'value6'], + }, +]; + +const CellTypeText: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.text); + + return ( + + ); +}; + +const CellTypeTextArea: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textarea); + + return ( + + ); +}; + +const CellTypeTextAreaAutosize: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textareaAutosize); + + return ( + + ); +}; + +const CellTypeNumber: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.number); + + return ( + + ); +}; + +const CellTypeTextArray: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textArray); + + const [inputValue, setInputValue] = useState(null); + const onChangeValueArray = useCallback((value: string[] | null) => { + setValue(value); + setInputValue(null); + }, []); + + return ( + value?.join(', ')} + size="m" + /> + ); +}; + +const columns: TableColumn[] = [ + { + title: 'TextArray', + accessor: 'textArray', + renderCell: CellTypeTextArray, + minWidth: 200, + }, + { + title: 'Text', + accessor: 'text', + renderCell: CellTypeText, + minWidth: 200, + }, + + { + title: 'Textarea', + accessor: 'textarea', + renderCell: CellTypeTextArea, + minWidth: 200, + }, + { + title: 'Textarea autosize', + accessor: 'textareaAutosize', + renderCell: CellTypeTextAreaAutosize, + minWidth: 200, + }, + + { + title: 'Number', + accessor: 'number', + renderCell: CellTypeNumber, + minWidth: 200, + }, +]; + +export const TextFieldCellExampleTypes = () => ( + +
+ +); diff --git a/src/components/Toolbar/Toolbar.css b/src/components/Toolbar/Toolbar.css index bf5a2bb..22ec818 100644 --- a/src/components/Toolbar/Toolbar.css +++ b/src/components/Toolbar/Toolbar.css @@ -1,20 +1,23 @@ .ct--Toolbar { --bar-default-border-radius: calc(var(--control-radius) * 2); --bar-default-border: var(--control-border-width) solid var(--color-bg-border); - background: var(--color-bg-stripe); - /* border: var(--control-border-width) solid var(--color-bg-border); */ + /* border: var(--control-border-width) solid var(--color-bg-border); */ &_form { &_default { border-radius: var(--bar-default-border-radius); } + &_defaultBrick { - border-radius: var(--bar-default-border-radius) + border-radius: + var(--bar-default-border-radius) var(--bar-default-border-radius) 0 0; } + &_brickDefault { - border-radius: 0 0 var(--bar-default-border-radius) + border-radius: + 0 0 var(--bar-default-border-radius) var(--bar-default-border-radius); } } @@ -23,9 +26,11 @@ &_all { border: var(--bar-default-border); } + &_top { border-top: var(--bar-default-border); } + &_bottom { border-bottom: var(--bar-default-border); } diff --git a/src/components/Toolbar/ToolbarDivider/ToolbarDivider.css b/src/components/Toolbar/ToolbarDivider/ToolbarDivider.css index bfb2f4a..801bf0a 100644 --- a/src/components/Toolbar/ToolbarDivider/ToolbarDivider.css +++ b/src/components/Toolbar/ToolbarDivider/ToolbarDivider.css @@ -1,5 +1,5 @@ .ct--ToolbarDivider { - background-color: var(--color-bg-border); width: calc(var(--control-border-width) * 2); height: var(--toolbar-divider-height); + background-color: var(--color-bg-border); } From 4028ff3f2e2b694ea656cbd8276e7c4e2f248533 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Mon, 12 Jan 2026 11:02:43 +0300 Subject: [PATCH 03/11] Update TextFieldCell.tsx --- src/components/TextFieldCell/TextFieldCell.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextFieldCell/TextFieldCell.tsx b/src/components/TextFieldCell/TextFieldCell.tsx index e9344c3..67b2b5b 100644 --- a/src/components/TextFieldCell/TextFieldCell.tsx +++ b/src/components/TextFieldCell/TextFieldCell.tsx @@ -22,6 +22,7 @@ const cnTextFieldCell = cn('TextFieldCell'); // замечены проблемы в textarea resize=auto, появляется полоска скролла, а она не нужна // в textarea resize=auto сделать возможным менять высоту контейнера вручную, при этом будет отключен авто режим // всегда ли поведение двойного нужно для компонента? или дать переключение режимов на откуп пользователям? +// Уголок textarea resize=true не прибит к краю ячейки, возможно потребуется доработка text-field для возможности проброса дополнительных отступов. export type TextFieldCellProps = Omit< TextFieldProps, From 1096711ee5100e59d8bc4b3040e68f6f75aada08 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Mon, 12 Jan 2026 11:04:48 +0300 Subject: [PATCH 04/11] Update pre-push --- .husky/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index dd56e3a..ae06106 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -# yarn run pre-push \ No newline at end of file +yarn run pre-push \ No newline at end of file From b965acaad4c182b0ce3de80aedb073f77ad55a2e Mon Sep 17 00:00:00 2001 From: gizeasy Date: Fri, 23 Jan 2026 11:54:36 +0300 Subject: [PATCH 05/11] feat --- package.json | 4 +- src/components/DataCell/DataCell.css | 22 +- .../TextFieldCell/TextFieldCell.css | 38 +++- .../TextFieldCell/TextFieldCell.tsx | 10 - .../__stand__/TextFieldCell.dev.stand.mdx | 207 +++++++++++++++++- .../__stand__/TextFieldCell.variants.tsx | 11 +- .../TextFieldCellExampleTypes.tsx | 3 + 7 files changed, 262 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 1a3df51..a289b41 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ } ], "peerDependencies": { - "@consta/icons": "^1.5.0", - "@consta/uikit": "^5.28.5", + "@consta/icons": "^1.6.0", + "@consta/uikit": "^5.29.0", "@reatom/core": "3.10.1", "@reatom/npm-react": "3.10.6" }, diff --git a/src/components/DataCell/DataCell.css b/src/components/DataCell/DataCell.css index a55ebd4..0970ff6 100644 --- a/src/components/DataCell/DataCell.css +++ b/src/components/DataCell/DataCell.css @@ -1,18 +1,19 @@ /* --table-data-cell-level - задается в компоненте */ .ct--DataCell { - padding-right: var(--space-s); - padding-left: - calc( - var(--space-s) + (var(--table-data-cell-slot-width) + var(--space-2xs)) * + --table-data-cell-padding-right: var(--space-s); + --table-data-cell-padding-left: calc( + var(--space-s) + (var(--table-data-cell-slot-width) + var(--space-2xs)) * var(--table-data-cell-level, 0) + var(--table-data-cell-additional-space, calc(0 * 1px)) - ); + ); + + padding-right: var(--table-data-cell-padding-right); + padding-left: var(--table-data-cell-padding-left); &_alignmentIndent { - --table-data-cell-additional-space: - calc( - var(--table-data-cell-gap) - var(--space-2xs) - ); + --table-data-cell-additional-space: calc( + var(--table-data-cell-gap) - var(--space-2xs) + ); } &_size { @@ -39,8 +40,7 @@ right: 0; border: var(--space-2xs) solid transparent; border-top: var(--space-2xs) solid var(--table-data-cell-indicator-color); - border-right: - var(--space-2xs) solid + border-right: var(--space-2xs) solid var(--table-data-cell-indicator-color); } } diff --git a/src/components/TextFieldCell/TextFieldCell.css b/src/components/TextFieldCell/TextFieldCell.css index c45bb12..775f04b 100644 --- a/src/components/TextFieldCell/TextFieldCell.css +++ b/src/components/TextFieldCell/TextFieldCell.css @@ -2,6 +2,14 @@ width: 100%; height: 100%; min-height: var(--text-filed-cell-min-height); + padding-left: var( + --text-filed-cell-padding-left, + var(--table-data-cell-padding-left) + ); + padding-right: var( + --text-filed-cell-padding-right, + var(--table-data-cell-padding-right) + ); &_size { &_s { @@ -14,11 +22,18 @@ } &[data-cell-edit-mode='true'] { - & .ct--DataCell-Slots { - overflow: hidden; - flex: 1; + &.ct--DataCell { + --text-filed-cell-padding-left: calc( + var(--table-data-cell-padding-right) - var(--space-s) + ); + --text-filed-cell-padding-right: calc( + var(--table-data-cell-padding-right) - var(--space-s) + ); + --field-control-layout-additional-padding-left: var(--space-s); + --field-control-layout-additional-padding-right: var(--space-s); } + & .ct--DataCell-Slots, & .ct--DataCell-ContentSlot { overflow: hidden; flex: 1; @@ -27,13 +42,10 @@ &-Field { flex: 1; - - /* flex-grow: strache; */ align-items: stretch; width: 100%; margin-top: 0; margin-bottom: 0; - line-height: 1.5em; } & .TextFieldTypeTextArea-TextArea { @@ -43,4 +55,18 @@ & .FieldControlLayout-Children { width: var(--field-control-layout-children-width); } + + & .ct--DataCell-Slots:has(.TextFieldTypeTextArea_resize), + & .ct--DataCell-ContentSlot:has(.TextFieldTypeTextArea_resize), + & .TextFieldTypeTextArea_resize, + & .TextFieldTypeTextArea_resize .FieldControlLayout-Container, + & .TextFieldTypeTextArea_resize .FieldControlLayout-Children, + & .TextFieldTypeTextArea_resize .TextFieldTypeTextArea-TextArea { + min-height: 100%; + height: 100%; + } + + & .TextAreaAutoSize { + display: initial; + } } diff --git a/src/components/TextFieldCell/TextFieldCell.tsx b/src/components/TextFieldCell/TextFieldCell.tsx index 67b2b5b..3c4b78b 100644 --- a/src/components/TextFieldCell/TextFieldCell.tsx +++ b/src/components/TextFieldCell/TextFieldCell.tsx @@ -16,14 +16,6 @@ import { cn } from '##/utils/bem'; const cnTextFieldCell = cn('TextFieldCell'); -// readonly вид, не понятно какую ячейку можно редактировать а какую нет -// у textarea вертикальные отступы отличаются от input и Text, по этому происходят прыжки текста при смене режима "просмотр/редактирование" -// также у textarea меняется межстрочный интервал в зависимсоти от размера(возможно только в коде) -// замечены проблемы в textarea resize=auto, появляется полоска скролла, а она не нужна -// в textarea resize=auto сделать возможным менять высоту контейнера вручную, при этом будет отключен авто режим -// всегда ли поведение двойного нужно для компонента? или дать переключение режимов на откуп пользователям? -// Уголок textarea resize=true не прибит к краю ячейки, возможно потребуется доработка text-field для возможности проброса дополнительных отступов. - export type TextFieldCellProps = Omit< TextFieldProps, 'view' | 'size' | 'form' | 'status' @@ -191,8 +183,6 @@ const TextFieldCellRender = ( size, } as const; - // - return ( + +```tsx +import React, { useCallback, useState } from 'react'; + +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; +import { TextFieldCell } from '@consta/table/TextFieldCell'; + +type Row = { + text: string; + textarea: string; + textareaAutosize: string; + number: string; + textArray: string[]; +}; + +const rows: Row[] = [ + { + text: 'value1', + textarea: 'value2', + textareaAutosize: 'value3', + number: 'value4', + textArray: ['value5', 'value6'], + }, +]; + +const CellTypeText: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.text); + + return ( + + ); +}; + +const CellTypeTextArea: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textarea); + + return ( + + ); +}; + +const CellTypeTextAreaAutosize: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textareaAutosize); + + return ( + + ); +}; + +const CellTypeNumber: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.number); + + return ( + + ); +}; + +const CellTypeTextArray: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textArray); + + const [inputValue, setInputValue] = useState(null); + const onChangeValueArray = useCallback((value: string[] | null) => { + setValue(value); + setInputValue(null); + }, []); + + return ( + value?.join(', ')} + size="m" + /> + ); +}; + +const columns: TableColumn[] = [ + { + title: 'TextArray', + accessor: 'textArray', + renderCell: CellTypeTextArray, + minWidth: 200, + }, + { + title: 'Text', + accessor: 'text', + renderCell: CellTypeText, + minWidth: 200, + }, + + { + title: 'Textarea', + accessor: 'textarea', + renderCell: CellTypeTextArea, + minWidth: 200, + }, + { + title: 'Textarea autosize', + accessor: 'textareaAutosize', + renderCell: CellTypeTextAreaAutosize, + minWidth: 200, + }, + + { + title: 'Number', + accessor: 'number', + renderCell: CellTypeNumber, + minWidth: 200, + }, +]; + +export const TextFieldCellExampleTypes = () => ( +
+); +``` + + + +# Свойства + +| Свойство | Тип | По умолчанию | Описание | +| ---------------- | --------------------------------------------------------------- | ------------ | ------------------------------------------------ | +| size | `'s' \| 'm'` | `'m'` | Размер ячейки | +| lineClamp | `number` | - | Ограничение количества строк текста | +| readModeRender | `(value?: TextFieldPropValue) => React.ReactNode ` | - | Функция для отображения значения в режиме чтения | +| level | `number` | - | Уровень вложенности ячейки | +| indicator | `'alert' \| 'warning'` | - | Индикатор состояния | +| truncate | `boolean` | - | Обрезка текста многоточием | +| value | `TextFieldPropValue` | - | Значение поля ввода | +| onChange | `TextFieldPropOnChange` | - | Обработчик изменения значения | +| defaultValue | `TextFieldPropValue` | - | Значение по умолчанию | +| id | `string` | - | Идентификатор поля | +| name | `string` | - | Имя поля | +| disabled | `boolean` | - | Флаг отключения поля | +| maxLength | `number` | - | Максимальная длина текста | +| minLength | `number` | - | Минимальная длина текста | +| onFocus | `React.FocusEventHandler` | - | Обработчик фокуса | +| onBlur | `React.FocusEventHandler` | - | Обработчик потери фокуса | +| placeholder | `string` | - | Подсказка для ввода | +| leftSide | `React.ReactNode` | - | Элемент слева внутри поля | +| rightSide | `React.ReactNode` | - | Элемент справа внутри поля | +| clearButton | `boolean` | - | Флаг отображения кнопки очистки | +| iconClear | `IconComponent` | - | Иконка для кнопки очистки | +| autoComplete | `string` | - | Значение атрибута autocomplete | +| readOnly | `boolean` | - | Флаг только для чтения | +| tabIndex | `number` | - | Порядок получения фокуса | +| ariaLabel | `string` | - | Метка для доступности | +| iconSize | `'s' \| 'm'` | - | Размер иконок | +| onClear | `React.MouseEventHandler` | - | Обработчик очистки поля | +| inputRef | `React.Ref` | - | Ссылка на DOM-элемент input | +| type | `'text' \| 'textarea' \| 'textarray' \| 'password' \| 'number'` | `'text'` | Тип поля ввода | +| max | `number` | - | Максимальное значение | +| min | `number` | - | Минимальное значение | +| step | `number` | - | Шаг изменения значения | +| incrementButtons | `boolean` | - | Флаг отображения кнопок инкремента | +| iconShowPassword | `IconComponent` | - | Иконка показа пароля | +| iconHidePassword | `IconComponent` | - | Иконка скрытия пароля | +| resize | `boolean \| 'auto'` | - | Возможность изменения размера | +| minRows | `number` | - | Минимальное количество строк | +| maxRows | `number` | - | Максимальное количество строк | +| rows | `number` | - | Количество строк | +| renderValueItem | `(item: string) => React.ReactNode` | - | Функция отображения элемента массива | +| inputValue | `string \| null` | - | Значение поля ввода для массива | +| onInputChange | `((value: string \| null) => void)` | - | Обработчик изменения значения ввода | diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx index a20720f..bf392cf 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx @@ -41,6 +41,8 @@ const RenderCell: TableRenderCell = (row) => { setInputValue(null); }, []); + const size = useSelect('size', ['m', 's'], 'm'); + const type = useSelect( 'type', ['text', 'number', 'password', 'textarea', 'textarray'], @@ -54,10 +56,9 @@ const RenderCell: TableRenderCell = (row) => { type === 'textarea', ) || 'false'; - const size = useSelect('size', ['s', 'm'], 'm') || 'm'; const placeholder = useText('placeholder', 'placeholder'); - const rows = useNumber('rows', 3, type === 'textarea' && resize !== 'auto'); + const lineClamp = useNumber('lineClamp', 0); const minRows = useNumber( 'minRows', 1, @@ -87,6 +88,7 @@ const RenderCell: TableRenderCell = (row) => { const leftSideText = useText('leftSideText', 'from'); const rightSideType = useSelect('rightSideType', ['icon', 'text']); const rightSideText = useText('rightSideText', 'm²'); + const level = useNumber('level', 0); const leftSideSelect = { text: leftSideText, @@ -101,6 +103,8 @@ const RenderCell: TableRenderCell = (row) => { const leftSide = leftSideType && leftSideSelect[leftSideType]; const rightSide = rightSideType && rightSideSelect[rightSideType]; + const indicator = useSelect('indicator', ['alert', 'warning']); + const props = { size, placeholder, @@ -110,6 +114,9 @@ const RenderCell: TableRenderCell = (row) => { maxLength, leftSide, rightSide, + level, + lineClamp, + indicator, }; if (type === 'textarray') { diff --git a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx index a5612b2..8a205ee 100644 --- a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx +++ b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes.tsx @@ -40,6 +40,7 @@ const CellTypeTextArea: TableRenderCell = (row) => { onChange={setValue} size="m" rows={3} + lineClamp={6} resize clearButton /> @@ -54,6 +55,8 @@ const CellTypeTextAreaAutosize: TableRenderCell = (row) => { type="textarea" value={value} resize="auto" + maxRows={6} + lineClamp={6} onChange={setValue} size="m" clearButton From a424d3dc1a5e5e016cf473896af4da8005ee0d08 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Fri, 23 Jan 2026 14:31:14 +0300 Subject: [PATCH 06/11] Update TextFieldCell.dev.stand.mdx --- .../__stand__/TextFieldCell.dev.stand.mdx | 85 +++++++++---------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx index 661ff52..40f9da5 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx @@ -3,7 +3,7 @@ import { MdxTabs } from '@consta/stand'; # Обзор -`TextFieldCell` позволяет вводить текст, числа или массивы строк. Компонент базируется на [`TextField`](##LIBS.LIB.STAND/lib:uikit/stand:components-textfield-canary) и подготовлен для использования в таблицах. Основные свойства компонента идентичны [`TextField`](##LIBS.LIB.STAND/lib:uikit/stand:components-textfield-canary), на этой странице сосредоточимся на свойствах, которые специфичны для `TextFieldCell`. +`TextFieldCell` позволяет вводить текст, числа или массивы строк. Компонент базируется на [`TextField`](##LIBS.LIB.STAND/lib:uikit/stand:components-textfield-canary) и подготовлен для использования в таблицах. Основные свойства компонента идентичны [`TextField`](##LIBS.LIB.STAND/lib:uikit/stand:components-textfield-canary). # Тип @@ -166,47 +166,42 @@ export const TextFieldCellExampleTypes = () => ( # Свойства -| Свойство | Тип | По умолчанию | Описание | -| ---------------- | --------------------------------------------------------------- | ------------ | ------------------------------------------------ | -| size | `'s' \| 'm'` | `'m'` | Размер ячейки | -| lineClamp | `number` | - | Ограничение количества строк текста | -| readModeRender | `(value?: TextFieldPropValue) => React.ReactNode ` | - | Функция для отображения значения в режиме чтения | -| level | `number` | - | Уровень вложенности ячейки | -| indicator | `'alert' \| 'warning'` | - | Индикатор состояния | -| truncate | `boolean` | - | Обрезка текста многоточием | -| value | `TextFieldPropValue` | - | Значение поля ввода | -| onChange | `TextFieldPropOnChange` | - | Обработчик изменения значения | -| defaultValue | `TextFieldPropValue` | - | Значение по умолчанию | -| id | `string` | - | Идентификатор поля | -| name | `string` | - | Имя поля | -| disabled | `boolean` | - | Флаг отключения поля | -| maxLength | `number` | - | Максимальная длина текста | -| minLength | `number` | - | Минимальная длина текста | -| onFocus | `React.FocusEventHandler` | - | Обработчик фокуса | -| onBlur | `React.FocusEventHandler` | - | Обработчик потери фокуса | -| placeholder | `string` | - | Подсказка для ввода | -| leftSide | `React.ReactNode` | - | Элемент слева внутри поля | -| rightSide | `React.ReactNode` | - | Элемент справа внутри поля | -| clearButton | `boolean` | - | Флаг отображения кнопки очистки | -| iconClear | `IconComponent` | - | Иконка для кнопки очистки | -| autoComplete | `string` | - | Значение атрибута autocomplete | -| readOnly | `boolean` | - | Флаг только для чтения | -| tabIndex | `number` | - | Порядок получения фокуса | -| ariaLabel | `string` | - | Метка для доступности | -| iconSize | `'s' \| 'm'` | - | Размер иконок | -| onClear | `React.MouseEventHandler` | - | Обработчик очистки поля | -| inputRef | `React.Ref` | - | Ссылка на DOM-элемент input | -| type | `'text' \| 'textarea' \| 'textarray' \| 'password' \| 'number'` | `'text'` | Тип поля ввода | -| max | `number` | - | Максимальное значение | -| min | `number` | - | Минимальное значение | -| step | `number` | - | Шаг изменения значения | -| incrementButtons | `boolean` | - | Флаг отображения кнопок инкремента | -| iconShowPassword | `IconComponent` | - | Иконка показа пароля | -| iconHidePassword | `IconComponent` | - | Иконка скрытия пароля | -| resize | `boolean \| 'auto'` | - | Возможность изменения размера | -| minRows | `number` | - | Минимальное количество строк | -| maxRows | `number` | - | Максимальное количество строк | -| rows | `number` | - | Количество строк | -| renderValueItem | `(item: string) => React.ReactNode` | - | Функция отображения элемента массива | -| inputValue | `string \| null` | - | Значение поля ввода для массива | -| onInputChange | `((value: string \| null) => void)` | - | Обработчик изменения значения ввода | +| Свойство | Тип | По умолчанию | Описание | +| ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------ | ------------------------------------------------ | +| [`size?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:размер) | `'s' \| 'm'` | `'m'` | Размер ячейки | +| `lineClamp?` | `number` | - | Ограничение количества строк текста | +| `readModeRender?` | `(value?: TextFieldPropValue) => React.ReactNode` | - | Функция для отображения значения в режиме чтения | +| [`level?`](##LIBS.LIB.STAND.TAB/lib:table/stand:components-datacell-stable/tab:dev/hash:уровень-вложенности) | `number` | - | Уровень вложенности ячейки | +| [`indicator?`](##LIBS.LIB.STAND.TAB/lib:table/stand:components-datacell-stable/tab:dev/hash:индикатор) | `'alert' \| 'warning'` | - | Индикатор состояния | +| `truncate?` | `boolean` | - | Обрезка текста многоточием | +| [`value?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:значение) | `TextFieldPropValue` | - | Значение поля ввода | +| `onChange?` | `TextFieldPropOnChange` | - | Обработчик изменения значения | +| `defaultValue?` | `TextFieldPropValue` | - | Значение по умолчанию | +| `id?` | `string` | - | Идентификатор поля | +| `name?` | `string` | - | Имя поля | +| `disabled?` | `boolean` | - | Флаг отключения поля | +| `maxLength?` | `number` | - | Максимальная длина текста | +| `minLength?` | `number` | - | Минимальная длина текста | +| [`placeholder?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:подсказка) | `string` | - | Подсказка для ввода | +| [`leftSide?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:дополнительная-информация) | `React.ReactNode` | - | Элемент слева внутри поля | +| [`rightSide?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:дополнительная-информация) | `React.ReactNode` | - | Элемент справа внутри поля | +| [`clearButton?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:кнопка-для-очистки-поля) | `boolean` | - | Флаг отображения кнопки очистки | +| `iconClear?` | `IconComponent` | - | Иконка для кнопки очистки | +| `iconSize?` | `'s' \| 'm'` | - | Размер иконок | +| `onClear?` | `React.MouseEventHandler` | - | Обработчик очистки поля | +| `inputRef?` | `React.Ref` | - | Ссылка на DOM-элемент input | +| [`type?`](#тип) | `'text' \| 'textarea' \| 'textarray' \| 'password' \| 'number'` | `'text'` | Тип поля ввода | +| `max?` | `number` | - | Максимальное значение | +| `min?` | `number` | - | Минимальное значение | +| [`step?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:шаг--значения-и-диапазон) | `number` | - | Шаг изменения значения | +| `incrementButtons?` | `boolean` | - | Флаг отображения кнопок инкремента | +| `iconShowPassword?` | `IconComponent` | - | Иконка показа пароля | +| `iconHidePassword?` | `IconComponent` | - | Иконка скрытия пароля | +| [`resize?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:изменение-высоты) | `boolean \| 'auto'` | - | Возможность изменения размера | +| [`minRows?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:автоматическая-высота) | `number` | - | Минимальное количество строк | +| [`maxRows?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:автоматическая-высота) | `number` | - | Максимальное количество строк | +| [`rows?`](##LIBS.LIB.STAND.TAB/lib:uikit/stand:components-textfield-canary/tab:dev/hash:поле-в-несколько-строк) | `number` | - | Количество строк | +| `renderValueItem?` | `(item: string) => React.ReactNode` | - | Функция отображения элемента массива | +| `inputValue?` | `string \| null` | - | Значение поля ввода для массива | +| `onInputChange?` | `((value: string \| null) => void)` | - | Обработчик изменения значения ввода | +| `className?` | `string` | - | Дополнительный CSS-класс. | From 584e8685f987d6d6f9d7802b1ebc8d08d975f50b Mon Sep 17 00:00:00 2001 From: gizeasy Date: Tue, 27 Jan 2026 14:21:10 +0300 Subject: [PATCH 07/11] feat --- src/components/Table/TableCell/TableCell.css | 25 ++++++++++++------- .../TextFieldCell/TextFieldCell.tsx | 9 ++++--- .../__stand__/TextFieldCell.variants.tsx | 18 +++++++++++-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/components/Table/TableCell/TableCell.css b/src/components/Table/TableCell/TableCell.css index bedd16f..d9ba2aa 100644 --- a/src/components/Table/TableCell/TableCell.css +++ b/src/components/Table/TableCell/TableCell.css @@ -1,4 +1,5 @@ .ct--TableCell { + --table-cell-edit-mode-border-color: var(--color-control-bg-border-focus); --table-cell-border-top: 0; --table-cell-border-bottom: 0; --table-cell-border-left: 0; @@ -33,12 +34,11 @@ display: block; width: 100%; height: 100%; - background: - color-mix( - in srgb, - rgb(from var(--color-bg-stripe) r g b / 1) 5%, - var(--color-bg-default) - ); + background: color-mix( + in srgb, + rgb(from var(--color-bg-stripe) r g b / 1) 5%, + var(--color-bg-default) + ); } } @@ -51,8 +51,15 @@ } &:has([data-cell-edit-mode='true']) { - box-shadow: - 1px 1px 0 0 var(--color-control-bg-border-focus) inset, - -1px -1px 0 0 var(--color-control-bg-border-focus) inset; + box-shadow: 1px 1px 0 0 var(--table-cell-edit-mode-border-color) inset, + -1px -1px 0 0 var(--table-cell-edit-mode-border-color) inset; + } + + &:has([data-cell-status='alert']) { + --table-cell-edit-mode-border-color: var(--color-bg-alert); + } + + &:has([data-cell-status='warning']) { + --table-cell-edit-mode-border-color: var(--color-bg-warning); } } diff --git a/src/components/TextFieldCell/TextFieldCell.tsx b/src/components/TextFieldCell/TextFieldCell.tsx index 3c4b78b..0a3b748 100644 --- a/src/components/TextFieldCell/TextFieldCell.tsx +++ b/src/components/TextFieldCell/TextFieldCell.tsx @@ -24,8 +24,9 @@ export type TextFieldCellProps = Omit< lineClamp?: number; readModeRender?: (value?: TextFieldPropValue) => React.ReactNode; level?: number; - indicator?: 'alert' | 'warning'; truncate?: boolean; + status?: 'alert' | 'warning'; + indicator?: 'alert' | 'warning'; }; type SplitProps = TextFieldCellProps<'textarea'> & @@ -53,7 +54,6 @@ const TextFieldCellRender = ( type, value, readModeRender = readModeRenderDefault, - indicator, level, truncate, defaultValue, @@ -101,6 +101,8 @@ const TextFieldCellRender = ( renderValueItem, inputValue, onInputChange, + status, + indicator, ...restProps } = props; const refRoot = useRef(null); @@ -191,10 +193,11 @@ const TextFieldCellRender = ( onDoubleClick={setEditMode.on} size={size} lineClamp={lineClamp} - indicator={indicator} + indicator={!editMode ? indicator : undefined} level={level} truncate={truncate} data-cell-edit-mode={editMode} + data-cell-status={editMode ? status : undefined} > {editMode ? : readModeRender(value)} diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx index bf392cf..d1e7d8b 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx @@ -1,5 +1,8 @@ +import { IconAlert } from '@consta/icons/IconAlert'; import { IconPhoto } from '@consta/icons/IconPhoto'; import { useBoolean, useNumber, useSelect, useText } from '@consta/stand'; +import { Badge } from '@consta/uikit/Badge'; +import { Loader } from '@consta/uikit/Loader'; import React, { useCallback, useState } from 'react'; import { Table, TableColumn, TableRenderCell } from '##/components/Table'; @@ -89,6 +92,7 @@ const RenderCell: TableRenderCell = (row) => { const rightSideType = useSelect('rightSideType', ['icon', 'text']); const rightSideText = useText('rightSideText', 'm²'); const level = useNumber('level', 0); + const status = useSelect('status', ['alert', 'warning']); const leftSideSelect = { text: leftSideText, @@ -116,6 +120,7 @@ const RenderCell: TableRenderCell = (row) => { rightSide, level, lineClamp, + status, indicator, }; @@ -141,7 +146,6 @@ const RenderCell: TableRenderCell = (row) => { type={type} onChange={setValue} resize={resizeMap[resize]} - maxRows={resize === 'auto' ? maxRows : undefined} minRows={resize === 'auto' ? minRows : undefined} rows={resize !== 'auto' ? rows : undefined} /> @@ -164,7 +168,17 @@ const RenderCell: TableRenderCell = (row) => { } return ( - + [ + , + value, + , + ]} + type={type} + onChange={setValue} + /> ); }; From a4dabd4e60f412b1f1343825f5c940aa972b21a4 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Tue, 27 Jan 2026 15:18:13 +0300 Subject: [PATCH 08/11] feat --- .../TextFieldCell/TextFieldCell.tsx | 6 +- .../__stand__/TextFieldCell.dev.stand.mdx | 12 ++ .../__stand__/TextFieldCell.variants.tsx | 12 +- .../TextFieldCellExampleAlertMessage.tsx | 123 ++++++++++++++++++ 4 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx diff --git a/src/components/TextFieldCell/TextFieldCell.tsx b/src/components/TextFieldCell/TextFieldCell.tsx index 0a3b748..a4c5913 100644 --- a/src/components/TextFieldCell/TextFieldCell.tsx +++ b/src/components/TextFieldCell/TextFieldCell.tsx @@ -27,6 +27,7 @@ export type TextFieldCellProps = Omit< truncate?: boolean; status?: 'alert' | 'warning'; indicator?: 'alert' | 'warning'; + readonly?: boolean; }; type SplitProps = TextFieldCellProps<'textarea'> & @@ -103,13 +104,14 @@ const TextFieldCellRender = ( onInputChange, status, indicator, + readonly, ...restProps } = props; const refRoot = useRef(null); const inputRef = useRef(null); const inputRefForked = useForkRef([inputRef, inputRefProp]); - const [editMode, setEditMode] = useFlag(false); + const [editMode, setEditMode] = useFlag(); useClickOutside({ isActive: editMode, @@ -190,7 +192,7 @@ const TextFieldCellRender = ( {...restProps} className={cnTextFieldCell({ size }, [className])} ref={useForkRef([refRoot, ref])} - onDoubleClick={setEditMode.on} + onDoubleClick={readonly ? undefined : setEditMode.on} size={size} lineClamp={lineClamp} indicator={!editMode ? indicator : undefined} diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx index 40f9da5..686ccb5 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx @@ -1,4 +1,5 @@ import { TextFieldCellExampleTypes } from './examples/TextFieldCellExampleTypes/TextFieldCellExampleTypes'; +import { TextFieldCellExampleAlertMessage } from './examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage'; import { MdxTabs } from '@consta/stand'; # Обзор @@ -164,6 +165,17 @@ export const TextFieldCellExampleTypes = () => ( +# Обработка ошибок + +В компоненте есть 2 свойства для отображения ошибок. + +`indicator` - отображает уголок в правом верхнем углу ячейки, в режиме чтения. +`status` - отображает обводку поля в режиме редактирования. + +Для вывода ошибки можете использовать [`Popover`](##LIBS.LIB.STAND/lib:uikit/stand:components-popover-stable) и [`Informer`](##LIBS.LIB.STAND/lib:uikit/stand:components-informer-stable) + + + # Свойства | Свойство | Тип | По умолчанию | Описание | diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx index d1e7d8b..e07f7e6 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx @@ -168,17 +168,7 @@ const RenderCell: TableRenderCell = (row) => { } return ( - [ - , - value, - , - ]} - type={type} - onChange={setValue} - /> + ); }; diff --git a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx new file mode 100644 index 0000000..a26c4d1 --- /dev/null +++ b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx @@ -0,0 +1,123 @@ +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 { useClickOutside } from '@consta/uikit/useClickOutside'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Transition } from 'react-transition-group'; + +import { Table, TableColumn, TableRenderCell } from '##/components/Table'; +import { TextFieldCell } from '##/components/TextFieldCell'; + +type Row = { + col: string; + col2: string; + textArray: string[]; +}; + +const rows: Row[] = [ + { + col: 'value1', + textArray: ['value5', 'value6'], + col2: 'value2', + }, +]; + +const CellTypeTextArray: TableRenderCell = (row) => { + const [value, setValue] = useState(row.row.textArray); + const [open, setOpen] = useState(false); + + const [inputValue, setInputValue] = useState(null); + const onChangeValueArray = useCallback((value: string[] | null) => { + setValue(value); + setInputValue(null); + }, []); + + const error = !!(value && value.length > 10); + const popoverRef = useRef(null); + const cellRef = useRef(null); + + useEffect(() => { + if (error) { + setOpen(true); + } else { + setOpen(false); + } + }, [error]); + + useClickOutside({ + isActive: open, + handler: () => setOpen(false), + ignoreClicksInsideRefs: [popoverRef, cellRef], + }); + + return ( + <> + value?.join(', ')} + size="m" + status={error ? 'alert' : undefined} + indicator={error ? 'alert' : undefined} + ref={cellRef} + onMouseEnter={() => setOpen(true)} + /> + + + {(animate) => { + return ( + + + + ); + }} + + + ); +}; + +const columns: TableColumn[] = [ + { + title: 'Колонка', + accessor: 'col', + }, + { + title: 'Редактируемый массив', + accessor: 'textArray', + renderCell: CellTypeTextArray, + minWidth: 200, + }, + { + title: 'Колонка', + accessor: 'col2', + }, +]; + +export const TextFieldCellExampleAlertMessage = () => ( + +
+ +); From 07cad33827b7cc7e5f7a34eeb9f6ff01cb8b3971 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Wed, 28 Jan 2026 15:30:59 +0300 Subject: [PATCH 09/11] feat --- package.json | 2 +- .../TextFieldCellExampleAlertMessage.tsx | 17 +++- src/hooks/useHoverWithDelay/index.ts | 1 + .../useHoverWithDelay/useHoverWithDelay.ts | 88 +++++++++++++++++++ src/utils/object/clearUndefined.ts | 11 +++ src/utils/object/index.ts | 1 + 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/hooks/useHoverWithDelay/index.ts create mode 100644 src/hooks/useHoverWithDelay/useHoverWithDelay.ts create mode 100644 src/utils/object/clearUndefined.ts diff --git a/package.json b/package.json index a289b41..000da08 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ ], "peerDependencies": { "@consta/icons": "^1.6.0", - "@consta/uikit": "^5.29.0", + "@consta/uikit": "^5.29.2", "@reatom/core": "3.10.1", "@reatom/npm-react": "3.10.6" }, diff --git a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx index a26c4d1..65c6ee8 100644 --- a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx +++ b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx @@ -11,6 +11,7 @@ import { Transition } from 'react-transition-group'; import { Table, TableColumn, TableRenderCell } from '##/components/Table'; import { TextFieldCell } from '##/components/TextFieldCell'; +import { useHoverWithDelay } from '##/hooks/useHoverWithDelay'; type Row = { col: string; @@ -48,10 +49,18 @@ const CellTypeTextArray: TableRenderCell = (row) => { } }, [error]); - useClickOutside({ - isActive: open, - handler: () => setOpen(false), - ignoreClicksInsideRefs: [popoverRef, cellRef], + // useClickOutside({ + // isActive: open, + // handler: () => setOpen(false), + // ignoreClicksInsideRefs: [popoverRef, cellRef], + // }); + + useHoverWithDelay({ + isActive: true, + refs: [popoverRef, cellRef], + onHover: () => setOpen(true), + onBlur: () => setOpen(false), + delay: 1000, }); return ( diff --git a/src/hooks/useHoverWithDelay/index.ts b/src/hooks/useHoverWithDelay/index.ts new file mode 100644 index 0000000..9896c3f --- /dev/null +++ b/src/hooks/useHoverWithDelay/index.ts @@ -0,0 +1 @@ +export * from './useHoverWithDelay'; diff --git a/src/hooks/useHoverWithDelay/useHoverWithDelay.ts b/src/hooks/useHoverWithDelay/useHoverWithDelay.ts new file mode 100644 index 0000000..2c4469e --- /dev/null +++ b/src/hooks/useHoverWithDelay/useHoverWithDelay.ts @@ -0,0 +1,88 @@ +import { useMutableRef } from '@consta/uikit/useMutableRef'; +import { useEffect, useRef } from 'react'; + +type UseHoverWithDelayProps = { + isActive?: boolean | (() => boolean | undefined); + refs: Array>; + onHover?: () => void; + onBlur?: () => void; + delay?: number; +}; + +export const useHoverWithDelay = (props: UseHoverWithDelayProps): void => { + const { refs, onHover, onBlur, delay = 500, isActive } = props; + const timeoutId = useRef(null); + const isHovered = useRef(false); + const mutableRef = useMutableRef([onHover, onBlur, isActive, delay] as const); + + useEffect(() => { + const handleMouseEnter = () => { + if ( + !(typeof mutableRef.current[2] === 'function' + ? mutableRef.current[2]() + : mutableRef.current) + ) { + return; + } + + console.log('handleMouseEnter'); + + if (timeoutId.current) { + clearTimeout(timeoutId.current); + timeoutId.current = null; + } + + if (!isHovered.current) { + isHovered.current = true; + if (mutableRef.current[0]) { + mutableRef.current[0](); + } + } + }; + + const handleMouseLeave = () => { + if ( + !(typeof mutableRef.current[2] === 'function' + ? mutableRef.current[2]() + : mutableRef.current) + ) { + return; + } + + console.log('handleMouseLeave'); + + if (timeoutId.current) { + clearTimeout(timeoutId.current); + } + + timeoutId.current = setTimeout(() => { + if (isHovered.current) { + isHovered.current = false; + if (mutableRef.current[1]) { + mutableRef.current[1](); + } + } + timeoutId.current = null; + }, mutableRef.current[3]) as unknown as NodeJS.Timeout; + }; + + for (let i = 0; i < refs.length; i++) { + const element = refs[i].current; + element?.addEventListener('mouseenter', handleMouseEnter); + element?.addEventListener('mouseleave', handleMouseLeave); + } + + return () => { + for (let i = 0; i < refs.length; i++) { + const element = refs[i].current; + element?.removeEventListener('mouseenter', handleMouseEnter); + element?.removeEventListener('mouseleave', handleMouseLeave); + } + + if (timeoutId.current) { + clearTimeout(timeoutId.current); + timeoutId.current = null; + } + }; + }, [refs]); +}; diff --git a/src/utils/object/clearUndefined.ts b/src/utils/object/clearUndefined.ts new file mode 100644 index 0000000..006f474 --- /dev/null +++ b/src/utils/object/clearUndefined.ts @@ -0,0 +1,11 @@ +export const clearUndefined = >( + obj: T, +): T => { + const result = { ...obj }; + for (const key in result) { + if (result[key] === undefined) { + delete result[key]; + } + } + return result as T; +}; diff --git a/src/utils/object/index.ts b/src/utils/object/index.ts index 4fc8966..24653bc 100644 --- a/src/utils/object/index.ts +++ b/src/utils/object/index.ts @@ -1,2 +1,3 @@ export * from './isEmpty'; export * from './isEq'; +export * from './clearUndefined'; From 31fad4e2468a990fe52738a4a24290f38bc3cb1c Mon Sep 17 00:00:00 2001 From: gizeasy Date: Fri, 30 Jan 2026 08:29:44 +0300 Subject: [PATCH 10/11] fix --- package.json | 2 +- .../__stand__/TextFieldCell.dev.stand.mdx | 148 ++++++++++++++++++ .../__stand__/TextFieldCell.variants.tsx | 4 +- .../TextFieldCellExampleAlertMessage.tsx | 71 +++++---- src/hooks/useHoverWithDelay/index.ts | 1 - .../useHoverWithDelay/useHoverWithDelay.ts | 88 ----------- 6 files changed, 189 insertions(+), 125 deletions(-) delete mode 100644 src/hooks/useHoverWithDelay/index.ts delete mode 100644 src/hooks/useHoverWithDelay/useHoverWithDelay.ts diff --git a/package.json b/package.json index 000da08..cd98d22 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ ], "peerDependencies": { "@consta/icons": "^1.6.0", - "@consta/uikit": "^5.29.2", + "@consta/uikit": "^5.30.0", "@reatom/core": "3.10.1", "@reatom/npm-react": "3.10.6" }, diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx index 686ccb5..c57ac3c 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx @@ -174,8 +174,156 @@ export const TextFieldCellExampleTypes = () => ( Для вывода ошибки можете использовать [`Popover`](##LIBS.LIB.STAND/lib:uikit/stand:components-popover-stable) и [`Informer`](##LIBS.LIB.STAND/lib:uikit/stand:components-informer-stable) +В примере снизу ошибка возникает если количество элементов в массиве больше 10. + + + +```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 { useClickOutside } from '@consta/uikit/useClickOutside'; +import { useFlag } from '@consta/uikit/useFlag'; +import { useHover } from '@consta/uikit/useHover'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Transition } from 'react-transition-group'; + +import { Table, TableColumn, TableRenderCell } from '##/components/Table'; +import { TextFieldCell } from '##/components/TextFieldCell'; + +type Row = { + col: string; + col2: string; + textArray: string[]; +}; + +const rows: Row[] = [ + { + col: 'value1', + textArray: Array.from({ length: 11 }, (_, i) => `элемент-${i + 1}`), + col2: 'value2', + }, +]; + +const CellTypeTextArray: TableRenderCell = (row) => { + const popoverRef = useRef(null); + const cellRef = useRef(null); + const inputRef = useRef(null); + + const [value, setValue] = useState(row.row.textArray); + const [open, setOpen] = useFlag(); + + const onChangeValue = useCallback((value: string[] | null) => { + setValue(value); + if (inputRef.current) { + inputRef.current.value = ''; + } + }, []); + + const error = !!(value && value.length > 10); + + useEffect(() => setOpen.set(error), [error]); + + useHover({ + isActive: error, + refs: [popoverRef, cellRef], + onHover: setOpen.on, + onBlur: setOpen.off, + hoverDelay: 500, + blurDelay: 300, + }); + + useClickOutside({ + isActive: open, + handler: setOpen.off, + ignoreClicksInsideRefs: [popoverRef, cellRef], + }); + + return ( + <> + value?.join(', ')} + size="m" + status={error ? 'alert' : undefined} + indicator={error ? 'alert' : undefined} + ref={cellRef} + inputRef={inputRef} + /> + + + {(animate) => { + return ( + + + + ); + }} + + + ); +}; + +const columns: TableColumn[] = [ + { + title: 'Колонка', + accessor: 'col', + minWidth: 150, + }, + { + title: 'Редактируемый массив', + accessor: 'textArray', + renderCell: CellTypeTextArray, + minWidth: 200, + }, + { + title: 'Колонка', + accessor: 'col2', + minWidth: 150, + }, +]; + +export const TextFieldCellExampleAlertMessage = () => ( + +
+ +); +``` + + + # Свойства | Свойство | Тип | По умолчанию | Описание | diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx index e07f7e6..d2df6e8 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.variants.tsx @@ -1,8 +1,5 @@ -import { IconAlert } from '@consta/icons/IconAlert'; import { IconPhoto } from '@consta/icons/IconPhoto'; import { useBoolean, useNumber, useSelect, useText } from '@consta/stand'; -import { Badge } from '@consta/uikit/Badge'; -import { Loader } from '@consta/uikit/Loader'; import React, { useCallback, useState } from 'react'; import { Table, TableColumn, TableRenderCell } from '##/components/Table'; @@ -147,6 +144,7 @@ const RenderCell: TableRenderCell = (row) => { onChange={setValue} resize={resizeMap[resize]} minRows={resize === 'auto' ? minRows : undefined} + maxRows={resize === 'auto' ? maxRows : undefined} rows={resize !== 'auto' ? rows : undefined} /> ); diff --git a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx index 65c6ee8..7f24628 100644 --- a/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx +++ b/src/components/TextFieldCell/__stand__/examples/TextFieldCellExampleAlertMessage/TextFieldCellExampleAlertMessage.tsx @@ -6,12 +6,13 @@ import { } from '@consta/uikit/MixPopoverAnimate'; import { Popover } from '@consta/uikit/Popover'; import { useClickOutside } from '@consta/uikit/useClickOutside'; +import { useFlag } from '@consta/uikit/useFlag'; +import { useHover } from '@consta/uikit/useHover'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Transition } from 'react-transition-group'; import { Table, TableColumn, TableRenderCell } from '##/components/Table'; import { TextFieldCell } from '##/components/TextFieldCell'; -import { useHoverWithDelay } from '##/hooks/useHoverWithDelay'; type Row = { col: string; @@ -22,45 +23,43 @@ type Row = { const rows: Row[] = [ { col: 'value1', - textArray: ['value5', 'value6'], + textArray: Array.from({ length: 11 }, (_, i) => `элемент-${i + 1}`), col2: 'value2', }, ]; const CellTypeTextArray: TableRenderCell = (row) => { + const popoverRef = useRef(null); + const cellRef = useRef(null); + const inputRef = useRef(null); + const [value, setValue] = useState(row.row.textArray); - const [open, setOpen] = useState(false); + const [open, setOpen] = useFlag(); - const [inputValue, setInputValue] = useState(null); - const onChangeValueArray = useCallback((value: string[] | null) => { + const onChangeValue = useCallback((value: string[] | null) => { setValue(value); - setInputValue(null); + if (inputRef.current) { + inputRef.current.value = ''; + } }, []); const error = !!(value && value.length > 10); - const popoverRef = useRef(null); - const cellRef = useRef(null); - - useEffect(() => { - if (error) { - setOpen(true); - } else { - setOpen(false); - } - }, [error]); - // useClickOutside({ - // isActive: open, - // handler: () => setOpen(false), - // ignoreClicksInsideRefs: [popoverRef, cellRef], - // }); + useEffect(() => setOpen.set(error), [error]); - useHoverWithDelay({ - isActive: true, + useHover({ + isActive: error, refs: [popoverRef, cellRef], - onHover: () => setOpen(true), - onBlur: () => setOpen(false), - delay: 1000, + onHover: setOpen.on, + onBlur: setOpen.off, + hoverDelay: 500, + blurDelay: 300, + }); + + useClickOutside({ + isActive: open, + handler: setOpen.off, + ignoreClicksInsideRefs: [popoverRef, cellRef], }); return ( @@ -69,19 +68,17 @@ const CellTypeTextArray: TableRenderCell = (row) => { type="textarray" value={value} clearButton - onChange={onChangeValueArray} - inputValue={inputValue} - onInputChange={setInputValue} + onChange={onChangeValue} readModeRender={(value) => value?.join(', ')} size="m" status={error ? 'alert' : undefined} indicator={error ? 'alert' : undefined} ref={cellRef} - onMouseEnter={() => setOpen(true)} + inputRef={inputRef} /> = (row) => { {(animate) => { return ( [] = [ { title: 'Колонка', accessor: 'col', + minWidth: 150, }, { title: 'Редактируемый массив', @@ -122,6 +128,7 @@ const columns: TableColumn[] = [ { title: 'Колонка', accessor: 'col2', + minWidth: 150, }, ]; diff --git a/src/hooks/useHoverWithDelay/index.ts b/src/hooks/useHoverWithDelay/index.ts deleted file mode 100644 index 9896c3f..0000000 --- a/src/hooks/useHoverWithDelay/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useHoverWithDelay'; diff --git a/src/hooks/useHoverWithDelay/useHoverWithDelay.ts b/src/hooks/useHoverWithDelay/useHoverWithDelay.ts deleted file mode 100644 index 2c4469e..0000000 --- a/src/hooks/useHoverWithDelay/useHoverWithDelay.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useMutableRef } from '@consta/uikit/useMutableRef'; -import { useEffect, useRef } from 'react'; - -type UseHoverWithDelayProps = { - isActive?: boolean | (() => boolean | undefined); - refs: Array>; - onHover?: () => void; - onBlur?: () => void; - delay?: number; -}; - -export const useHoverWithDelay = (props: UseHoverWithDelayProps): void => { - const { refs, onHover, onBlur, delay = 500, isActive } = props; - const timeoutId = useRef(null); - const isHovered = useRef(false); - const mutableRef = useMutableRef([onHover, onBlur, isActive, delay] as const); - - useEffect(() => { - const handleMouseEnter = () => { - if ( - !(typeof mutableRef.current[2] === 'function' - ? mutableRef.current[2]() - : mutableRef.current) - ) { - return; - } - - console.log('handleMouseEnter'); - - if (timeoutId.current) { - clearTimeout(timeoutId.current); - timeoutId.current = null; - } - - if (!isHovered.current) { - isHovered.current = true; - if (mutableRef.current[0]) { - mutableRef.current[0](); - } - } - }; - - const handleMouseLeave = () => { - if ( - !(typeof mutableRef.current[2] === 'function' - ? mutableRef.current[2]() - : mutableRef.current) - ) { - return; - } - - console.log('handleMouseLeave'); - - if (timeoutId.current) { - clearTimeout(timeoutId.current); - } - - timeoutId.current = setTimeout(() => { - if (isHovered.current) { - isHovered.current = false; - if (mutableRef.current[1]) { - mutableRef.current[1](); - } - } - timeoutId.current = null; - }, mutableRef.current[3]) as unknown as NodeJS.Timeout; - }; - - for (let i = 0; i < refs.length; i++) { - const element = refs[i].current; - element?.addEventListener('mouseenter', handleMouseEnter); - element?.addEventListener('mouseleave', handleMouseLeave); - } - - return () => { - for (let i = 0; i < refs.length; i++) { - const element = refs[i].current; - element?.removeEventListener('mouseenter', handleMouseEnter); - element?.removeEventListener('mouseleave', handleMouseLeave); - } - - if (timeoutId.current) { - clearTimeout(timeoutId.current); - timeoutId.current = null; - } - }; - }, [refs]); -}; From 9f8be498d8be4ba8ccb9875fe0b002acba71480e Mon Sep 17 00:00:00 2001 From: gizeasy Date: Fri, 30 Jan 2026 08:46:32 +0300 Subject: [PATCH 11/11] Update TextFieldCell.dev.stand.mdx --- .../TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx index c57ac3c..0effdf7 100644 --- a/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx +++ b/src/components/TextFieldCell/__stand__/TextFieldCell.dev.stand.mdx @@ -194,8 +194,8 @@ import { useHover } from '@consta/uikit/useHover'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Transition } from 'react-transition-group'; -import { Table, TableColumn, TableRenderCell } from '##/components/Table'; -import { TextFieldCell } from '##/components/TextFieldCell'; +import { Table, TableColumn, TableRenderCell } from '@consta/table/Table'; +import { TextFieldCell } from '@consta/table/TextFieldCell'; type Row = { col: string;