diff --git a/README-ru.md b/README-ru.md index 9691c5e..466d033 100644 --- a/README-ru.md +++ b/README-ru.md @@ -392,6 +392,28 @@ const ReorderingExample = () => { }; ``` +### Переупорядочивание без drag handle + +Передайте `dragWithoutHandle`, чтобы вся строка стала зоной начала перетаскивания, и не добавляйте +`dragHandleColumn` в определения столбцов: + +```tsx +const columns: ColumnDef[] = [ + {accessorKey: 'name', header: 'Name'}, + {accessorKey: 'age', header: 'Age'}, +]; + +return ( + + + +); +``` + +Перетаскивание начинается после движения указателя на 8 пикселей, поэтому обычные клики по строке +и контролам продолжают работать. Чтобы исключить пользовательскую область строки из зоны начала +перетаскивания, вызовите `preventDefault()` в её обработчике `onPointerDown`. + ### Переупорядочивание столбцов Оберните таблицу в `ColumnReorderingProvider`, чтобы включить перетаскивание столбцов за их заголовки. diff --git a/README.md b/README.md index 33176af..e664885 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,28 @@ const ReorderingExample = () => { }; ``` +#### Reordering without a drag handle + +Set `dragWithoutHandle` to use the whole row as the drag activator and omit +`dragHandleColumn` from the column definitions: + +```tsx +const columns: ColumnDef[] = [ + {accessorKey: 'name', header: 'Name'}, + {accessorKey: 'age', header: 'Age'}, +]; + +return ( + +
+ +); +``` + +The pointer must move by 8 pixels before dragging starts, so regular row and control clicks keep +working. To exclude a custom part of a row from starting a drag, call `preventDefault()` in its +`onPointerDown` handler. + #### Column reordering Wrap the table with `ColumnReorderingProvider` to enable drag-and-drop reordering of columns by their headers. diff --git a/src/components/BaseDraggableRow/BaseDraggableRow.tsx b/src/components/BaseDraggableRow/BaseDraggableRow.tsx index 47b0352..030f84b 100644 --- a/src/components/BaseDraggableRow/BaseDraggableRow.tsx +++ b/src/components/BaseDraggableRow/BaseDraggableRow.tsx @@ -28,6 +28,7 @@ export const BaseDraggableRow = React.forwardRef( isChildMode, activeItemKey, targetItemIndex = -1, + dragWithoutHandle, enableNesting, // The `useSortable` hook is provided by `@dnd-kit/sortable` library and imported via `SortableListContext`. // This is a temporary solution to prevent importing the entire `@dnd-kit` library @@ -40,6 +41,7 @@ export const BaseDraggableRow = React.forwardRef( transform = null, transition, isDragging = false, + listeners, } = useSortable?.({ id: row.id, }) || {}; @@ -72,17 +74,39 @@ export const BaseDraggableRow = React.forwardRef( ? attributesProp(draggableRow) : attributesProp; + const handlePointerDown = (event: React.PointerEvent) => { + attributes?.onPointerDown?.(event); + + if (event.defaultPrevented) { + return; + } + + listeners?.onPointerDown?.(event); + }; + return { ...attributes, + onPointerDown: dragWithoutHandle + ? handlePointerDown + : attributes?.onPointerDown, 'data-key': draggableRow.id, 'data-depth': depth, 'data-draggable': true, + 'data-drag-without-handle': dragWithoutHandle || undefined, 'data-dragging': isDragging, 'data-drag-active': isDragActive, 'data-expanded': isDragActive && isParent, }; }, - [attributesProp, depth, isDragging, isDragActive, isParent], + [ + attributesProp, + depth, + dragWithoutHandle, + isDragging, + isDragActive, + isParent, + listeners, + ], ); return ( diff --git a/src/components/BaseTable/__stories__/BaseTable.stories.tsx b/src/components/BaseTable/__stories__/BaseTable.stories.tsx index 4c9b612..d1a83da 100644 --- a/src/components/BaseTable/__stories__/BaseTable.stories.tsx +++ b/src/components/BaseTable/__stories__/BaseTable.stories.tsx @@ -83,6 +83,13 @@ export const Reordering: StoryObj = { render: ReorderingStory, }; +export const ReorderingWithoutDragHandle: StoryObj = { + render: ReorderingStory, + args: { + dragWithoutHandle: true, + }, +}; + export const ReorderingTree: StoryObj = { render: ReorderingTreeStory, }; diff --git a/src/components/BaseTable/__stories__/stories/ReorderingStory.tsx b/src/components/BaseTable/__stories__/stories/ReorderingStory.tsx index 64053bb..293d7a9 100644 --- a/src/components/BaseTable/__stories__/stories/ReorderingStory.tsx +++ b/src/components/BaseTable/__stories__/stories/ReorderingStory.tsx @@ -12,11 +12,15 @@ import type {Item} from '../types'; const columns: ColumnDef[] = [dragHandleColumn as ColumnDef, ...originalColumns]; -export const ReorderingStory = () => { +interface ReorderingStoryProps { + dragWithoutHandle?: boolean; +} + +export const ReorderingStory = ({dragWithoutHandle = false}: ReorderingStoryProps) => { const [data, setData] = React.useState(originalData); const table = useTable({ - columns, + columns: dragWithoutHandle ? originalColumns : columns, data, getRowId: (item) => item.id, }); @@ -45,7 +49,11 @@ export const ReorderingStory = () => { }, []); return ( - + ); diff --git a/src/components/ReorderingProvider/ReorderingProvider.scss b/src/components/ReorderingProvider/ReorderingProvider.scss index c8d3ff9..15fa4aa 100644 --- a/src/components/ReorderingProvider/ReorderingProvider.scss +++ b/src/components/ReorderingProvider/ReorderingProvider.scss @@ -7,6 +7,14 @@ $dragHandleBlock: '.#{variables.$ns}drag-handle'; position: relative; &[data-draggable='true'] { + &[data-drag-without-handle='true'] { + cursor: grab; + + &[data-dragging='true'] { + cursor: grabbing; + } + } + &:not([data-drag-active='true']):hover { #{$dragHandleBlock} { z-index: 1; diff --git a/src/components/ReorderingProvider/ReorderingProvider.tsx b/src/components/ReorderingProvider/ReorderingProvider.tsx index 7609a4f..5831729 100644 --- a/src/components/ReorderingProvider/ReorderingProvider.tsx +++ b/src/components/ReorderingProvider/ReorderingProvider.tsx @@ -13,6 +13,8 @@ export interface ReorderingProviderProps { table: Table; /** Children */ children?: React.ReactNode; + /** Enables dragging a row by any part of it instead of requiring a drag handle. Default: `false` */ + dragWithoutHandle?: SortableListProps['dragWithoutHandle']; /** A list of the dnd-kit modifiers */ dndModifiers?: SortableListDndContextProps['modifiers']; /** Determines whether elements can be nested using drag-and-drop */ @@ -24,6 +26,7 @@ export interface ReorderingProviderProps { export const ReorderingProvider = ({ table, children, + dragWithoutHandle = false, dndModifiers, enableNesting, onReorder, @@ -36,6 +39,7 @@ export const ReorderingProvider = ({ items={rowIds} onDragEnd={onReorder} enableNesting={enableNesting} + dragWithoutHandle={dragWithoutHandle} dndModifiers={dndModifiers} > {children} diff --git a/src/components/SortableList/SortableList.tsx b/src/components/SortableList/SortableList.tsx index 165951a..6f8f409 100644 --- a/src/components/SortableList/SortableList.tsx +++ b/src/components/SortableList/SortableList.tsx @@ -14,6 +14,8 @@ import { toRowSortableId, } from '../TableDndRoot'; +import {ROW_DRAG_ACTIVATION_DISTANCE} from './constants'; + const useRowSortable: typeof useSortable = (args) => useSortable({ ...args, @@ -23,6 +25,7 @@ const useRowSortable: typeof useSortable = (args) => export interface SortableListProps extends UseSortableListParams { children?: React.ReactNode; + dragWithoutHandle?: boolean; dndModifiers?: import('@dnd-kit/core').Modifier[]; } @@ -32,6 +35,7 @@ export const SortableList = ({ onDragStart, onDragEnd, enableNesting, + dragWithoutHandle = false, dndModifiers, }: SortableListProps) => { const registry = React.useContext(TableDndRegistryContext); @@ -56,11 +60,12 @@ export const SortableList = ({ const scopeConfig = React.useMemo( () => ({ type: 'row' as const, + activationDistance: dragWithoutHandle ? ROW_DRAG_ACTIVATION_DISTANCE : undefined, modifiers: dndModifiers, autoScroll: true, handlers, }), - [dndModifiers, handlers], + [dndModifiers, dragWithoutHandle, handlers], ); const contextValue = React.useMemo( @@ -73,10 +78,19 @@ export const SortableList = ({ isParentMode, targetItemIndex, enableNesting, + dragWithoutHandle, useSortable: useRowSortable, }) satisfies SortableListContextValue, // eslint-disable-next-line react-hooks/exhaustive-deps - [activeItemKey, isChildMode, isNextChildMode, isParentMode, targetItemIndex, enableNesting], + [ + activeItemKey, + dragWithoutHandle, + enableNesting, + isChildMode, + isNextChildMode, + isParentMode, + targetItemIndex, + ], ); const content = ( diff --git a/src/components/SortableList/constants.ts b/src/components/SortableList/constants.ts new file mode 100644 index 0000000..52ae313 --- /dev/null +++ b/src/components/SortableList/constants.ts @@ -0,0 +1 @@ +export const ROW_DRAG_ACTIVATION_DISTANCE = 8; diff --git a/src/components/SortableListContext/SortableListContext.tsx b/src/components/SortableListContext/SortableListContext.tsx index 2b274fe..b3e6a9d 100644 --- a/src/components/SortableListContext/SortableListContext.tsx +++ b/src/components/SortableListContext/SortableListContext.tsx @@ -6,6 +6,7 @@ import type {useSortableList} from '../../hooks'; export interface SortableListContextValue extends Omit, 'handlers'> { + dragWithoutHandle?: boolean; enableNesting?: boolean; useSortable?: typeof useSortable; } diff --git a/src/components/Table/__stories__/Table.stories.tsx b/src/components/Table/__stories__/Table.stories.tsx index 47b868d..5f7c3ec 100644 --- a/src/components/Table/__stories__/Table.stories.tsx +++ b/src/components/Table/__stories__/Table.stories.tsx @@ -74,6 +74,13 @@ export const Reordering: StoryObj = { render: ReorderingStory, }; +export const ReorderingWithoutDragHandle: StoryObj = { + render: ReorderingStory, + args: { + dragWithoutHandle: true, + }, +}; + export const ColumnReordering: StoryObj = { render: ColumnReorderingStory, }; diff --git a/src/components/Table/__stories__/stories/ReorderingStory.tsx b/src/components/Table/__stories__/stories/ReorderingStory.tsx index d33dddb..33a80c2 100644 --- a/src/components/Table/__stories__/stories/ReorderingStory.tsx +++ b/src/components/Table/__stories__/stories/ReorderingStory.tsx @@ -12,11 +12,15 @@ import {Table} from '../../Table'; const columns: ColumnDef[] = [dragHandleColumn as ColumnDef, ...originalColumns]; -export const ReorderingStory = () => { +interface ReorderingStoryProps { + dragWithoutHandle?: boolean; +} + +export const ReorderingStory = ({dragWithoutHandle = false}: ReorderingStoryProps) => { const [data, setData] = React.useState(originalData); const table = useTable({ - columns, + columns: dragWithoutHandle ? originalColumns : columns, data, getRowId: (item) => item.id, }); @@ -45,7 +49,11 @@ export const ReorderingStory = () => { }, []); return ( - +
); diff --git a/src/components/TableDndRoot/TableDndRoot.tsx b/src/components/TableDndRoot/TableDndRoot.tsx index 1b809de..2eaa366 100644 --- a/src/components/TableDndRoot/TableDndRoot.tsx +++ b/src/components/TableDndRoot/TableDndRoot.tsx @@ -32,7 +32,8 @@ export const TableDndRoot = ({scopes, children}: TableDndRootProps) => { const scopeList = React.useMemo(() => Object.values(scopes), [scopes]); const columnScope = scopeList.find((scope) => scope.type === 'column'); - const activationDistance = columnScope?.activationDistance; + const rowScope = scopeList.find((scope) => scope.type === 'row'); + const activationDistance = columnScope?.activationDistance ?? rowScope?.activationDistance; const pointerSensor = useSensor( PointerSensor,