Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table dnd enhancement #3839

Merged
merged 2 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions packages/react/src/components/Table/StatefulTable.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { merge, pick, cloneDeep } from 'lodash-es';
import { screen, render, fireEvent, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Screen16, ViewOff16 } from '@carbon/icons-react';
import { BreadcrumbItem } from 'carbon-components-react';

import { settings } from '../../constants/Settings';
import { EMPTY_STRING_DISPLAY_VALUE } from '../../constants/Filters';
import Breadcrumb from '../Breadcrumb/Breadcrumb';

import * as reducer from './baseTableReducer';
import StatefulTable from './StatefulTable';
Expand Down Expand Up @@ -218,6 +220,135 @@ describe('stateful table with real reducer', () => {
// Make sure we started the drags
expect(handleDrag).toHaveBeenCalledTimes(3);
});

it('does drop over breadcrumb node', () => {
// Reduce screen size to show overflow menu inside the breadcrumb
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
writable: true,
configurable: true,
value: 400,
});
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
writable: true,
configurable: true,
value: 500,
});

const data = [
{
id: '0',
values: {
string: 'row 0',
},
isDraggable: true,
},
{
id: '1',
values: {
string: 'row 1',
},
isDraggable: true,
},
];

const handleDrag = jest.fn();
let lastDroppedOnNode;

const { container } = render(
<>
<div style={{ width: '40vw', padding: 10 }}>
<Breadcrumb hasOverflow>
<BreadcrumbItem href="#" title="Folder with very long name is created for example">
Folder with very long name is created for example
</BreadcrumbItem>
<BreadcrumbItem href="#" title="2 Devices">
2 Devices
</BreadcrumbItem>
<BreadcrumbItem href="#" title="A really long folder name">
A really long folder name
</BreadcrumbItem>
<BreadcrumbItem href="#" title="4 Another folder">
4 Another folder
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage title="5th level folder">
5th level folder
</BreadcrumbItem>
</Breadcrumb>
</div>
<StatefulTable
id="dndTable"
columns={tableColumns}
data={data}
options={{ hasDragAndDrop: true, hasBreadcrumbDrop: true }}
actions={{
table: {
onDrag() {
handleDrag();
return {
dropIds: ['Folder 1'],
preview: 'mock preview',
};
},
onDrop(dragRowId, droppedOnNode) {
lastDroppedOnNode = droppedOnNode;
},
},
}}
/>
</>
);

const breadcrumbNodes = container.querySelectorAll(`.${prefix}--breadcrumb-item`);
// will return 3 as screen size is reduced
expect(breadcrumbNodes.length).toBe(3);

const dragHandles = container.querySelectorAll(`.${iotPrefix}--table-drag-handle`);
expect(dragHandles.length).toBe(2);

// mimicks drop over breadcrumb node
fireEvent.mouseDown(dragHandles[0]);
fireEvent.mouseMove(dragHandles[0], { buttons: 1, clientX: 0, clientY: 0 });
fireEvent.mouseMove(breadcrumbNodes[0], { buttons: 1, clientX: 100, clientY: 100 });
fireEvent.mouseEnter(breadcrumbNodes[0]);
fireEvent.mouseUp(breadcrumbNodes[0]);

expect(lastDroppedOnNode).toEqual(breadcrumbNodes[0]);
expect(lastDroppedOnNode.title).toEqual('Folder with very long name is created for example');

// mimicks hover over breadcrumb node but won't drop
fireEvent.mouseDown(dragHandles[0]);
fireEvent.mouseMove(dragHandles[0], { buttons: 1, clientX: 0, clientY: 0 });
fireEvent.mouseMove(breadcrumbNodes[0], { clientX: 100, clientY: 100 });
fireEvent.mouseEnter(breadcrumbNodes[0]);
fireEvent.mouseLeave(breadcrumbNodes[0]);
// Added to avoid warning of wrraping in act() in case of state change due to mouse events
fireEvent.keyDown(container, { key: 'a' }); // this is ignored, but needed for code coverage
fireEvent.keyDown(container, { key: 'Escape' });

const ellipsisNode = container.querySelectorAll(`.${prefix}--overflow-menu`);

userEvent.click(ellipsisNode[0]);
const overflowMenuNode = screen.getByText('A really long folder name').closest('li');

// mimicks drop over breadcrumb node inside overflow menu
fireEvent.mouseDown(dragHandles[0]);
fireEvent.mouseMove(dragHandles[0], { buttons: 1, clientX: 0, clientY: 0 });
fireEvent.mouseMove(overflowMenuNode, { clientX: 100, clientY: 100 });
fireEvent.mouseEnter(overflowMenuNode);
fireEvent.mouseUp(overflowMenuNode);

expect(lastDroppedOnNode).toEqual(overflowMenuNode);

// mimicks hover over breadcrumb node inside overflow menu but won't drop
fireEvent.mouseDown(dragHandles[0]);
fireEvent.mouseMove(dragHandles[0], { buttons: 1, clientX: 0, clientY: 0 });
fireEvent.mouseMove(overflowMenuNode, { clientX: 100, clientY: 100 });
fireEvent.mouseEnter(overflowMenuNode);
fireEvent.mouseLeave(overflowMenuNode);

delete HTMLElement.prototype.clientWidth;
delete HTMLElement.prototype.scrollWidth;
});
});

it('should clear filters', async () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/react/src/components/Table/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ const propTypes = {
* `actions.table.onDrag` and `actions.table.onDrop` callback props.
*/
hasDragAndDrop: PropTypes.bool,
/** Making this true in addition to hasDragAndDrop will consider breadcrumb nodes as drop targets */
hasBreadcrumbDrop: PropTypes.bool,
/** Freezes table header and footer */
pinHeaderAndFooter: PropTypes.bool,
}),
Expand Down Expand Up @@ -448,6 +450,7 @@ export const defaultProps = (baseProps) => ({
hasFilterRowIcon: false,
pinColumn: PIN_COLUMN.NONE,
hasDragAndDrop: false,
hasBreadcrumbDrop: false,
pinHeaderAndFooter: false,
},
size: undefined,
Expand Down Expand Up @@ -1205,7 +1208,8 @@ const Table = (props) => {
'shouldLazyRender',
'preserveCellWhiteSpace',
'useRadioButtonSingleSelect',
'hasDragAndDrop'
'hasDragAndDrop',
'hasBreadcrumbDrop'
)}
hideDragHandles={hideDragHandles}
hasRowExpansion={!!options.hasRowExpansion}
Expand Down
50 changes: 43 additions & 7 deletions packages/react/src/components/Table/Table.main.story.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { action } from '@storybook/addon-actions';
import { object, select, boolean, text, number } from '@storybook/addon-knobs';
import { cloneDeep, debounce, merge, uniqueId } from 'lodash-es';
import { ToastNotification } from 'carbon-components-react';
import { ToastNotification, BreadcrumbItem } from 'carbon-components-react';
import { SettingsAdjust16 } from '@carbon/icons-react';

import StoryNotice from '../../internal/StoryNotice';
Expand All @@ -12,6 +12,7 @@ import RuleBuilder from '../RuleBuilder/RuleBuilder';
import useStoryState from '../../internal/storyState';
import FlyoutMenu, { FlyoutMenuDirection } from '../FlyoutMenu/FlyoutMenu';
import { csvDownloadHandler } from '../../utils/componentUtilityFunctions';
import Breadcrumb from '../Breadcrumb/Breadcrumb';

import TableREADME from './mdx/Table.mdx';
import SortingREADME from './mdx/Sorting.mdx';
Expand Down Expand Up @@ -520,6 +521,11 @@ function NaiveMultiRowDragPreview({ rows }) {
}

export function WithDragAndDrop() {
const { hasBreadcrumbDrop } = getTableKnobs({
knobsToCreate: ['hasBreadcrumbDrop'],
getDefaultValue: () => true,
});

const columns = [
{
id: 'type',
Expand Down Expand Up @@ -579,12 +585,32 @@ export function WithDragAndDrop() {
<button type="button" style={{ marginBottom: '1rem' }} onClick={reset}>
Reset Rows
</button>
<div style={{ width: '40vw', padding: 10 }}>
<Breadcrumb hasOverflow>
<BreadcrumbItem href="#" title="Folder with very long name is created for example">
Folder with very long name is created for example
</BreadcrumbItem>
<BreadcrumbItem href="#" title="2 Devices">
2 Devices
</BreadcrumbItem>
<BreadcrumbItem href="#" title="A really long folder name">
A really long folder name
</BreadcrumbItem>
<BreadcrumbItem href="#" title="4 Another folder">
4 Another folder
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage title="5th level folder">
5th level folder
</BreadcrumbItem>
</Breadcrumb>
</div>
<StatefulTable
secondaryTitle="Table"
columns={columns}
data={data}
options={{
hasDragAndDrop: true,
hasBreadcrumbDrop,
hasResize: true,
hasRowSelection: 'multi',
hasFilter: true,
Expand All @@ -600,13 +626,23 @@ export function WithDragAndDrop() {
preview: <NaiveMultiRowDragPreview rows={rows} />,
};
},
onDrop: (dragRowIds, dropRowId) => {
action('onDrop')(dragRowIds, dropRowId);

onDrop: (dragRowIds, dropRowIdOrNode) => {
action('onDrop')(dragRowIds, dropRowIdOrNode);
const newData = data.filter((row) => !dragRowIds.includes(row.id));
const folderRow = newData.find((row) => dropRowId === row.id);
folderRow.values.count += dragRowIds.length;
folderRow.values.name = `${folderRow.values.string} (${folderRow.values.count} inside)`;
if (typeof dropRowIdOrNode === 'string') {
const folderRow = newData.find((row) => dropRowIdOrNode === row.id);
folderRow.values.count += dragRowIds.length;
folderRow.values.name = `${folderRow.values.string} (${folderRow.values.count} inside)`;
} else if (
dropRowIdOrNode instanceof Element ||
(dropRowIdOrNode && dropRowIdOrNode.nodeType === Node.ELEMENT_NODE)
) {
const name =
dropRowIdOrNode.title && dropRowIdOrNode.title !== ''
? dropRowIdOrNode.title
: dropRowIdOrNode.innerText;
console.info(`>>> Dropped ${dragRowIds} onto breadcrumb node ${name}`);
}
setData(newData);
},
},
Expand Down
7 changes: 7 additions & 0 deletions packages/react/src/components/Table/Table.story.helpers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1667,6 +1667,13 @@ export const getTableKnobs = ({ knobsToCreate, getDefaultValue, useGroups = fals
hasDragAndDrop: shouldCreate('hasDragAndDrop')
? boolean('Drag and drop (hasDragAndDrop)', getDefaultValue('hasDragAndDrop'), DND_GROUP)
: null,
hasBreadcrumbDrop: shouldCreate('hasBreadcrumbDrop')
? boolean(
'Enable drop over breadcrumb (hasBreadcrumbDrop)',
getDefaultValue('hasBreadcrumbDrop'),
DND_GROUP
)
: null,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ const propTypes = {
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
/** If room is reserved for drag handles at the start of rows. */
hasDragAndDrop: PropTypes.bool,
/** If true will pass it to DnD component so that breadcrumb items are considered as drop targets. */
hasBreadcrumbDrop: PropTypes.bool,
/** If all drag handles should be hidden. This happens when an undraggable row is in the selection. */
hideDragHandles: PropTypes.bool,
/** Optional base z-index for the drag image. See details on Table component. */
Expand Down Expand Up @@ -148,6 +150,7 @@ const defaultProps = {
actionFailedText: 'Action failed',
size: undefined,
hasDragAndDrop: false,
hasBreadcrumbDrop: false,
hideDragHandles: false,
zIndex: 0,
pinColumn: PIN_COLUMN.NONE,
Expand Down Expand Up @@ -194,6 +197,7 @@ const TableBody = ({
preserveCellWhiteSpace,
size,
hasDragAndDrop,
hasBreadcrumbDrop,
hideDragHandles,
zIndex,
pinColumn,
Expand Down Expand Up @@ -288,7 +292,7 @@ const TableBody = ({
handleStartPossibleDrag,
handleEnterRow,
handleLeaveRow,
} = useTableDnd(rows, selectedIds, zIndex, actions.onDrag, actions.onDrop);
} = useTableDnd(rows, selectedIds, zIndex, actions.onDrag, actions.onDrop, hasBreadcrumbDrop);

const tableBodyClassNames = classnames(
pinColumnClassNames({ pinColumn, hasRowSelection, hasRowExpansion, hasRowNesting })
Expand Down
18 changes: 18 additions & 0 deletions packages/react/src/components/Table/TableBody/_table-dnd.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,21 @@ body.#{$iot-prefix}--is-dragging {
transition: none;
}
}

// Additional styles for overlay in case of breadcrumb nodes to change the background color and show
// border when drop is happening
.#{$iot-prefix}--breadcrumb-drop-node-overlay {
background-color: $carbon--blue-10;
border: dashed 2px $carbon--blue-60;
}

// To hide default underline during drop which is shown on link hover of breadcrumb node
.#{$prefix}--row--on--link--dropping {
text-decoration: none !important;
}

// Added to overflow menu for items which are shown in menu when breadcrumb length too big
.#{$prefix}--breadcrumbmenu--on--row--dropping {
background-color: $carbon--blue-10 !important;
outline: dashed 2px $carbon--blue-60 !important;
}
Loading
Loading