diff --git a/libs/react/ui-core/src/index.ts b/libs/react/ui-core/src/index.ts index 8e19d37..a10e0bd 100644 --- a/libs/react/ui-core/src/index.ts +++ b/libs/react/ui-core/src/index.ts @@ -1,3 +1,4 @@ +export * from './lib/table/table'; export * from './lib/markdown/markdown'; export * from './lib/spinner/spinner'; export * from './lib/steps/steps'; diff --git a/libs/react/ui-core/src/lib/dropdown/dropdown.stories.tsx b/libs/react/ui-core/src/lib/dropdown/dropdown.stories.tsx index dcc20c0..ecae310 100644 --- a/libs/react/ui-core/src/lib/dropdown/dropdown.stories.tsx +++ b/libs/react/ui-core/src/lib/dropdown/dropdown.stories.tsx @@ -31,5 +31,5 @@ Primary.args = { {label: 'Вариант 04', key: 'key 4'}, ], icon: , - errorText: 'Ошибка!!!' + errorText: 'Ошибка!!!', }; diff --git a/libs/react/ui-core/src/lib/dropdown/useDropdown.tsx b/libs/react/ui-core/src/lib/dropdown/useDropdown.tsx index f5909a2..8476bc2 100644 --- a/libs/react/ui-core/src/lib/dropdown/useDropdown.tsx +++ b/libs/react/ui-core/src/lib/dropdown/useDropdown.tsx @@ -15,13 +15,19 @@ export function useDropdown(props:DropdownProps):useDropdownProps { props.defaultSelectedKey && props.items.find((item) => item.key === props.defaultSelectedKey)?.label || null) + useEffect(() => { + if(props.defaultSelectedKey) { + setActiveItemKey(props.defaultSelectedKey) + } + }, [props.defaultSelectedKey]) + useEffect(() => { document.addEventListener('mousedown', handleClickOutside) return () => { document.removeEventListener('mousedown', handleClickOutside) } - }) + }, []) const handleOpen = useCallback((isOpen: boolean) => { if(!props.disabled) { diff --git a/libs/react/ui-core/src/lib/table/Body/tableBody.tsx b/libs/react/ui-core/src/lib/table/Body/tableBody.tsx new file mode 100644 index 0000000..a7a9d28 --- /dev/null +++ b/libs/react/ui-core/src/lib/table/Body/tableBody.tsx @@ -0,0 +1,41 @@ +import React, {FC} from 'react' +import styles from '../table.module.scss' +import {ColumnsType} from '../TableProps' + +const TableBody:FC> = React.memo(({data, columns}) => { + return ( + + { + (data.map((item, index) => { + return ( + + {columns.map((column, tdIndex) => { + let attributes + if(column.onCell) { + attributes = column.onCell(item, index) + } + if((attributes?.colSpan === 0 || column.colSpan === 0) || (attributes?.rowSpan === 0 || column.rowSpan === 0)) return null + return ( + + { + column.render + ? column.render(item[column.dataIndex], index) // index - row number + : item[column.dataIndex] + } + + ) + })} + + ) + })) + } + + ) +}) + +export default TableBody + +type MyProps = { + data: RecordType[] + columns: ColumnsType[] +} diff --git a/libs/react/ui-core/src/lib/table/Footer/tableFooter.tsx b/libs/react/ui-core/src/lib/table/Footer/tableFooter.tsx new file mode 100644 index 0000000..f4dc7c3 --- /dev/null +++ b/libs/react/ui-core/src/lib/table/Footer/tableFooter.tsx @@ -0,0 +1,49 @@ +import React, {FC} from 'react' +import styles from '../table.module.scss' +import {ColumnsType} from '../TableProps' + +const TableFooter:FC> = React.memo(({footer, columns, dataLength}) => { + + return ( + <> + { + footer && + (!React.isValidElement(footer) + ? + + + {columns.map((column, index) => { + let attributes + if(column.onCell) { + attributes = column.onCell(footer, dataLength) // footer row number + } + + if((attributes?.colSpan === 0 || column.colSpan === 0) || (attributes?.rowSpan === 0 || column.rowSpan === 0)) return null + return ( + + {footer[column.dataIndex]} + + ) + })} + + + : + + + + {footer} + + + ) + } + + ) +}) + +export default TableFooter + +type MyProps = { + footer?: RecordType | React.ReactNode + columns: ColumnsType[] + dataLength: number +} diff --git a/libs/react/ui-core/src/lib/table/Header/tableHeader.tsx b/libs/react/ui-core/src/lib/table/Header/tableHeader.tsx new file mode 100644 index 0000000..d1a447e --- /dev/null +++ b/libs/react/ui-core/src/lib/table/Header/tableHeader.tsx @@ -0,0 +1,37 @@ +import React, {FC} from 'react' +import styles from '../table.module.scss' +import {ColumnsType, TableSortType} from '../TableProps' +import Icon from '../../icon/icon' +import {useTableHeader} from './useTableHeader' + +const TableHeader:FC> = React.memo(({columns, sortValue, sortType, sortTable}) => { + const {getIconClasses} = useTableHeader({columns, sortValue, sortType, sortTable}) + return ( + + + {columns.map((item) => { + if(item.colSpan === 0 || item.rowSpan === 0) return null + return ( + + + {item.title} + { + item.sorter && item.sorter && sortTable(item.dataIndex, sortType)} className={getIconClasses(item.dataIndex)} name={'ri-arrow-down-s-fill'} size={18} type={'fill'} /> + } + + + ) + })} + + + ) +}) + +export default TableHeader + +export type TableHeaderProps = { + columns: ColumnsType[] + sortValue: string + sortType: TableSortType + sortTable: (value: string, type: TableSortType) => void +} diff --git a/libs/react/ui-core/src/lib/table/Header/useTableHeader.ts b/libs/react/ui-core/src/lib/table/Header/useTableHeader.ts new file mode 100644 index 0000000..6bd4d3f --- /dev/null +++ b/libs/react/ui-core/src/lib/table/Header/useTableHeader.ts @@ -0,0 +1,18 @@ +import {useCallback} from 'react' +import styles from '../table.module.scss' +import {TableHeaderProps} from './tableHeader' +import {getClasses} from '../../../utils/getClasses' + +export const useTableHeader = (props: TableHeaderProps) => { + + const getIconClasses = useCallback((dataIndex: string) => { + const conditions:{[index: string]:boolean} = { + "table-head-sort": true, + "table-head-sort-active": props.sortValue === dataIndex && props.sortType !== '', + "table-head-sort-ascending": props.sortValue === dataIndex && props.sortType === 'ascending', + }; + return getClasses(conditions, styles) + }, [props]); + + return {getIconClasses} +} diff --git a/libs/react/ui-core/src/lib/table/TableProps.d.ts b/libs/react/ui-core/src/lib/table/TableProps.d.ts new file mode 100644 index 0000000..a08258c --- /dev/null +++ b/libs/react/ui-core/src/lib/table/TableProps.d.ts @@ -0,0 +1,62 @@ +import {DefaultParams} from '../../default-types/defaultParams' +import React from 'react' +import {ClickableObjectMini} from '../../default-types/ClickableObjectMini' + +export interface TableProps extends DefaultParams, ClickableObjectMini{ + + /** ColumnsType<{DataType}> {
+ *    title: string
+ *    key: React.Key
+ *    dataIndex: string
+ *    colSpan?: number
+ *    rowSpan?: number
+ *    width?: number
+ *    render?: (value: DataType, index: number) => React.ReactNode
+ *    sorter?: boolean
+ *    onCell?: GetComponentProps
+ * } + * + * */ + columns: ColumnsType[] + + /** Data of table body + * + * DataType[] - Array of column's dataIndexes + * + * */ + data: RecordType[] + + /** Data of table footer + * + * DataType - Last object of column's dataIndexes or ReactNode + * + * */ + footer?: RecordType | React.ReactNode + + /** Table layout prop */ + tableLayout?: TableLayout +} + +export interface ColumnsType { + title: string + key: React.Key + dataIndex: string + colSpan?: number + rowSpan?: number + width?: number + sorter?: boolean + render?: ( + value: RecordType, + index: number, + ) => React.ReactNode; + onCell?: GetComponentProps +} + +export type GetComponentProps = ( + data: DataType, + index: number, +) => React.TdHTMLAttributes; + +export type TableLayout = 'auto' | 'fixed'; + +export type TableSortType = 'descending' | 'ascending' | '' diff --git a/libs/react/ui-core/src/lib/table/table.module.scss b/libs/react/ui-core/src/lib/table/table.module.scss new file mode 100644 index 0000000..31deeae --- /dev/null +++ b/libs/react/ui-core/src/lib/table/table.module.scss @@ -0,0 +1 @@ +@use '../../../../../../styles/components/table' as *; diff --git a/libs/react/ui-core/src/lib/table/table.spec.tsx b/libs/react/ui-core/src/lib/table/table.spec.tsx new file mode 100644 index 0000000..012d4d0 --- /dev/null +++ b/libs/react/ui-core/src/lib/table/table.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { Table } from './table'; + +describe('Table', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/libs/react/ui-core/src/lib/table/table.stories.tsx b/libs/react/ui-core/src/lib/table/table.stories.tsx new file mode 100644 index 0000000..fc72085 --- /dev/null +++ b/libs/react/ui-core/src/lib/table/table.stories.tsx @@ -0,0 +1,780 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { Table } from './table'; +import React from 'react' +import {Button} from '../button/button' +import {Badge} from '../badge/badge' +import {Checkbox} from '../checkbox/checkbox' + +export default { + component: Table, + title: 'Table', + parameters: { + jsx: { + showFunctions: true, + } + }, + argTypes: { + tableLayout: { defaultValue: 'auto' }, + className: {control: {type: 'text'}}, + } +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + + return ( +
+ ) +}; + +export const Primary = Template.bind({}); +Primary.args = { + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 1, + render: (value, index) =>
+ + + +
+ ); +}) + +Table.defaultProps = { tableLayout: 'auto' }; + + diff --git a/libs/react/ui-core/src/lib/table/useTable.ts b/libs/react/ui-core/src/lib/table/useTable.ts new file mode 100644 index 0000000..a570d7d --- /dev/null +++ b/libs/react/ui-core/src/lib/table/useTable.ts @@ -0,0 +1,68 @@ +import {useCallback, useEffect, useMemo, useState} from 'react' +import {getClasses} from '../../utils/getClasses' +import styles from './table.module.scss' +import {TableProps, TableSortType} from './TableProps' + +export const useTable = (props: TableProps) => { + const [data, setData] = useState(props.data) + const [sortValue, setSortValue] = useState('') + const [sortType, setSortType] = useState('') + + const sortData = useCallback((value: string, type: string) => { + if (type === 'ascending') { + return (props.data.map((item) => item)).sort((a, b) => a[value] > b[value] ? 1 : -1) + } + else if (type === 'descending') { + return (props.data.map((item) => item)).sort((a, b) => a[value] <= b[value] ? 1 : -1) + } + else return props.data + }, [props.data]) + + const sortTable = useCallback(async (value: string, type: string) => { + if(value === sortValue) { + if(type === '') { + setSortType('ascending') + const sortedData = await sortData(value, 'ascending') + setData(sortedData) + } + else if (type === 'ascending') { + setSortType('descending') + const sortedData = await sortData(value, 'descending') + setData(sortedData) + } + else if (type === 'descending'){ + setSortType('') + setData(props.data) + } + } + else { + setSortValue(value) + setSortType('ascending') + setData(sortData(value, 'ascending')) + } + return (props.data.map((item) => item)).sort((a, b) => a[value] > b[value] ? 1 : -1) + }, [props.data, sortValue, sortData]) + + useEffect(() => { + const setInitialData = async () => { + if(sortValue !== '') { + const sortedData = await sortData(sortValue, sortType) + setData(sortedData) + } + else { + setData(props.data) + } + } + setInitialData() + }, [props.data]) + + const classes = useMemo(() => { + const conditions:{[index: string]:boolean} = { + "table": true, + "table-fixed": props.tableLayout === 'fixed' + }; + return getClasses(conditions, styles, props.className) + }, [props]); + + return {classes, data, sortTable, sortType, sortValue} +} diff --git a/styles/components/dropdown/index.scss b/styles/components/dropdown/index.scss index 5e882e4..277baf5 100644 --- a/styles/components/dropdown/index.scss +++ b/styles/components/dropdown/index.scss @@ -22,6 +22,7 @@ } .dropdown-value { + width: 100%; user-select: none; box-sizing: border-box; display: flex; @@ -43,9 +44,8 @@ color: dt.$general-60; } .dropdown-label { - width: fit-content; + width: 100%; min-width: 146px; - max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/styles/components/table/index.scss b/styles/components/table/index.scss new file mode 100644 index 0000000..4d30bdd --- /dev/null +++ b/styles/components/table/index.scss @@ -0,0 +1,60 @@ +@use '../../../styles/design-tokens' as dt; + +.table { + border-collapse: collapse; + table-layout: auto; + overflow: auto; + width: 100%; + + &.table-fixed { + table-layout: fixed; + } + + & .table-head, .table-cell { + box-sizing: border-box; + border: 1px solid dt.$general-50; + color: dt.$general-90; + padding: 16px 20px; + text-align: center; + + font-size: dt.$typo-font-p-medium-size; + line-height: dt.$typo-font-p-medium-line-height; + font-weight: dt.$typo-font-p-medium-weight; + font-family: dt.$typo-font-p-regular-family; + } + & .table-head, .table-footer { + background-color: dt.$general-30; + } + & .table-head { + + + + & .table-head-cell { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + + & .table-head-sort { + cursor: pointer; + color: dt.$general-90; + opacity: .3; + transition: all 0.2s ease; + &:hover { + opacity: .7; + } + } + & .table-head-sort-active { + opacity: 1; + } + & .table-head-sort-ascending { + transform: rotate(-180deg); + } + } + } + & .table-footer { + &-element { + text-align: left; + } + } +}