From c1f0b617adaf5bfc798e47ce40ee118d76dd24b0 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 23 May 2024 14:45:19 +0300 Subject: [PATCH 1/5] [DataTable]: starting point of new dnd implementation. --- .../tables/editableTable/ProjectTableDemo.tsx | 3 +- .../src/table/DataTableRow.module.scss | 7 ++ uui-components/src/table/DataTableRow.tsx | 24 ++++++- uui-core/src/services/dnd/DndActor.tsx | 18 +++++- uui-core/src/types/tables.ts | 8 ++- uui/components/dnd/DropLevel.module.scss | 13 ++++ uui/components/dnd/DropLevel.tsx | 64 +++++++++++++++++++ uui/components/dnd/index.tsx | 1 + uui/components/tables/DataTableRow.tsx | 12 +++- 9 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 uui-components/src/table/DataTableRow.module.scss create mode 100644 uui/components/dnd/DropLevel.module.scss create mode 100644 uui/components/dnd/DropLevel.tsx diff --git a/app/src/demo/tables/editableTable/ProjectTableDemo.tsx b/app/src/demo/tables/editableTable/ProjectTableDemo.tsx index 33f9f4202f..b513e4bf98 100644 --- a/app/src/demo/tables/editableTable/ProjectTableDemo.tsx +++ b/app/src/demo/tables/editableTable/ProjectTableDemo.tsx @@ -50,7 +50,7 @@ export function ProjectTableDemo() { getMetadata: () => metadata, }); - const [tableState, setTableState] = useState({ sorting: [{ field: 'order' }], visibleCount: 1000 }); + const [tableState, setTableState] = useState({ sorting: [{ field: 'order' }], visibleCount: 1000, folded: { 1: false } }); const dataTableFocusManager = useDataTableFocusManager({}, []); const searchHandler = useCallback( @@ -122,6 +122,7 @@ export function ProjectTableDemo() { getRowOptions: (task) => ({ ...lens.prop('items').key(task.id).toProps(), // pass IEditable to each row to allow editing isSelectable: true, + // checkbox: { isVisible: true }, dnd: { srcData: { ...task, isTask: true }, dstData: { ...task, isTask: true }, diff --git a/uui-components/src/table/DataTableRow.module.scss b/uui-components/src/table/DataTableRow.module.scss new file mode 100644 index 0000000000..a433b12a48 --- /dev/null +++ b/uui-components/src/table/DataTableRow.module.scss @@ -0,0 +1,7 @@ +.container { + position: absolute; + top: 0; + height: 100%; + z-index: 100; + width: 100%; +} diff --git a/uui-components/src/table/DataTableRow.tsx b/uui-components/src/table/DataTableRow.tsx index 3c1afd67e4..6bc1965d91 100644 --- a/uui-components/src/table/DataTableRow.tsx +++ b/uui-components/src/table/DataTableRow.tsx @@ -2,8 +2,11 @@ import React, { ReactNode } from 'react'; import isEqual from 'react-fast-compare'; import { DataColumnProps, DataRowProps, uuiMod, DndActorRenderParams, DndActor, uuiMarkers, DataTableRowProps, Lens, IEditable, + DropLevelProps, } from '@epam/uui-core'; import { DataTableRowContainer } from './DataTableRowContainer'; +import { FlexRow } from '../layout'; +import css from './DataTableRow.module.scss'; const uuiDataTableRow = { uuiTableRow: 'uui-table-row', @@ -52,6 +55,18 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop }); }; + const renderDropLevels = (params: DropLevelProps) => { + return ( + + {props.renderDropLevel?.({ ...params, row: props, level: 0 })} + {props.renderDropLevel?.({ ...params, row: props, level: 1 })} + {props.renderDropLevel?.({ ...params, row: props, level: 2 })} + + ); + }; + const renderRow = (params: Partial, clickHandler?: (props: DataRowProps) => void, overlays?: ReactNode) => { return ( (prop const clickHandler = props.onClick || props.onSelect || props.onFold || props.onCheck; if (props.dnd && (props.dnd.srcData || props.dnd.canAcceptDrop)) { - return renderRow(params, clickHandler, props.renderDropMarkers?.(params)) } />; + return ( + renderRow(params, clickHandler, overlays) } + renderDropLevels={ renderDropLevels } + /> + ); } else { return renderRow({}, clickHandler); } diff --git a/uui-core/src/services/dnd/DndActor.tsx b/uui-core/src/services/dnd/DndActor.tsx index 271866b8c1..76188ebe02 100644 --- a/uui-core/src/services/dnd/DndActor.tsx +++ b/uui-core/src/services/dnd/DndActor.tsx @@ -17,7 +17,10 @@ import { DndContextState } from './DndContext'; export interface DndActorProps extends IDndActor { /** Render callback for DragActor content */ - render(props: DndActorRenderParams): React.ReactNode; + render(props: DndActorRenderParams, overlays?: React.ReactNode): React.ReactNode; + renderDropLevels?(props: DndActorRenderParams): React.ReactNode; + + isMultilevel?: boolean; } const DND_START_THRESHOLD = 5; @@ -301,7 +304,18 @@ function TREE_SHAKEABLE_INIT() { } }; - return this.props.render(params); + // if (this.props.isMultilevel + // && this.state.dndContextState.isDragging + // ) { + // return this.props.renderDropLevels(params, this.props.render(params)); + // } + + return this.props.render( + params, + this.props.isMultilevel && this.state.dndContextState.isDragging + ? this.props.renderDropLevels?.(params) + : null, + ); } }; } diff --git a/uui-core/src/types/tables.ts b/uui-core/src/types/tables.ts index 7fdad16484..ceb4a18ecc 100644 --- a/uui-core/src/types/tables.ts +++ b/uui-core/src/types/tables.ts @@ -177,6 +177,12 @@ export interface DataTableColumnsConfigOptions { allowColumnsResizing?: boolean; } +export interface DropLevelProps extends DndActorRenderParams, IHasCX { + size: string; + row: DataRowProps; + level: number; +} + export interface DataTableRowProps extends DataRowProps { /** Array of visible columns */ columns?: DataColumnProps[]; @@ -189,7 +195,7 @@ export interface DataTableRowProps extends DataRowProps< * Render callback for the drop marker. Rendered only if 'dnd' option was provided via getRowProps. * If omitted, default renderer will be used. * */ - renderDropMarkers?: (props: DndActorRenderParams) => ReactNode; + renderDropLevel?: (props: DropLevelProps) => ReactNode; } export interface RenderEditorProps extends IEditable, IHasValidationMessage, ICanFocus { diff --git a/uui/components/dnd/DropLevel.module.scss b/uui/components/dnd/DropLevel.module.scss new file mode 100644 index 0000000000..df32fbb13d --- /dev/null +++ b/uui/components/dnd/DropLevel.module.scss @@ -0,0 +1,13 @@ +.drop-level { + height: 100%; + margin-right: 5px; + + &:hover { + border-bottom: 3px solid; + border-color: var(--uui-info-50); + + &:last-child { + margin-right: 0; + } + } +} diff --git a/uui/components/dnd/DropLevel.tsx b/uui/components/dnd/DropLevel.tsx new file mode 100644 index 0000000000..c044d8d3df --- /dev/null +++ b/uui/components/dnd/DropLevel.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { FlexCell } from '@epam/uui-components'; +import { DropLevelProps } from '@epam/uui-core'; +import css from './DropLevel.module.scss'; + +export function DropLevel(props: DropLevelProps) { + const getIndent = (level: number) => { + switch (props.size) { + case '24': + return level * 6; + case '30': + case '36': + return level * 12; + case '42': + case '48': + case '60': + return level * 24; + default: + return level * 24; + } + }; + + const getFoldingWidth = () => { + switch (props.size) { + case '24': + return 12; + case '30': + case '36': + return 18; + case '42': + case '48': + case '60': + return 24; + default: + return 12; + } + }; + + const getCheckboxWidth = () => { + const additionalItemSize = +props.size < 30 ? 12 : 18; + return additionalItemSize; + }; + + const getDropLevelWidth = (level: number) => { + const foldingWidth = getFoldingWidth(); + if (level === 0) { + const checkboxWidth = props.row.isCheckable ? getCheckboxWidth() : 0; + return getIndent(1) + foldingWidth + checkboxWidth; + } + + return getIndent(1) + foldingWidth; + }; + + const width = props.level < 2 ? getDropLevelWidth(props.level) : '100%'; + + return ( + + + ); +} diff --git a/uui/components/dnd/index.tsx b/uui/components/dnd/index.tsx index 81f74d0e00..71d6700b78 100644 --- a/uui/components/dnd/index.tsx +++ b/uui/components/dnd/index.tsx @@ -1 +1,2 @@ export * from './DropMarker'; +export * from './DropLevel'; diff --git a/uui/components/tables/DataTableRow.tsx b/uui/components/tables/DataTableRow.tsx index 72fa4b36c4..160bf7f2a7 100644 --- a/uui/components/tables/DataTableRow.tsx +++ b/uui/components/tables/DataTableRow.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { DataTableRow as uuiDataTableRow } from '@epam/uui-components'; import { withMods, DataTableCellProps, DndActorRenderParams, DataTableRowProps, + DropLevelProps, } from '@epam/uui-core'; import { DataTableCell } from './DataTableCell'; import { DataTableRowMods } from './types'; -import { DropMarker } from '../dnd'; +import { DropLevel, DropMarker } from '../dnd'; import css from './DataTableRow.module.scss'; import './variables.scss'; @@ -18,7 +19,12 @@ export const renderCell = (props: DataTableCellProps) => { export const renderDropMarkers = (props: DndActorRenderParams) => ; -export const propsMods = { renderCell, renderDropMarkers }; +export const renderDropLevel = (mods: DataTableRowMods) => + function RenderDropLevel(props: DropLevelProps) { + return ; + }; + +export const propsMods = (mods: DataTableRowMods) => ({ renderCell, renderDropLevel: renderDropLevel(mods) }); export const DataTableRow = withMods( uuiDataTableRow, @@ -27,5 +33,5 @@ export const DataTableRow = withMods( css.root, 'uui-dt-vars', borderBottom && 'uui-dt-row-border', css['size-' + (size || '36')], ]; }, - () => propsMods, + (mods) => propsMods(mods), ); From 5a7672e8286e2bebe0794059c23b58ee0dcf334c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 23 May 2024 19:59:12 +0300 Subject: [PATCH 2/5] [DnD]: added tracking of dragging over with levels. --- app/src/demo/dnd/DndCriterion.tsx | 1 + app/src/demo/dnd/DndMaterial.tsx | 1 + app/src/demo/dnd/DndModule.tsx | 1 + app/src/demo/dnd/DndSection.tsx | 1 + app/src/docs/_examples/dnd/Basic.example.tsx | 1 + .../src/table/DataTableHeaderCell.tsx | 1 + uui-components/src/table/DataTableRow.tsx | 28 ++++-- uui-core/src/services/dnd/DndActor.tsx | 60 ++++++++++--- uui-core/src/services/dnd/DndContext.tsx | 90 ++++--------------- .../src/services/dnd/DndRowsDataService.ts | 30 +++++++ .../src/services/dnd/MouseCoordsService.ts | 73 +++++++++++++++ uui-core/src/types/contexts.ts | 8 +- uui-core/src/types/dnd.ts | 26 +++--- uui-core/src/types/tables.ts | 8 +- uui/components/dnd/DropLevel.module.scss | 15 ++-- uui/components/dnd/DropLevel.tsx | 8 +- uui/components/tables/DataTableRow.tsx | 6 +- .../columnsConfigurationModal/ColumnRow.tsx | 1 + 18 files changed, 237 insertions(+), 122 deletions(-) create mode 100644 uui-core/src/services/dnd/DndRowsDataService.ts create mode 100644 uui-core/src/services/dnd/MouseCoordsService.ts diff --git a/app/src/demo/dnd/DndCriterion.tsx b/app/src/demo/dnd/DndCriterion.tsx index b2b06e0f21..148a7750f1 100644 --- a/app/src/demo/dnd/DndCriterion.tsx +++ b/app/src/demo/dnd/DndCriterion.tsx @@ -41,6 +41,7 @@ export class DndCriterion extends React.Component { return ( { return ( { return ( { return ( ( extends React.Component(prop }); }; - const renderDropLevels = (params: DropLevelProps) => { + const renderDropLevels = (params: DndDropLevelsRenderParams & { size: string }) => { return ( - {props.renderDropLevel?.({ ...params, row: props, level: 0 })} - {props.renderDropLevel?.({ ...params, row: props, level: 1 })} - {props.renderDropLevel?.({ ...params, row: props, level: 2 })} + { [...params.path, params.id].map((id, index) => props.renderDropLevel({ + ...params, + row: props, + id, + level: index + 1, + key: `${id}-bottom`, + position: 'bottom', + })) } + + { props.renderDropLevel({ + ...params, + row: props, + id: params.id, + level: params.path.length + 2, + key: `${params.id}-inside`, + position: 'inside', + }) } ); }; @@ -103,8 +117,10 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop id) } render={ (params, overlays) => renderRow(params, clickHandler, overlays) } - renderDropLevels={ renderDropLevels } + renderDropLevels={ props.renderDropLevel ? renderDropLevels : null } /> ); } else { diff --git a/uui-core/src/services/dnd/DndActor.tsx b/uui-core/src/services/dnd/DndActor.tsx index 76188ebe02..bc2cfdfd0d 100644 --- a/uui-core/src/services/dnd/DndActor.tsx +++ b/uui-core/src/services/dnd/DndActor.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { IDndActor, DropPosition, AcceptDropParams, DndActorRenderParams, DropPositionOptions, + DndDropLevelsRenderParams, } from '../../types/dnd'; import { UuiContexts } from '../../types/contexts'; @@ -15,12 +16,15 @@ import { uuiDndState, uuiMarkers } from '../../constants'; import { UuiContext } from '../UuiContext'; import { DndContextState } from './DndContext'; -export interface DndActorProps extends IDndActor { +export interface DndActorProps extends IDndActor { /** Render callback for DragActor content */ render(props: DndActorRenderParams, overlays?: React.ReactNode): React.ReactNode; - renderDropLevels?(props: DndActorRenderParams): React.ReactNode; + renderDropLevels?(props: DndDropLevelsRenderParams): React.ReactNode; isMultilevel?: boolean; + + id: TId; + path?: TId[]; } const DND_START_THRESHOLD = 5; @@ -32,6 +36,7 @@ interface DndActorState { isDragging: boolean; isMouseOver: boolean; position?: DropPosition; + draggingOverLevel: number | null; dndContextState: DndContextState; } @@ -42,6 +47,7 @@ const initialState: DndActorState = { isDragging: false, isMouseOver: false, position: null, + draggingOverLevel: null, dndContextState: { isDragging: false, }, @@ -55,7 +61,7 @@ const initialState: DndActorState = { export const DndActor = TREE_SHAKEABLE_INIT(); function TREE_SHAKEABLE_INIT() { - return class DndActorComponent extends React.Component, DndActorState> { + return class DndActorComponent extends React.Component, DndActorState> { state = initialState; static contextType = UuiContext; public context: UuiContexts; @@ -63,6 +69,12 @@ function TREE_SHAKEABLE_INIT() { componentDidMount() { this.context?.uuiDnD?.subscribe?.(this.contextUpdateHandler); + this.context?.uuiDnD.setDndRowData({ + id: this.props.id, + path: this.props.path ?? [], + dstData: this.props.dstData, + srcData: this.props.srcData, + }); window.addEventListener('pointerup', this.windowPointerUpHandler); window.addEventListener('pointermove', this.windowPointerMoveHandler); } @@ -250,10 +262,35 @@ function TREE_SHAKEABLE_INIT() { }; } + const getDropParams = (id: TId, e: React.MouseEvent): AcceptDropParams => { + const { + left, top, width, height, + } = e.currentTarget.getBoundingClientRect(); + const dndRowData = this.context?.uuiDnD?.getDndRowData(id); + return { + srcData: this.context.uuiDnD.dragData, + dstData: dndRowData?.dstData, + offsetLeft: e.clientX - left, + offsetTop: e.clientY - top, + targetWidth: width, + targetHeight: height, + }; + }; + + const pointerEnterDropLevel = (id: TId, position: DropPosition, level: number) => (e: React.PointerEvent) => { + if (this.context.uuiDnD.isDragging) { + releasePointerCaptureOnEventTarget(e); // allows you to trigger pointer events on other nodes + const dropProps = getDropParams(id, e); + const positionOptions = this.props.canAcceptDrop(dropProps); + const actualPosition = positionOptions?.[position] ? position : null; + this.setState((s) => ({ ...s, isMouseOver: true, position: actualPosition, draggingOverLevel: level })); + } + }; + if (this.props.canAcceptDrop) { const pointerLeaveHandler = () => { if (this.context.uuiDnD.isDragging) { - this.setState((s) => ({ ...s, isMouseOver: false, position: null })); + this.setState((s) => ({ ...s, isMouseOver: false, position: null, draggingOverLevel: null })); } }; @@ -304,17 +341,16 @@ function TREE_SHAKEABLE_INIT() { } }; - // if (this.props.isMultilevel - // && this.state.dndContextState.isDragging - // ) { - // return this.props.renderDropLevels(params, this.props.render(params)); - // } - return this.props.render( params, this.props.isMultilevel && this.state.dndContextState.isDragging - ? this.props.renderDropLevels?.(params) - : null, + ? this.props.renderDropLevels?.({ + id: this.props.id, + path: this.props.path, + onPointerEnter: pointerEnterDropLevel, + isDraggedOver: this.context.uuiDnD?.isDragging && this.state.isMouseOver, + draggingOverLevel: this.state.draggingOverLevel, + }) : null, ); } }; diff --git a/uui-core/src/services/dnd/DndContext.tsx b/uui-core/src/services/dnd/DndContext.tsx index ea0eeaab06..ab467ff36c 100644 --- a/uui-core/src/services/dnd/DndContext.tsx +++ b/uui-core/src/services/dnd/DndContext.tsx @@ -4,6 +4,8 @@ import { getScrollParentOfEventTarget } from '../../helpers/events'; import * as React from 'react'; import { IDndContext } from '../../types/contexts'; import { BaseContext } from '../BaseContext'; +import { MouseCoordsService, TMouseCoords } from './MouseCoordsService'; +import { DndRowData, DndRowsDataService } from './DndRowsDataService'; const maxScrollSpeed = 2000; // px/second @@ -16,9 +18,9 @@ export interface DndContextState { renderGhost?(): React.ReactNode; } -export class DndContext extends BaseContext implements IDndContext { +export class DndContext extends BaseContext implements IDndContext { public isDragging = false; - public dragData: any; + public dragData: TSrcData; private scrollZoneSize = 85; private ghostOffsetX: number = 0; private ghostOffsetY: number = 0; @@ -26,6 +28,7 @@ export class DndContext extends BaseContext implements IDndCont private renderGhostCallback: () => React.ReactNode = null; private lastScrollTime = new Date().getTime(); private mouseCoordsService = new MouseCoordsService(); + private rowsDataService = new DndRowsDataService(); init() { super.init(); @@ -44,13 +47,22 @@ export class DndContext extends BaseContext implements IDndCont window.removeEventListener('pointerup', this.windowPointerUpHandler); this.mouseCoordsService.destroy(); } + this.rowsDataService.destroy(); } public getMouseCoords = (): TMouseCoords => { return this.mouseCoordsService.getCoords(); }; + + public setDndRowData(data: DndRowData) { + this.rowsDataService.setDndRowData(data); + } + + public getDndRowData(id: TId) { + return this.rowsDataService.getDndRowData(id); + } - public startDrag(node: HTMLElement, data: {}, renderGhost: () => React.ReactNode) { + public startDrag(node: HTMLElement, data: TSrcData, renderGhost: () => React.ReactNode) { const offset = getOffset(node); const mouseCoords = this.mouseCoordsService.getCoords(); @@ -172,75 +184,3 @@ export class DndContext extends BaseContext implements IDndCont } } } - -export type TMouseCoords = { - mousePageX: number, - mousePageY: number, - mouseDx: number, - mouseDy: number, - mouseDxSmooth: number, - mouseDySmooth: number, - mouseDownPageX: number, - mouseDownPageY: number, - buttons: number, -}; - -class MouseCoordsService { - private _prevMouseCoords: TMouseCoords; - - init = () => { - this._prevMouseCoords = { - mousePageX: 0, - mousePageY: 0, - mouseDx: 0, - mouseDy: 0, - mouseDxSmooth: 0, - mouseDySmooth: 0, - mouseDownPageX: 0, - mouseDownPageY: 0, - buttons: 0, - }; - if (isClientSide) { - document.addEventListener('pointermove', this.handleMouseCoordsChange); - } - }; - - public destroy() { - if (isClientSide) { - document.removeEventListener('pointermove', this.handleMouseCoordsChange); - } - } - - private handleMouseCoordsChange = (e: PointerEvent) => { - this._prevMouseCoords = getMouseCoordsFromPointerEvent(e, this._prevMouseCoords); - }; - - public getCoords = () => { - return this._prevMouseCoords; - }; -} - -function getMouseCoordsFromPointerEvent(e: PointerEvent, prevCoords: TMouseCoords): TMouseCoords { - const mouseDx = e.pageX - prevCoords.mousePageX; - const mouseDy = e.pageY - prevCoords.mousePageY; - const mouseDxSmooth = prevCoords.mouseDxSmooth * 0.8 + mouseDx * 0.2; - const mouseDySmooth = prevCoords.mouseDySmooth * 0.8 + mouseDy * 0.2; - const mousePageX = e.pageX; - const mousePageY = e.pageY; - const result: TMouseCoords = { - mouseDx, - mouseDy, - mouseDxSmooth, - mouseDySmooth, - mousePageX, - mousePageY, - buttons: e.buttons, - mouseDownPageX: prevCoords.mouseDownPageX || 0, - mouseDownPageY: prevCoords.mouseDownPageY || 0, - }; - if ((prevCoords.buttons === 0 && e.buttons > 0) || e.pointerType === 'touch') { - result.mouseDownPageX = mousePageX; - result.mouseDownPageY = mousePageY; - } - return result; -} diff --git a/uui-core/src/services/dnd/DndRowsDataService.ts b/uui-core/src/services/dnd/DndRowsDataService.ts new file mode 100644 index 0000000000..ddbba0a878 --- /dev/null +++ b/uui-core/src/services/dnd/DndRowsDataService.ts @@ -0,0 +1,30 @@ +import { newMap } from '../../data'; +import { IDndData, IMap } from '../../types'; + +export interface DndRowData extends IDndData { + id: TId; + path: TId[]; + /** Source item data. This is the srcData of the actor that is being dropped into. */ + srcData: TSrcData; + /** Destination item data. This is the dstData of the actor into which the drop is performed. */ + dstData?: TDstData; +} + +export class DndRowsDataService { + dndRows: IMap>; + constructor() { + this.dndRows = newMap({ complexIds: true }); + } + + setDndRowData(data: DndRowData) { + this.dndRows.set(data.id, data); + } + + getDndRowData(id: TId) { + return this.dndRows.get(id) ?? null; + } + + destroy() { + this.dndRows = newMap({ complexIds: true }); + } +} diff --git a/uui-core/src/services/dnd/MouseCoordsService.ts b/uui-core/src/services/dnd/MouseCoordsService.ts new file mode 100644 index 0000000000..c67a593d47 --- /dev/null +++ b/uui-core/src/services/dnd/MouseCoordsService.ts @@ -0,0 +1,73 @@ +import { isClientSide } from '../../helpers/ssr'; + +export type TMouseCoords = { + mousePageX: number, + mousePageY: number, + mouseDx: number, + mouseDy: number, + mouseDxSmooth: number, + mouseDySmooth: number, + mouseDownPageX: number, + mouseDownPageY: number, + buttons: number, +}; + +export class MouseCoordsService { + private _prevMouseCoords: TMouseCoords; + + init = () => { + this._prevMouseCoords = { + mousePageX: 0, + mousePageY: 0, + mouseDx: 0, + mouseDy: 0, + mouseDxSmooth: 0, + mouseDySmooth: 0, + mouseDownPageX: 0, + mouseDownPageY: 0, + buttons: 0, + }; + if (isClientSide) { + document.addEventListener('pointermove', this.handleMouseCoordsChange); + } + }; + + public destroy() { + if (isClientSide) { + document.removeEventListener('pointermove', this.handleMouseCoordsChange); + } + } + + private handleMouseCoordsChange = (e: PointerEvent) => { + this._prevMouseCoords = getMouseCoordsFromPointerEvent(e, this._prevMouseCoords); + }; + + public getCoords = () => { + return this._prevMouseCoords; + }; +} + +function getMouseCoordsFromPointerEvent(e: PointerEvent, prevCoords: TMouseCoords): TMouseCoords { + const mouseDx = e.pageX - prevCoords.mousePageX; + const mouseDy = e.pageY - prevCoords.mousePageY; + const mouseDxSmooth = prevCoords.mouseDxSmooth * 0.8 + mouseDx * 0.2; + const mouseDySmooth = prevCoords.mouseDySmooth * 0.8 + mouseDy * 0.2; + const mousePageX = e.pageX; + const mousePageY = e.pageY; + const result: TMouseCoords = { + mouseDx, + mouseDy, + mouseDxSmooth, + mouseDySmooth, + mousePageX, + mousePageY, + buttons: e.buttons, + mouseDownPageX: prevCoords.mouseDownPageX || 0, + mouseDownPageY: prevCoords.mouseDownPageY || 0, + }; + if ((prevCoords.buttons === 0 && e.buttons > 0) || e.pointerType === 'touch') { + result.mouseDownPageX = mousePageX; + result.mouseDownPageY = mousePageY; + } + return result; +} diff --git a/uui-core/src/types/contexts.ts b/uui-core/src/types/contexts.ts index fcc7eaf7e5..55fdbb07fb 100644 --- a/uui-core/src/types/contexts.ts +++ b/uui-core/src/types/contexts.ts @@ -1,7 +1,7 @@ import { AnalyticsEvent, Link } from './objects'; import * as PropTypes from 'prop-types'; import { IModal, INotification } from './props'; -import { DndContextState, TMouseCoords } from '../services/dnd/DndContext'; +import { DndContextState } from '../services/dnd/DndContext'; import { Lock } from '../services/LockContext'; import { IHistory4 } from '../services/routing/HistoryAdaptedRouter'; import { NotificationOperation } from '../services/NotificationContext'; @@ -9,6 +9,8 @@ import { ModalOperation } from '../services/ModalContext'; import { LayoutLayer } from '../services/LayoutContext'; import { FileUploadOptions, FileUploadResponse } from '../services/ApiContext'; +import { TMouseCoords } from '../services/dnd/MouseCoordsService'; +import { DndRowData } from '../services/dnd/DndRowsDataService'; export interface IBaseContext { /** Add your handler, which will be called on context updates */ @@ -102,13 +104,15 @@ export interface IModalContext extends IBaseContext { getOperations(): ModalOperation[]; } -export interface IDndContext extends IBaseContext { +export interface IDndContext extends IBaseContext { startDrag(node: Node, data: any, renderGhost: () => React.ReactNode): void; endDrag(): void; /** Indicates that drag in progress */ isDragging: boolean; dragData?: any; getMouseCoords: () => TMouseCoords + setDndRowData(data: DndRowData): void; + getDndRowData(id: TId): DndRowData; } /** Save data to the localStorage */ diff --git a/uui-core/src/types/dnd.ts b/uui-core/src/types/dnd.ts index fe55c79af6..a2db4f40a8 100644 --- a/uui-core/src/types/dnd.ts +++ b/uui-core/src/types/dnd.ts @@ -4,11 +4,15 @@ export type DropPosition = 'top' | 'bottom' | 'left' | 'right' | 'inside'; export type DropPositionOptions = Partial>; -export interface AcceptDropParams { +export interface IDndData { /** Source item data. This is the srcData of the actor that is being dropped into. */ - srcData: TSrcData; + srcData?: TSrcData; /** Destination item data. This is the dstData of the actor into which the drop is performed. */ dstData?: TDstData; +} + +export interface AcceptDropParams extends Omit, 'srcData'> { + srcData: TSrcData; offsetLeft: number; offsetTop: number; targetWidth: number; @@ -20,6 +24,14 @@ export interface DropParams extends AcceptDropParams { + id: TId; + path: TId[]; + isDraggedOver?: boolean; + draggingOverLevel?: number | null; + onPointerEnter?: (id: TId, position: DropPosition, level: number) => (e: React.PointerEvent) => void; +} + export interface DndActorRenderParams { /** True, if the element can be dragged. Doesn't mean that DnD is active. */ isDraggable: boolean; @@ -68,15 +80,7 @@ export interface DndActorRenderParams { ref?: React.Ref; } -export interface IDndActor { - /** Data used when this component acts as a drag source. - * If provided, it means this component can be dragged. Can be used in combination with dstData. - */ - srcData?: TSrcData; - /** Data used when this component acts as a drop destination. - * If provided, it means something can be dragged onto this component. Can be used in combination with srcData. - */ - dstData?: TDstData; +export interface IDndActor extends IDndData { /** A pure function that gets permitted positions for a drop action */ canAcceptDrop?(params: AcceptDropParams): DropPositionOptions | null; /** Called when accepted drop action performed on this actor. Usually used to reorder and update items */ diff --git a/uui-core/src/types/tables.ts b/uui-core/src/types/tables.ts index ceb4a18ecc..6370a4c6b2 100644 --- a/uui-core/src/types/tables.ts +++ b/uui-core/src/types/tables.ts @@ -6,7 +6,7 @@ import { import { PickerBaseOptions } from './pickers'; import { DataRowProps } from './dataRows'; import { FilterPredicateName } from './dataQuery'; -import { DndActorRenderParams, DropParams } from './dnd'; +import { DndDropLevelsRenderParams, DropParams, DropPosition } from './dnd'; import { DataSourceState, SortDirection, SortingOption, } from './dataSources'; @@ -177,10 +177,12 @@ export interface DataTableColumnsConfigOptions { allowColumnsResizing?: boolean; } -export interface DropLevelProps extends DndActorRenderParams, IHasCX { +export interface DropLevelProps extends DndDropLevelsRenderParams, IHasCX { size: string; + position: DropPosition; row: DataRowProps; level: number; + key: string; } export interface DataTableRowProps extends DataRowProps { @@ -195,7 +197,7 @@ export interface DataTableRowProps extends DataRowProps< * Render callback for the drop marker. Rendered only if 'dnd' option was provided via getRowProps. * If omitted, default renderer will be used. * */ - renderDropLevel?: (props: DropLevelProps) => ReactNode; + renderDropLevel?: (props: DropLevelProps) => ReactNode; } export interface RenderEditorProps extends IEditable, IHasValidationMessage, ICanFocus { diff --git a/uui/components/dnd/DropLevel.module.scss b/uui/components/dnd/DropLevel.module.scss index df32fbb13d..b58dd25703 100644 --- a/uui/components/dnd/DropLevel.module.scss +++ b/uui/components/dnd/DropLevel.module.scss @@ -1,13 +1,14 @@ .drop-level { height: 100%; margin-right: 5px; +} - &:hover { - border-bottom: 3px solid; - border-color: var(--uui-info-50); - &:last-child { - margin-right: 0; - } - } +.drop-level-dragging-over-row { + border-bottom: 3px solid; + border-color: var(--uui-info-50); } + +.drop-level-active { + margin-right: 0; +} \ No newline at end of file diff --git a/uui/components/dnd/DropLevel.tsx b/uui/components/dnd/DropLevel.tsx index c044d8d3df..1811ca4643 100644 --- a/uui/components/dnd/DropLevel.tsx +++ b/uui/components/dnd/DropLevel.tsx @@ -3,7 +3,7 @@ import { FlexCell } from '@epam/uui-components'; import { DropLevelProps } from '@epam/uui-core'; import css from './DropLevel.module.scss'; -export function DropLevel(props: DropLevelProps) { +export function DropLevel(props: DropLevelProps) { const getIndent = (level: number) => { switch (props.size) { case '24': @@ -51,13 +51,15 @@ export function DropLevel(props: DropLevelProps) { return getIndent(1) + foldingWidth; }; - const width = props.level < 2 ? getDropLevelWidth(props.level) : '100%'; + const isActiveLevel = props.isDraggedOver && props.draggingOverLevel !== null && props.level >= props.draggingOverLevel; + const width = props.level <= props.path.length + 1 ? getDropLevelWidth(props.level) : '100%'; return ( ); diff --git a/uui/components/tables/DataTableRow.tsx b/uui/components/tables/DataTableRow.tsx index 160bf7f2a7..40e64b8282 100644 --- a/uui/components/tables/DataTableRow.tsx +++ b/uui/components/tables/DataTableRow.tsx @@ -19,12 +19,12 @@ export const renderCell = (props: DataTableCellProps) => { export const renderDropMarkers = (props: DndActorRenderParams) => ; -export const renderDropLevel = (mods: DataTableRowMods) => - function RenderDropLevel(props: DropLevelProps) { +export const renderDropLevel = (mods: DataTableRowMods) => + function RenderDropLevel(props: DropLevelProps) { return ; }; -export const propsMods = (mods: DataTableRowMods) => ({ renderCell, renderDropLevel: renderDropLevel(mods) }); +export const propsMods = (mods: DataTableRowMods) => ({ renderCell, renderDropLevel: renderDropLevel(mods) }); export const DataTableRow = withMods( uuiDataTableRow, diff --git a/uui/components/tables/columnsConfigurationModal/ColumnRow.tsx b/uui/components/tables/columnsConfigurationModal/ColumnRow.tsx index e445e75b45..0659713574 100644 --- a/uui/components/tables/columnsConfigurationModal/ColumnRow.tsx +++ b/uui/components/tables/columnsConfigurationModal/ColumnRow.tsx @@ -64,6 +64,7 @@ export const ColumnRow = React.memo(function ColumnRow(props: ColumnRowProps Date: Fri, 24 May 2024 16:35:23 +0300 Subject: [PATCH 3/5] [DnD]: dirty version. --- uui-components/src/table/DataTableRow.tsx | 5 +- uui-core/src/constants/selectors.ts | 1 + uui-core/src/services/dnd/DndActor.tsx | 80 +++++++++++++++++-- uui-core/src/services/dnd/DndContext.tsx | 15 +++- uui-core/src/types/contexts.ts | 7 ++ uui-core/src/types/dnd.ts | 3 + uui/components/dnd/DropLevel.tsx | 5 +- .../tables/DataTableRow.module.scss | 4 + uui/components/tables/variables.scss | 1 + 9 files changed, 108 insertions(+), 13 deletions(-) diff --git a/uui-components/src/table/DataTableRow.tsx b/uui-components/src/table/DataTableRow.tsx index 2909ec09fd..bbaeadb279 100644 --- a/uui-components/src/table/DataTableRow.tsx +++ b/uui-components/src/table/DataTableRow.tsx @@ -57,9 +57,7 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop const renderDropLevels = (params: DndDropLevelsRenderParams & { size: string }) => { return ( - + { [...params.path, params.id].map((id, index) => props.renderDropLevel({ ...params, row: props, @@ -100,6 +98,7 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop props.isSelected && uuiMod.selected, params.isDraggable && uuiMarkers.draggable, props.isInvalid && uuiMod.invalid, + params.isRowHighlighted && uuiMarkers.dropping, uuiDataTableRow.uuiTableRow, props.cx, props.isFocused && uuiMod.focus, diff --git a/uui-core/src/constants/selectors.ts b/uui-core/src/constants/selectors.ts index 6c87477b80..2205179ea0 100644 --- a/uui-core/src/constants/selectors.ts +++ b/uui-core/src/constants/selectors.ts @@ -62,6 +62,7 @@ export const uuiMarkers = { scrolledRight: '-scrolled-right', scrolledTop: '-scrolled-top', scrolledBottom: '-scrolled-bottom', + dropping: 'uui-dropping-marker', } as const; export const uuiDndState = { diff --git a/uui-core/src/services/dnd/DndActor.tsx b/uui-core/src/services/dnd/DndActor.tsx index bc2cfdfd0d..9e8f0becac 100644 --- a/uui-core/src/services/dnd/DndActor.tsx +++ b/uui-core/src/services/dnd/DndActor.tsx @@ -37,6 +37,7 @@ interface DndActorState { isMouseOver: boolean; position?: DropPosition; draggingOverLevel: number | null; + dndContextState: DndContextState; } @@ -50,6 +51,7 @@ const initialState: DndActorState = { draggingOverLevel: null, dndContextState: { isDragging: false, + draggingOverInfo: null, }, }; @@ -283,7 +285,13 @@ function TREE_SHAKEABLE_INIT() { const dropProps = getDropParams(id, e); const positionOptions = this.props.canAcceptDrop(dropProps); const actualPosition = positionOptions?.[position] ? position : null; - this.setState((s) => ({ ...s, isMouseOver: true, position: actualPosition, draggingOverLevel: level })); + this.setState((s) => ({ + ...s, + isMouseOver: true, + position: actualPosition, + draggingOverLevel: level, + })); + this.context?.uuiDnD.setDraggingOverInfo(positionOptions?.[position] ? { id, position } : null); } }; @@ -311,12 +319,12 @@ function TREE_SHAKEABLE_INIT() { params.eventHandlers.onTouchStart = (e) => e.preventDefault(); // prevent defaults on ios - params.eventHandlers.onPointerEnter = pointerMoveHandler; - params.eventHandlers.onPointerMove = pointerMoveHandler; + params.eventHandlers.onPointerEnter = !this.props.isMultilevel ? pointerMoveHandler : null; + params.eventHandlers.onPointerMove = !this.props.isMultilevel ? pointerMoveHandler : null; params.eventHandlers.onPointerLeave = pointerLeaveHandler; } - params.eventHandlers.onPointerUp = (e) => { + params.eventHandlers.onPointerUp = !this.props.isMultilevel ? (e) => { if (this.context.uuiDnD.isDragging) { if (isEventTargetInsideDraggable(e, e.currentTarget)) { return; @@ -339,8 +347,68 @@ function TREE_SHAKEABLE_INIT() { // this.setState(s => ({ ...s, pendingMouseDownTarget: null })); // } } + } : null; + + const onPointerUp = (id: TId) => (e: React.PointerEvent) => { + if (this.context.uuiDnD.isDragging) { + if (isEventTargetInsideDraggable(e, e.currentTarget)) { + return; + } + e.preventDefault(); + if (!!this.state.position && this.props.onDrop) { + this.props.onDrop({ + ...getDropParams(id, e), + position: this.state.position, + }); + } + this.context.uuiDnD.endDrag(); + this.setState(() => initialState); + } else { + // TBD: investigate. Should blur inputs, but doesn't work so far. + // if (this.state.pendingMouseDownTarget) { + // $(this.state.pendingMouseDownTarget).trigger("mousedown"); + // $(this.state.pendingMouseDownTarget).trigger("mouseup"); + // $(this.state.pendingMouseDownTarget).trigger("click"); + // this.setState(s => ({ ...s, pendingMouseDownTarget: null })); + // } + } }; + const shouldHighlightRow = (id: TId) => { + if (this.state.dndContextState.draggingOverInfo == null) { + return false; + } + + const rowData = this.context?.uuiDnD?.getDndRowData(id); + const draggingOverItemData = this.context?.uuiDnD?.getDndRowData(this.state.dndContextState.draggingOverInfo.id); + + const { position } = this.state.dndContextState.draggingOverInfo; + if (position === 'bottom') { + const draggingOverItemParent = draggingOverItemData.path[draggingOverItemData.path.length - 1]; + const rowParent = rowData.path[rowData.path.length - 1]; + if ( + draggingOverItemData.id === rowData.id + || draggingOverItemParent === rowParent + || draggingOverItemParent === rowData.id) { + return true; + } + return false; + } + + if (position === 'inside') { + if (rowData.id === draggingOverItemData.id + || rowData.path[rowData.path.length - 1] === draggingOverItemData.id + ) { + return true; + } + return false; + } + + return false; + }; + + params.isRowHighlighted = shouldHighlightRow(this.props.id); + return this.props.render( params, this.props.isMultilevel && this.state.dndContextState.isDragging @@ -348,9 +416,11 @@ function TREE_SHAKEABLE_INIT() { id: this.props.id, path: this.props.path, onPointerEnter: pointerEnterDropLevel, + onPointerUp: onPointerUp, isDraggedOver: this.context.uuiDnD?.isDragging && this.state.isMouseOver, draggingOverLevel: this.state.draggingOverLevel, - }) : null, + }) + : null, ); } }; diff --git a/uui-core/src/services/dnd/DndContext.tsx b/uui-core/src/services/dnd/DndContext.tsx index ab467ff36c..7a91b72be4 100644 --- a/uui-core/src/services/dnd/DndContext.tsx +++ b/uui-core/src/services/dnd/DndContext.tsx @@ -2,15 +2,16 @@ import { isClientSide } from '../../helpers/ssr'; import { getOffset } from '../../helpers/getOffset'; import { getScrollParentOfEventTarget } from '../../helpers/events'; import * as React from 'react'; -import { IDndContext } from '../../types/contexts'; +import { IDndContext, IDraggingOverInfo } from '../../types/contexts'; import { BaseContext } from '../BaseContext'; import { MouseCoordsService, TMouseCoords } from './MouseCoordsService'; import { DndRowData, DndRowsDataService } from './DndRowsDataService'; const maxScrollSpeed = 2000; // px/second -export interface DndContextState { +export interface DndContextState { isDragging: boolean; + draggingOverInfo?: IDraggingOverInfo | null; ghostOffsetX?: number; ghostOffsetY?: number; ghostWidth?: number; @@ -18,8 +19,9 @@ export interface DndContextState { renderGhost?(): React.ReactNode; } -export class DndContext extends BaseContext implements IDndContext { +export class DndContext extends BaseContext> implements IDndContext { public isDragging = false; + public draggingOverInfo: IDraggingOverInfo | null = null; public dragData: TSrcData; private scrollZoneSize = 85; private ghostOffsetX: number = 0; @@ -62,6 +64,10 @@ export class DndContext extends BaseC return this.rowsDataService.getDndRowData(id); } + public setDraggingOverInfo(info: IDraggingOverInfo) { + this.update({ isDragging: this.isDragging, draggingOverInfo: info }); + } + public startDrag(node: HTMLElement, data: TSrcData, renderGhost: () => React.ReactNode) { const offset = getOffset(node); const mouseCoords = this.mouseCoordsService.getCoords(); @@ -79,6 +85,7 @@ export class DndContext extends BaseC this.update({ isDragging: true, + draggingOverInfo: null, ghostOffsetX: this.ghostOffsetX, ghostOffsetY: this.ghostOffsetY, ghostWidth: this.ghostWidth, @@ -98,7 +105,7 @@ export class DndContext extends BaseC } new Promise((res) => { - this.update({ isDragging: false }); + this.update({ isDragging: false, draggingOverInfo: null }); res(); }).then(() => { this.renderGhostCallback = null; diff --git a/uui-core/src/types/contexts.ts b/uui-core/src/types/contexts.ts index 55fdbb07fb..3e540f13a2 100644 --- a/uui-core/src/types/contexts.ts +++ b/uui-core/src/types/contexts.ts @@ -11,6 +11,7 @@ import { LayoutLayer } from '../services/LayoutContext'; import { FileUploadOptions, FileUploadResponse } from '../services/ApiContext'; import { TMouseCoords } from '../services/dnd/MouseCoordsService'; import { DndRowData } from '../services/dnd/DndRowsDataService'; +import { DropPosition } from './dnd'; export interface IBaseContext { /** Add your handler, which will be called on context updates */ @@ -104,9 +105,15 @@ export interface IModalContext extends IBaseContext { getOperations(): ModalOperation[]; } +export interface IDraggingOverInfo { + id: TId; + position: DropPosition; +} + export interface IDndContext extends IBaseContext { startDrag(node: Node, data: any, renderGhost: () => React.ReactNode): void; endDrag(): void; + setDraggingOverInfo(info: IDraggingOverInfo): void; /** Indicates that drag in progress */ isDragging: boolean; dragData?: any; diff --git a/uui-core/src/types/dnd.ts b/uui-core/src/types/dnd.ts index a2db4f40a8..c31c8d943e 100644 --- a/uui-core/src/types/dnd.ts +++ b/uui-core/src/types/dnd.ts @@ -28,8 +28,10 @@ export interface DndDropLevelsRenderParams { id: TId; path: TId[]; isDraggedOver?: boolean; + isRowHighlighted?: boolean; draggingOverLevel?: number | null; onPointerEnter?: (id: TId, position: DropPosition, level: number) => (e: React.PointerEvent) => void; + onPointerUp?: (id: TId) => (e: React.PointerEvent) => void; } export interface DndActorRenderParams { @@ -56,6 +58,7 @@ export interface DndActorRenderParams { /** Drop position. Chosen from accepted drop positions according to pointer coordinates */ position?: DropPosition; + isRowHighlighted?: boolean; /** * Event handlers. Component is expected to pass these events to the top element it renders. diff --git a/uui/components/dnd/DropLevel.tsx b/uui/components/dnd/DropLevel.tsx index 1811ca4643..a45173de5f 100644 --- a/uui/components/dnd/DropLevel.tsx +++ b/uui/components/dnd/DropLevel.tsx @@ -59,7 +59,10 @@ export function DropLevel(props: DropLevelProps) { width={ width } minWidth={ getDropLevelWidth(props.level) } cx={ [css.dropLevel, props.isDraggedOver ? css.dropLevelDraggingOverRow : false, isActiveLevel ? css.dropLevelActive : false] } - rawProps={ { onPointerEnter: props.onPointerEnter(props.id, props.position, props.level) } } + rawProps={ { + onPointerEnter: props.onPointerEnter?.(props.id, props.position, props.level), + onPointerUp: props.onPointerUp?.(props.id), + } } > ); diff --git a/uui/components/tables/DataTableRow.module.scss b/uui/components/tables/DataTableRow.module.scss index dd9d8ade93..8338bfb72f 100644 --- a/uui/components/tables/DataTableRow.module.scss +++ b/uui/components/tables/DataTableRow.module.scss @@ -41,6 +41,10 @@ &:global(.uui-dt-row-border) { border-bottom-color: var(--uui-dt-border); } + + &:global(.uui-dropping-marker) { + --uui-dt-row-bg: var(--uui-dt-row-bg-dropping-capabilities); + } } :global(.uui-dt-row-border) { diff --git a/uui/components/tables/variables.scss b/uui/components/tables/variables.scss index 0528e6bdb7..19b494f5ce 100644 --- a/uui/components/tables/variables.scss +++ b/uui/components/tables/variables.scss @@ -11,6 +11,7 @@ --uui-dt-row-bg-hover: var(--uui-surface-higher); --uui-dt-row-bg-invalid: var(--uui-error-5); --uui-dt-row-bg-selected: var(--uui-info-5); + --uui-dt-row-bg-dropping-capabilities: var(--uui-info-5); --uui-dt-header-row-bg: var(--uui-neutral-5); --uui-dt-header-row-bg-hover: var(--uui-surface-highest); From 70d84651411a6cee172b8bad2a71929a8ab6227b Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 27 May 2024 11:22:09 +0300 Subject: [PATCH 4/5] [DnD]: made tiny refactoring --- uui-components/src/table/DataTableRow.tsx | 20 +-- uui-core/src/services/dnd/DndActor.tsx | 145 +++++++++++++--------- uui-core/src/types/dnd.ts | 14 ++- uui-core/src/types/tables.ts | 11 +- uui/components/dnd/DropLevel.tsx | 8 +- 5 files changed, 103 insertions(+), 95 deletions(-) diff --git a/uui-components/src/table/DataTableRow.tsx b/uui-components/src/table/DataTableRow.tsx index bbaeadb279..2c2f50f0c6 100644 --- a/uui-components/src/table/DataTableRow.tsx +++ b/uui-components/src/table/DataTableRow.tsx @@ -55,26 +55,10 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop }); }; - const renderDropLevels = (params: DndDropLevelsRenderParams & { size: string }) => { + const renderDropLevels = (params: DndDropLevelsRenderParams) => { return ( - { [...params.path, params.id].map((id, index) => props.renderDropLevel({ - ...params, - row: props, - id, - level: index + 1, - key: `${id}-bottom`, - position: 'bottom', - })) } - - { props.renderDropLevel({ - ...params, - row: props, - id: params.id, - level: params.path.length + 2, - key: `${params.id}-inside`, - position: 'inside', - }) } + { params.dropLevelsProps.map((dropLevelProps) => props.renderDropLevel({ ...dropLevelProps, isRowCheckable: props.isCheckable })) } ); }; diff --git a/uui-core/src/services/dnd/DndActor.tsx b/uui-core/src/services/dnd/DndActor.tsx index 9e8f0becac..b86ff49c09 100644 --- a/uui-core/src/services/dnd/DndActor.tsx +++ b/uui-core/src/services/dnd/DndActor.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { IDndActor, DropPosition, AcceptDropParams, DndActorRenderParams, DropPositionOptions, DndDropLevelsRenderParams, + DndDropLevelRenderProps, } from '../../types/dnd'; import { UuiContexts } from '../../types/contexts'; @@ -218,6 +219,87 @@ function TREE_SHAKEABLE_INIT() { } } + private getMultilevelDropParams = (id: TId, e: React.MouseEvent): AcceptDropParams => { + const { + left, top, width, height, + } = e.currentTarget.getBoundingClientRect(); + const dndRowData = this.context?.uuiDnD?.getDndRowData(id); + return { + srcData: this.context.uuiDnD.dragData, + dstData: dndRowData?.dstData, + offsetLeft: e.clientX - left, + offsetTop: e.clientY - top, + targetWidth: width, + targetHeight: height, + }; + }; + + private pointerEnterDropLevel = (id: TId, position: DropPosition, level: number) => (e: React.PointerEvent) => { + if (this.context.uuiDnD.isDragging) { + releasePointerCaptureOnEventTarget(e); // allows you to trigger pointer events on other nodes + const dropProps = this.getMultilevelDropParams(id, e); + const positionOptions = this.props.canAcceptDrop(dropProps); + const actualPosition = positionOptions?.[position] ? position : null; + this.setState((s) => ({ + ...s, + isMouseOver: true, + position: actualPosition, + draggingOverLevel: level, + })); + this.context?.uuiDnD.setDraggingOverInfo(positionOptions?.[position] ? { id, position } : null); + } + }; + + private onMultilevelDropPointerUp = (id: TId) => (e: React.PointerEvent) => { + if (this.context.uuiDnD.isDragging) { + if (isEventTargetInsideDraggable(e, e.currentTarget)) { + return; + } + e.preventDefault(); + if (!!this.state.position && this.props.onDrop) { + this.props.onDrop({ + ...this.getMultilevelDropParams(id, e), + position: this.state.position, + }); + } + this.context.uuiDnD.endDrag(); + this.setState(() => initialState); + } + }; + + getDropLevelsProps(): DndDropLevelRenderProps[] { + const lastLevel = this.props.path.length + 2; + const lastPosition = 'inside'; + const isDraggedOver = this.context.uuiDnD?.isDragging && this.state.isMouseOver; + const draggingOverLevel = this.state.draggingOverLevel; + return [ + ...[...this.props.path, this.props.id].map((id, index) => { + const position = 'bottom'; + const level = index + 1; + return { + id, + level, + lastLevel, + key: `${id}-${position}`, + isDraggedOver, + draggingOverLevel, + onPointerUp: this.onMultilevelDropPointerUp(id), + onPointerEnter: this.pointerEnterDropLevel(id, position, level), + }; + }), + { + id: this.props.id, + level: lastLevel, + lastLevel, + key: `${this.props.id}-${lastPosition}`, + isDraggedOver, + draggingOverLevel, + onPointerUp: this.onMultilevelDropPointerUp(this.props.id), + onPointerEnter: this.pointerEnterDropLevel(this.props.id, lastPosition, lastLevel), + }, + ]; + } + render() { const params: DndActorRenderParams = { isDraggable: !!this.props.srcData, @@ -264,37 +346,6 @@ function TREE_SHAKEABLE_INIT() { }; } - const getDropParams = (id: TId, e: React.MouseEvent): AcceptDropParams => { - const { - left, top, width, height, - } = e.currentTarget.getBoundingClientRect(); - const dndRowData = this.context?.uuiDnD?.getDndRowData(id); - return { - srcData: this.context.uuiDnD.dragData, - dstData: dndRowData?.dstData, - offsetLeft: e.clientX - left, - offsetTop: e.clientY - top, - targetWidth: width, - targetHeight: height, - }; - }; - - const pointerEnterDropLevel = (id: TId, position: DropPosition, level: number) => (e: React.PointerEvent) => { - if (this.context.uuiDnD.isDragging) { - releasePointerCaptureOnEventTarget(e); // allows you to trigger pointer events on other nodes - const dropProps = getDropParams(id, e); - const positionOptions = this.props.canAcceptDrop(dropProps); - const actualPosition = positionOptions?.[position] ? position : null; - this.setState((s) => ({ - ...s, - isMouseOver: true, - position: actualPosition, - draggingOverLevel: level, - })); - this.context?.uuiDnD.setDraggingOverInfo(positionOptions?.[position] ? { id, position } : null); - } - }; - if (this.props.canAcceptDrop) { const pointerLeaveHandler = () => { if (this.context.uuiDnD.isDragging) { @@ -349,31 +400,6 @@ function TREE_SHAKEABLE_INIT() { } } : null; - const onPointerUp = (id: TId) => (e: React.PointerEvent) => { - if (this.context.uuiDnD.isDragging) { - if (isEventTargetInsideDraggable(e, e.currentTarget)) { - return; - } - e.preventDefault(); - if (!!this.state.position && this.props.onDrop) { - this.props.onDrop({ - ...getDropParams(id, e), - position: this.state.position, - }); - } - this.context.uuiDnD.endDrag(); - this.setState(() => initialState); - } else { - // TBD: investigate. Should blur inputs, but doesn't work so far. - // if (this.state.pendingMouseDownTarget) { - // $(this.state.pendingMouseDownTarget).trigger("mousedown"); - // $(this.state.pendingMouseDownTarget).trigger("mouseup"); - // $(this.state.pendingMouseDownTarget).trigger("click"); - // this.setState(s => ({ ...s, pendingMouseDownTarget: null })); - // } - } - }; - const shouldHighlightRow = (id: TId) => { if (this.state.dndContextState.draggingOverInfo == null) { return false; @@ -413,12 +439,7 @@ function TREE_SHAKEABLE_INIT() { params, this.props.isMultilevel && this.state.dndContextState.isDragging ? this.props.renderDropLevels?.({ - id: this.props.id, - path: this.props.path, - onPointerEnter: pointerEnterDropLevel, - onPointerUp: onPointerUp, - isDraggedOver: this.context.uuiDnD?.isDragging && this.state.isMouseOver, - draggingOverLevel: this.state.draggingOverLevel, + dropLevelsProps: this.getDropLevelsProps(), }) : null, ); diff --git a/uui-core/src/types/dnd.ts b/uui-core/src/types/dnd.ts index c31c8d943e..7297f8b29c 100644 --- a/uui-core/src/types/dnd.ts +++ b/uui-core/src/types/dnd.ts @@ -25,13 +25,19 @@ export interface DropParams extends AcceptDropParams { + isRowCheckable?: boolean; + dropLevelsProps: DndDropLevelRenderProps[]; +} + +export interface DndDropLevelRenderProps { id: TId; - path: TId[]; + level: number; + lastLevel: number; + key: string; isDraggedOver?: boolean; - isRowHighlighted?: boolean; draggingOverLevel?: number | null; - onPointerEnter?: (id: TId, position: DropPosition, level: number) => (e: React.PointerEvent) => void; - onPointerUp?: (id: TId) => (e: React.PointerEvent) => void; + onPointerEnter?: (e: React.PointerEvent) => void; + onPointerUp?: (e: React.PointerEvent) => void; } export interface DndActorRenderParams { diff --git a/uui-core/src/types/tables.ts b/uui-core/src/types/tables.ts index 6370a4c6b2..4fcafe9222 100644 --- a/uui-core/src/types/tables.ts +++ b/uui-core/src/types/tables.ts @@ -6,7 +6,7 @@ import { import { PickerBaseOptions } from './pickers'; import { DataRowProps } from './dataRows'; import { FilterPredicateName } from './dataQuery'; -import { DndDropLevelsRenderParams, DropParams, DropPosition } from './dnd'; +import { DndDropLevelRenderProps, DropParams } from './dnd'; import { DataSourceState, SortDirection, SortingOption, } from './dataSources'; @@ -177,12 +177,9 @@ export interface DataTableColumnsConfigOptions { allowColumnsResizing?: boolean; } -export interface DropLevelProps extends DndDropLevelsRenderParams, IHasCX { - size: string; - position: DropPosition; - row: DataRowProps; - level: number; - key: string; +export interface DropLevelProps extends DndDropLevelRenderProps, IHasCX { + size?: string; + isRowCheckable: boolean; } export interface DataTableRowProps extends DataRowProps { diff --git a/uui/components/dnd/DropLevel.tsx b/uui/components/dnd/DropLevel.tsx index a45173de5f..44c83d250b 100644 --- a/uui/components/dnd/DropLevel.tsx +++ b/uui/components/dnd/DropLevel.tsx @@ -44,7 +44,7 @@ export function DropLevel(props: DropLevelProps) { const getDropLevelWidth = (level: number) => { const foldingWidth = getFoldingWidth(); if (level === 0) { - const checkboxWidth = props.row.isCheckable ? getCheckboxWidth() : 0; + const checkboxWidth = props.isRowCheckable ? getCheckboxWidth() : 0; return getIndent(1) + foldingWidth + checkboxWidth; } @@ -52,7 +52,7 @@ export function DropLevel(props: DropLevelProps) { }; const isActiveLevel = props.isDraggedOver && props.draggingOverLevel !== null && props.level >= props.draggingOverLevel; - const width = props.level <= props.path.length + 1 ? getDropLevelWidth(props.level) : '100%'; + const width = props.level < props.lastLevel ? getDropLevelWidth(props.level) : '100%'; return ( (props: DropLevelProps) { minWidth={ getDropLevelWidth(props.level) } cx={ [css.dropLevel, props.isDraggedOver ? css.dropLevelDraggingOverRow : false, isActiveLevel ? css.dropLevelActive : false] } rawProps={ { - onPointerEnter: props.onPointerEnter?.(props.id, props.position, props.level), - onPointerUp: props.onPointerUp?.(props.id), + onPointerEnter: props.onPointerEnter, + onPointerUp: props.onPointerUp, } } > From 37dc9f9a5c83e32de5dcdc557b6f4183bd11b32e Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 27 May 2024 12:43:42 +0300 Subject: [PATCH 5/5] [Dnd]: good dragGhost styles added. --- uui-components/src/table/DataTableRow.tsx | 35 +++++++++++++++++++ uui-core/src/constants/selectors.ts | 2 ++ uui-core/src/services/dnd/DndActor.tsx | 3 +- uui-core/src/services/dnd/DndContext.tsx | 2 ++ .../tables/DataTableRow.module.scss | 14 +++++++- 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/uui-components/src/table/DataTableRow.tsx b/uui-components/src/table/DataTableRow.tsx index 2c2f50f0c6..cdbf12dea4 100644 --- a/uui-components/src/table/DataTableRow.tsx +++ b/uui-components/src/table/DataTableRow.tsx @@ -3,6 +3,7 @@ import isEqual from 'react-fast-compare'; import { DataColumnProps, DataRowProps, uuiMod, DndActorRenderParams, DndActor, uuiMarkers, DataTableRowProps, Lens, IEditable, DndDropLevelsRenderParams, + uuiDndState, } from '@epam/uui-core'; import { DataTableRowContainer } from './DataTableRowContainer'; import { FlexRow } from '../layout'; @@ -83,6 +84,7 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop params.isDraggable && uuiMarkers.draggable, props.isInvalid && uuiMod.invalid, params.isRowHighlighted && uuiMarkers.dropping, + params.isDndInProgress && uuiDndState.dragInProgress, uuiDataTableRow.uuiTableRow, props.cx, props.isFocused && uuiMod.focus, @@ -92,6 +94,38 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop /> ); }; + + const renderDragGhost = (params: Partial, clickHandler?: (props: DataRowProps) => void, overlays?: ReactNode) => { + return ( + clickHandler(props)) } + rawProps={ { + ...props.rawProps, + ...params.eventHandlers, + role: 'row', + 'aria-expanded': (props.isFolded === undefined || props.isFolded === null) ? undefined : !props.isFolded, + ...(props.isSelectable && { 'aria-selected': props.isSelected }), + style: { width: props.columns[0].width, minWidth: props.columns[0].minWidth ?? props.columns[0].width }, + } } + cx={ [ + params.classNames, + props.isSelected && uuiMod.selected, + params.isDraggable && uuiMarkers.draggable, + props.isInvalid && uuiMod.invalid, + params.isRowHighlighted && uuiMarkers.dropping, + uuiDataTableRow.uuiTableRow, + uuiDndState.dragGhost, + props.cx, + props.isFocused && uuiMod.focus, + ] } + overlays={ overlays } + link={ props.link } + /> + ); + }; const clickHandler = props.onClick || props.onSelect || props.onFold || props.onCheck; @@ -103,6 +137,7 @@ const DataTableRowImpl = React.forwardRef(function DataTableRow(prop id={ props.id } path={ props.path.map(({ id }) => id) } render={ (params, overlays) => renderRow(params, clickHandler, overlays) } + renderDragGhost={ (params, overlays) => renderDragGhost(params, clickHandler, overlays) } renderDropLevels={ props.renderDropLevel ? renderDropLevels : null } /> ); diff --git a/uui-core/src/constants/selectors.ts b/uui-core/src/constants/selectors.ts index 2205179ea0..b2f7b61a9d 100644 --- a/uui-core/src/constants/selectors.ts +++ b/uui-core/src/constants/selectors.ts @@ -69,6 +69,8 @@ export const uuiDndState = { draggedOut: 'uui-dragged-out', dropAccepted: 'uui-drop-accepted', dragGhost: 'uui-drag-ghost', + dragHandle: 'uui-drag-handle', + dragInProgress: 'uui-drag-in-progress', } as const; export const uuiDataTableHeaderCell = { diff --git a/uui-core/src/services/dnd/DndActor.tsx b/uui-core/src/services/dnd/DndActor.tsx index b86ff49c09..21852eda88 100644 --- a/uui-core/src/services/dnd/DndActor.tsx +++ b/uui-core/src/services/dnd/DndActor.tsx @@ -21,6 +21,7 @@ export interface DndActorProps extends IDndActor): React.ReactNode; + renderDragGhost?(props: DndActorRenderParams, overlays?: React.ReactNode): React.ReactNode; isMultilevel?: boolean; @@ -115,7 +116,7 @@ function TREE_SHAKEABLE_INIT() { if (dist > DND_START_THRESHOLD) { this.context.uuiDnD.startDrag(this.dndRef.current, this.props.srcData, () => - this.props.render({ + (this.props.renderDragGhost ?? this.props.render)({ isDragGhost: true, isDraggedOver: false, isDraggable: false, diff --git a/uui-core/src/services/dnd/DndContext.tsx b/uui-core/src/services/dnd/DndContext.tsx index 7a91b72be4..6bddd5b63a 100644 --- a/uui-core/src/services/dnd/DndContext.tsx +++ b/uui-core/src/services/dnd/DndContext.tsx @@ -97,6 +97,7 @@ export class DndContext extends BaseC const ev = document.createEvent('Events'); ev.initEvent('dragstart', true, false); document.body.dispatchEvent(ev); + document.body.style.cursor = 'grabbing'; } public endDrag() { @@ -111,6 +112,7 @@ export class DndContext extends BaseC this.renderGhostCallback = null; this.dragData = null; this.isDragging = false; + document.body.style.cursor = 'default'; }); } diff --git a/uui/components/tables/DataTableRow.module.scss b/uui/components/tables/DataTableRow.module.scss index 8338bfb72f..5b443ae1d7 100644 --- a/uui/components/tables/DataTableRow.module.scss +++ b/uui/components/tables/DataTableRow.module.scss @@ -14,7 +14,7 @@ } } - &:global(.-draggable) { + &:global(.-draggable):not(:global(.uui-drag-in-progress)) { @include dnd-cursor-style(); } @@ -31,7 +31,19 @@ } &:global(.uui-drag-ghost) { + opacity: 0.8; + cursor: grabbing; + + :global(.uui-drag-handle) { + visibility: visible; + } + @include dnd-ghost-shadow(); + @include dnd-cursor-style(); + } + + &:global(.uui-drag-in-progress) { + cursor: grabbing; } &:global(.uui-focus) {