diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.test.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.test.tsx index 6a25cc1166a..6428596c823 100644 --- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.test.tsx +++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.test.tsx @@ -1,6 +1,7 @@ import { mountThemedComponent } from '@shared/tests/utils'; import { AnalyticalTable } from '@ui5/webcomponents-react/lib/AnalyticalTable'; import React from 'react'; +import { act } from 'react-dom/test-utils'; const columns = [ { @@ -146,6 +147,7 @@ describe('AnalyticalTable', () => { .instance(); // @ts-ignore component.onclick({}); + console.log(component); // test desc function inside the popover element component = wrapper @@ -172,9 +174,19 @@ describe('AnalyticalTable', () => { minRows={5} selectable={true} subRowsKey="subRows" + isTreeTable={true} /> ); + let colInst = wrapper + .find({ role: 'columnheader' }) + .at(0) + .instance(); + + // @ts-ignore + expect(colInst.draggable).toBeDefined(); + // @ts-ignore + expect(colInst.draggable).toBeFalsy(); expect(wrapper.render()).toMatchSnapshot(); }); @@ -209,4 +221,33 @@ describe('AnalyticalTable', () => { expect(wrapper.render()).toMatchSnapshot(); }); + + test('test drag and drop of a draggable column', () => { + const wrapper = mountThemedComponent(); + + // get first column of the table and simulate dragging of it + let componentDrag = wrapper.find({ role: 'columnheader' }).at(0); + let inst = componentDrag.instance(); + // @ts-ignore + let dragColumnId = inst.id; + + // @ts-ignore + expect(inst.draggable).toBeDefined(); + // @ts-ignore + expect(inst.draggable).toBeTruthy(); + // @ts-ignore + componentDrag.simulate('drag'); + + // get second column of the table and simulate dropping on it + let dataTransfer = {}; + // @ts-ignore + dataTransfer.getData = () => { + return dragColumnId; + }; + let componentDrop = wrapper.find({ role: 'columnheader' }).at(1); + // @ts-ignore + componentDrop.simulate('drop', { dataTransfer: dataTransfer }); + + expect(wrapper.render()).toMatchSnapshot(); + }); }); diff --git a/packages/main/src/components/AnalyticalTable/ColumnHeader/Resizer.tsx b/packages/main/src/components/AnalyticalTable/ColumnHeader/Resizer.tsx index 9fe5389ae1e..a4c7a400339 100644 --- a/packages/main/src/components/AnalyticalTable/ColumnHeader/Resizer.tsx +++ b/packages/main/src/components/AnalyticalTable/ColumnHeader/Resizer.tsx @@ -8,6 +8,7 @@ const Resizer = (props) => { const startX = useRef(0); const resizerRef: RefObject = useRef(); const onColumnSizeChanged = props['onColumnSizeChanged']; + const onColumnBeingResized = props['onColumnBeingResized']; const onResize = useCallback( (e) => { @@ -30,8 +31,9 @@ const Resizer = (props) => { document.removeEventListener('mouseleave', onEndResize); delete resizerRef.current.parentElement.style.userSelect; + onColumnBeingResized({ value: false }); }, - [onResize, resizerRef] + [onResize, resizerRef, onColumnBeingResized] ); const onStartResize = useCallback( @@ -44,8 +46,9 @@ const Resizer = (props) => { document.addEventListener('mousemove', onResize); document.addEventListener('mouseup', onEndResize); document.addEventListener('mouseleave', onEndResize); + onColumnBeingResized({ value: true }); }, - [onResize, onEndResize, parentWidth, startX, resizerRef] + [onResize, onEndResize, parentWidth, startX, resizerRef, onColumnBeingResized] ); return ( diff --git a/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx b/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx index 674d8d03c94..45b5c0122e2 100644 --- a/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx +++ b/packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx @@ -1,8 +1,8 @@ import { Event } from '@ui5/webcomponents-react-base/lib/Event'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { Icon } from '@ui5/webcomponents-react/lib/Icon'; -import React, { CSSProperties, FC, ReactNode, ReactNodeArray, useMemo } from 'react'; -import { createUseStyles } from 'react-jss'; +import React, { CSSProperties, DragEventHandler, FC, ReactNode, ReactNodeArray, useMemo } from 'react'; +import { createUseStyles, useTheme } from 'react-jss'; import { JSSTheme } from '../../../interfaces/JSSTheme'; import { Resizer } from './Resizer'; import { ColumnType } from '../types/ColumnType'; @@ -13,6 +13,7 @@ import '@ui5/webcomponents/dist/icons/sort-descending'; import '@ui5/webcomponents/dist/icons/sort-ascending'; export interface ColumnHeaderProps { + id: string; defaultSortDesc: boolean; onFilteredChange: (event: Event) => void; children: ReactNode | ReactNodeArray; @@ -26,6 +27,14 @@ export interface ColumnHeaderProps { isLastColumn?: boolean; onSort?: (e: Event) => void; onGroupBy?: (e: Event) => void; + onDragStart: DragEventHandler; + onDragOver: DragEventHandler; + onDrop: DragEventHandler; + onDragEnter: DragEventHandler; + onDragEnd: DragEventHandler; + dragOver: boolean; + isResizing: boolean; + isDraggable: boolean; } const styles = ({ parameters }: JSSTheme) => ({ @@ -64,6 +73,7 @@ export const ColumnHeader: FC = (props) => { const classes = useStyles(props); const { + id, children, column, className, @@ -73,7 +83,14 @@ export const ColumnHeader: FC = (props) => { filterable, isLastColumn, onSort, - onGroupBy + onGroupBy, + onDragEnter, + onDragOver, + onDragStart, + onDrop, + onDragEnd, + isDraggable, + dragOver } = props; const openBy = useMemo(() => { @@ -106,6 +123,7 @@ export const ColumnHeader: FC = (props) => { }, [classes, column.filterValue, column.isSorted, column.isGrouped, column.isSortedDesc, children]); const isResizable = !isLastColumn && column.canResize; + const theme = useTheme() as JSSTheme; const innerStyle: CSSProperties = useMemo(() => { const modifiedStyles = { ...style, @@ -118,13 +136,27 @@ export const ColumnHeader: FC = (props) => { if (isResizable) { modifiedStyles.maxWidth = `calc(100% - 16px)`; } + if (dragOver) { + modifiedStyles.borderLeft = '3px solid ' + theme.parameters.sapSelectedColor; + } return modifiedStyles as CSSProperties; }, [style, isResizable]); if (!column) return null; return ( -
+
{groupable || sortable || filterable ? (
+ > + + + + + Fra + +
+ > + + + 40 + +
+ > + + + MAR + +
+ > + + + 28 + +
+ + + +
`; + +exports[`AnalyticalTable test drag and drop of a draggable column 1`] = ` +
+
+ + Test + +
+
+
+
+
+
+
+ + Age + +
+
+
+ + + + Sort Ascending + + + Sort Descending + + + +
+
+
+
+
+ + Name + +
+
+
+ + + + Sort Ascending + + + Sort Descending + + + +
+
+
+
+
+ + Friend Name + +
+
+
+ + + + Sort Ascending + + + Sort Descending + + + +
+
+
+
+
+ + + Friend Age + + +
+
+
+ + + + Sort Ascending + + + Sort Descending + + + +
+
+
+
+
+
+ + 40 + +
+
+ + Fra + +
+
+ + MAR + +
+
+ + 28 + +
+
+
+
+ + 20 + +
+
+ + bla + +
+
+ + Nei + +
+
+ + 50 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/packages/main/src/components/AnalyticalTable/demo/demo.stories.tsx b/packages/main/src/components/AnalyticalTable/demo/demo.stories.tsx index bdb2594787b..f6721ae0b45 100644 --- a/packages/main/src/components/AnalyticalTable/demo/demo.stories.tsx +++ b/packages/main/src/components/AnalyticalTable/demo/demo.stories.tsx @@ -79,6 +79,7 @@ export const defaultTable = () => { groupBy={array('groupBy', [])} rowHeight={number('rowHeight', 60)} selectedRowKey={text('selectedRowKey', `row_5`)} + onColumnsReordered={action('onColumnsReordered')} />
); diff --git a/packages/main/src/components/AnalyticalTable/hooks/useDragAndDrop.ts b/packages/main/src/components/AnalyticalTable/hooks/useDragAndDrop.ts new file mode 100644 index 00000000000..850c2929890 --- /dev/null +++ b/packages/main/src/components/AnalyticalTable/hooks/useDragAndDrop.ts @@ -0,0 +1,57 @@ +import { useCallback, useRef, useState } from 'react'; + +const getColumnId = (column) => { + return typeof column.accessor === 'string' ? column.accessor : column.id; +}; + +export const useDragAndDrop = (props, setColumnOrder, columnOrder, isBeingResized, onColumnsOrderChanged) => { + const [dragOver, setDragOver] = useState(''); + + const handleDragStart = useCallback( + (e) => { + if (isBeingResized) { + e.preventDefault(); + return; + } + e.dataTransfer.setData('colId', e.currentTarget.id); + }, + [isBeingResized] + ); + + const handleDragOver = useCallback((e) => { + e.preventDefault(); + }, []); + + const handleDragEnter = useCallback((e) => { + setDragOver(e.currentTarget.id); + }, []); + + const handleOnDrop = useCallback( + (e) => { + setDragOver(''); + + const droppedColId = e.currentTarget.id; + const draggedColId = e.dataTransfer.getData('colId'); + if (droppedColId === draggedColId) return; + + const internalColumnOrder = columnOrder.length > 0 ? columnOrder : props.columns.map((col) => getColumnId(col)); + const droppedColIdx = internalColumnOrder.findIndex((col) => col === droppedColId); + const draggedColIdx = internalColumnOrder.findIndex((col) => col === draggedColId); + + const tempCols = [...internalColumnOrder]; + tempCols.splice(droppedColIdx, 0, tempCols.splice(draggedColIdx, 1)[0]); + setColumnOrder(tempCols); + + const newOrderedColumns = [...props.columns]; + newOrderedColumns.splice(droppedColIdx, 0, newOrderedColumns.splice(draggedColIdx, 1)[0]); + onColumnsOrderChanged(e.currentTarget, props.columns[draggedColIdx], newOrderedColumns); + }, + [columnOrder] + ); + + const handleOnDragEnd = useCallback(() => { + setDragOver(''); + }, [dragOver]); + + return [dragOver, handleDragEnter, handleDragStart, handleDragOver, handleOnDrop, handleOnDragEnd]; +}; diff --git a/packages/main/src/components/AnalyticalTable/hooks/useResizeColumns.ts b/packages/main/src/components/AnalyticalTable/hooks/useResizeColumns.ts index 912a783e068..7c92b397267 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useResizeColumns.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useResizeColumns.ts @@ -12,5 +12,13 @@ export const useResizeColumns = () => { [setResizedColumns, resizedColumns] ); - return [resizedColumns, onColumnSizeChanged]; + const [isBeingResized, setBeingResized] = useState(false); + const onColumnBeingResized = useCallback( + ({ value }) => { + setBeingResized(value); + }, + [setBeingResized] + ); + + return [resizedColumns, onColumnSizeChanged, isBeingResized, onColumnBeingResized]; }; diff --git a/packages/main/src/components/AnalyticalTable/hooks/useTableHeaderStyling.ts b/packages/main/src/components/AnalyticalTable/hooks/useTableHeaderStyling.ts index 41d74883eb2..07ac953bc7e 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useTableHeaderStyling.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useTableHeaderStyling.ts @@ -1,16 +1,17 @@ import { useCallback } from 'react'; -export const useTableHeaderStyling = (classes, onColumnSizeChanged) => +export const useTableHeaderStyling = (classes, onColumnSizeChanged, onColumnBeingResized) => useCallback( (instance) => { instance.getHeaderProps.push((column) => { return { className: classes.th, onColumnSizeChanged, + onColumnBeingResized, column }; }); return instance; }, - [classes.th, onColumnSizeChanged] + [classes.th, onColumnSizeChanged, onColumnBeingResized] ); diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx index 02be28a0d7f..36380a29fb3 100644 --- a/packages/main/src/components/AnalyticalTable/index.tsx +++ b/packages/main/src/components/AnalyticalTable/index.tsx @@ -15,7 +15,7 @@ import React, { useMemo } from 'react'; import { createUseStyles, useTheme } from 'react-jss'; -import { useExpanded, useFilters, useGroupBy, useSortBy, useTable } from 'react-table'; +import { useExpanded, useFilters, useGroupBy, useSortBy, useTable, useColumnOrder } from 'react-table'; import { CommonProps } from '../../interfaces/CommonProps'; import { JSSTheme } from '../../interfaces/JSSTheme'; import styles from './AnayticalTable.jss'; @@ -37,6 +37,7 @@ import { useWindowResize } from './hooks/useWindowResize'; import { makeTemplateColumns } from './hooks/utils'; import { TitleBar } from './TitleBar'; import { VirtualTableBody } from './virtualization/VirtualTableBody'; +import { useDragAndDrop } from './hooks/useDragAndDrop'; export interface ColumnConfiguration { accessor?: string; @@ -85,6 +86,7 @@ export interface TableProps extends CommonProps { groupable?: boolean; groupBy?: string[]; selectable?: boolean; + columnOrder?: object[]; // events @@ -92,6 +94,7 @@ export interface TableProps extends CommonProps { onGroup?: (e?: Event) => void; onRowSelected?: (e?: Event) => any; onRowExpandChange?: (e?: Event) => any; + onColumnsReordered?: (e?: Event) => void; /** * additional options which will be passed to [react-table“s useTable hook](https://github.com/tannerlinsley/react-table/blob/master/docs/api.md#table-options) */ @@ -134,6 +137,7 @@ const AnalyticalTable: FC = forwardRef((props: TableProps, ref: Ref< selectedRowKey, LoadingComponent, onRowExpandChange, + onColumnsReordered, noDataText, NoDataComponent, visibleRows, @@ -146,12 +150,12 @@ const AnalyticalTable: FC = forwardRef((props: TableProps, ref: Ref< const classes = useStyles({ rowHeight: props.rowHeight }); const [selectedRowPath, onRowClicked] = useRowSelection(onRowSelected, selectedRowKey); - const [resizedColumns, onColumnSizeChanged] = useResizeColumns(); + const [resizedColumns, onColumnSizeChanged, isBeingResized, onColumnBeingResized] = useResizeColumns(); const [analyticalTableRef, reactWindowRef] = useTableScrollHandles(ref); const getSubRows = useCallback((row) => row[subRowsKey] || [], [subRowsKey]); - const { getTableProps, headerGroups, rows, prepareRow, setState, state: tableState } = useTable( + const { getTableProps, headerGroups, rows, prepareRow, setState, state: tableState, setColumnOrder } = useTable( { columns, data, @@ -161,11 +165,12 @@ const AnalyticalTable: FC = forwardRef((props: TableProps, ref: Ref< }, useFilters, useGroupBy, + useColumnOrder, useSortBy, useExpanded, useTableStyling(classes), useTableHeaderGroupStyling(classes, resizedColumns), - useTableHeaderStyling(classes, onColumnSizeChanged), + useTableHeaderStyling(classes, onColumnSizeChanged, onColumnBeingResized), useTableRowStyling( classes, resizedColumns, @@ -248,7 +253,26 @@ const AnalyticalTable: FC = forwardRef((props: TableProps, ref: Ref< [tableState.groupBy, onGroup] ); + const onColumnsOrderChanged = useCallback( + (target, column, columnsNewOrder) => { + onColumnsReordered( + Event.of(null, target, { + columnsNewOrder, + column + }) + ); + }, + [tableState.columnOrder, onColumnsReordered] + ); + const [headerRef, tableWidth] = useWindowResize(); + const [dragOver, handleDragEnter, handleDragStart, handleDragOver, handleOnDrop, handleOnDragEnd] = useDragAndDrop( + props, + setColumnOrder, + tableState.columnOrder, + isBeingResized, + onColumnsOrderChanged + ); return (
@@ -265,6 +289,7 @@ const AnalyticalTable: FC = forwardRef((props: TableProps, ref: Ref<
{headerGroup.headers.map((column, index) => ( = forwardRef((props: TableProps, ref: Ref< filterable={props.filterable} onSort={props.onSort} onGroupBy={onGroupByChanged} + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDrop={handleOnDrop} + onDragEnter={handleDragEnter} + onDragEnd={handleOnDragEnd} + dragOver={column.id === dragOver} + column={column} + isDraggable={!isTreeTable} > {column.render('Header')} @@ -341,6 +374,7 @@ AnalyticalTable.defaultProps = { subRowsKey: 'subRows', onGroup: () => {}, onRowExpandChange: () => {}, + onColumnsReordered: () => {}, isTreeTable: false, alternateRowColor: false }; diff --git a/packages/main/src/components/FilterBar/demo.stories.tsx b/packages/main/src/components/FilterBar/demo.stories.tsx index def4dd45270..fe682ffec0e 100644 --- a/packages/main/src/components/FilterBar/demo.stories.tsx +++ b/packages/main/src/components/FilterBar/demo.stories.tsx @@ -11,8 +11,14 @@ import { VariantManagement } from '@ui5/webcomponents-react/lib/VariantManagemen import { action } from '@storybook/addon-actions'; import notes from './FilterBar.md'; -const variantItems = [{ label: 'Variant 1', key: '1' }, { label: 'Variant 2', key: '2' }]; -const filterItems = [{ text: 'Text 1', key: '1' }, { text: 'Text 2', key: '2' }]; +const variantItems = [ + { label: 'Variant 1', key: '1' }, + { label: 'Variant 2', key: '2' } +]; +const filterItems = [ + { text: 'Text 1', key: '1' }, + { text: 'Text 2', key: '2' } +]; const renderVariants = () => { return ( diff --git a/packages/main/src/components/VariantManagement/demo.stories.tsx b/packages/main/src/components/VariantManagement/demo.stories.tsx index 79fb98a4b57..743a5648e4f 100644 --- a/packages/main/src/components/VariantManagement/demo.stories.tsx +++ b/packages/main/src/components/VariantManagement/demo.stories.tsx @@ -6,7 +6,10 @@ import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; import { VariantManagement } from '@ui5/webcomponents-react/lib/VariantManagement'; import notes from './VariantManagement.md'; -const variantItems = [{ label: 'Variant 1', key: '1' }, { label: 'Variant 2', key: '2' }]; +const variantItems = [ + { label: 'Variant 1', key: '1' }, + { label: 'Variant 2', key: '2' } +]; export const renderStory = () => (