diff --git a/packages/react/src/components/Table/StatefulTable.test.jsx b/packages/react/src/components/Table/StatefulTable.test.jsx index f134ae5be1..d5cc41c749 100644 --- a/packages/react/src/components/Table/StatefulTable.test.jsx +++ b/packages/react/src/components/Table/StatefulTable.test.jsx @@ -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'; @@ -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( + <> +
+ + + Folder with very long name is created for example + + + 2 Devices + + + A really long folder name + + + 4 Another folder + + + 5th level folder + + +
+ + + ); + + 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 () => { diff --git a/packages/react/src/components/Table/Table.jsx b/packages/react/src/components/Table/Table.jsx index fcfc125648..89a41fddef 100644 --- a/packages/react/src/components/Table/Table.jsx +++ b/packages/react/src/components/Table/Table.jsx @@ -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, }), @@ -448,6 +450,7 @@ export const defaultProps = (baseProps) => ({ hasFilterRowIcon: false, pinColumn: PIN_COLUMN.NONE, hasDragAndDrop: false, + hasBreadcrumbDrop: false, pinHeaderAndFooter: false, }, size: undefined, @@ -1205,7 +1208,8 @@ const Table = (props) => { 'shouldLazyRender', 'preserveCellWhiteSpace', 'useRadioButtonSingleSelect', - 'hasDragAndDrop' + 'hasDragAndDrop', + 'hasBreadcrumbDrop' )} hideDragHandles={hideDragHandles} hasRowExpansion={!!options.hasRowExpansion} diff --git a/packages/react/src/components/Table/Table.main.story.jsx b/packages/react/src/components/Table/Table.main.story.jsx index 211d313e9e..7c713fd284 100644 --- a/packages/react/src/components/Table/Table.main.story.jsx +++ b/packages/react/src/components/Table/Table.main.story.jsx @@ -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'; @@ -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'; @@ -520,6 +521,11 @@ function NaiveMultiRowDragPreview({ rows }) { } export function WithDragAndDrop() { + const { hasBreadcrumbDrop } = getTableKnobs({ + knobsToCreate: ['hasBreadcrumbDrop'], + getDefaultValue: () => true, + }); + const columns = [ { id: 'type', @@ -579,12 +585,32 @@ export function WithDragAndDrop() { +
+ + + Folder with very long name is created for example + + + 2 Devices + + + A really long folder name + + + 4 Another folder + + + 5th level folder + + +
, }; }, - 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); }, }, diff --git a/packages/react/src/components/Table/Table.story.helpers.jsx b/packages/react/src/components/Table/Table.story.helpers.jsx index 328ba7c766..dbe1032b84 100644 --- a/packages/react/src/components/Table/Table.story.helpers.jsx +++ b/packages/react/src/components/Table/Table.story.helpers.jsx @@ -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, }; }; diff --git a/packages/react/src/components/Table/TableBody/TableBody.jsx b/packages/react/src/components/Table/TableBody/TableBody.jsx index 1a421cc0c6..ae15ca81ba 100644 --- a/packages/react/src/components/Table/TableBody/TableBody.jsx +++ b/packages/react/src/components/Table/TableBody/TableBody.jsx @@ -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. */ @@ -148,6 +150,7 @@ const defaultProps = { actionFailedText: 'Action failed', size: undefined, hasDragAndDrop: false, + hasBreadcrumbDrop: false, hideDragHandles: false, zIndex: 0, pinColumn: PIN_COLUMN.NONE, @@ -194,6 +197,7 @@ const TableBody = ({ preserveCellWhiteSpace, size, hasDragAndDrop, + hasBreadcrumbDrop, hideDragHandles, zIndex, pinColumn, @@ -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 }) diff --git a/packages/react/src/components/Table/TableBody/_table-dnd.scss b/packages/react/src/components/Table/TableBody/_table-dnd.scss index ba3ae25508..df85120f89 100644 --- a/packages/react/src/components/Table/TableBody/_table-dnd.scss +++ b/packages/react/src/components/Table/TableBody/_table-dnd.scss @@ -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; +} diff --git a/packages/react/src/components/Table/TableBody/useTableDnd.jsx b/packages/react/src/components/Table/TableBody/useTableDnd.jsx index 41a0c83a1c..d5c9e4b609 100644 --- a/packages/react/src/components/Table/TableBody/useTableDnd.jsx +++ b/packages/react/src/components/Table/TableBody/useTableDnd.jsx @@ -91,7 +91,7 @@ function getRtlVerticalScrollbarWidth() { * rows being dragged and the ID of the row being dropped on. * @returns {UseTableDndResult} Values to mix into the table. */ -function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { +function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop, hasBreadcrumbDrop) { // These are related. When the user "mousedown"s on a drag handle, we consider it a "possible" // drag. At this point we add all the event listeners to track the drag. Put only after they move // past some threshold do we actually set `isDragging`. At that point the drag is "real" and we'll @@ -252,6 +252,9 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { [] ); + /** Ref to the root DOM element of the row overlay. Used to directly update its style. */ + const overlayRef = useRef(null); + useEffect( /** * Once a drag appears to start (user "mousedown"s on a drag handle) this adds the needed event @@ -283,6 +286,159 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { } } + /** + * Callback when item drag mouse enters li of overflow menu + * @param {React.MouseEvent} e + */ + function handleOverflowLiEnter(e) { + const node = e.currentTarget; + const hyperLink = node.querySelector(`.${prefix}--overflow-menu-options__btn`); + hyperLink.classList.add(`${prefix}--breadcrumbmenu--on--row--dropping`); + activeDropRowIdRef.current = node; + } + + /** + * Callback when item drag mouse leaves li of overflow menu + * @param {React.MouseEvent} e + */ + function handleOverflowLiLeave(e) { + const node = e.currentTarget; + if (activeDropRowIdRef.current === node) { + activeDropRowIdRef.current = null; + } + const hyperLink = node.querySelector(`.${prefix}--overflow-menu-options__btn`); + if (hyperLink) hyperLink.classList.remove(`${prefix}--breadcrumbmenu--on--row--dropping`); + } + + /** + * Callback when the mouse enters a table row. The table needs to add this as an onmouseenter + * handler. + * @param {React.MouseEvent} e + * @returns + */ + function handleEnterNode(e) { + /* istanbul ignore if */ + if (!isPossibleDrag) { + // shouldn't happen + return; + } + const node = e.currentTarget; + // To have breadcrumb leaf node as disabled by specfying isCurrentPage prop which adds the below class + // and for the same page drop shouldn't be allowed hence the below condition + if (!node.classList.contains(`${prefix}--breadcrumb-item--current`)) { + const link = node.querySelector(`.${prefix}--link`); + const linkRect = link.getBoundingClientRect(); + // There is another way to have breadcrumb leaf node as disabled by not setting the href, + // in any case it should be a enabled hyperlink to consider as drop target + // istanbul ignore else + if (link.nodeType === Node.ELEMENT_NODE && link.tagName.toLowerCase() === 'a') { + const verticalPadding = 5; + const horizontalPadding = 4; + const style = { + display: 'block', + top: `${linkRect.top - verticalPadding}px`, + left: `${linkRect.left - horizontalPadding}px`, + height: `${linkRect.height + 2 * verticalPadding}px`, + width: `${linkRect.width + 2 * horizontalPadding}px`, + zIndex: -1, + }; + // istanbul ignore else + if (overlayRef.current) { + Object.assign(overlayRef.current.style, style); + overlayRef.current.classList.add(`${iotPrefix}--breadcrumb-drop-node-overlay`); + } + + link.classList.add(`${prefix}--row--on--link--dropping`); + activeDropRowIdRef.current = node; + } + } + } + + /** + * Callback when the mouse leaves a breadcrumb node. Removes the class added on mouse enter + * @param {React.MouseEvent} e + */ + function handleLeaveNode(e) { + const node = e.currentTarget; + /* istanbul ignore else */ + if (activeDropRowIdRef.current === node) { + activeDropRowIdRef.current = null; + const link = node.querySelector(`.${prefix}--link`); + link.classList.remove(`${prefix}--row--on--link--dropping`); + /* istanbul ignore else */ + if (overlayRef.current) { + overlayRef.current.classList.remove(`${iotPrefix}--breadcrumb-drop-node-overlay`); + overlayRef.current.style.display = 'none'; + } + } + } + + /** + * Attaches event listeners to list nodes of overflow menu items of breadcrumb + */ + function handleMenuItemEnter() { + const items = document.body.querySelectorAll(`.breadcrumb--overflow-items`); + /* istanbul ignore else */ + if (items && items.length > 0) { + const listItems = items[0].querySelectorAll(`.${prefix}--overflow-menu-options__option`); + listItems.forEach((li) => { + li.addEventListener('mouseenter', handleOverflowLiEnter); + li.addEventListener('mouseleave', handleOverflowLiLeave); + }); + } + } + + /** + * Removes event listeners from list nodes of overflow menu items of breadcrumb + */ + function handleMenuItemLeave() { + const items = document.body.querySelectorAll(`.breadcrumb--overflow-items`); + /* istanbul ignore else */ + if (items && items.length > 0) { + const listItems = items[0].querySelectorAll(`.${prefix}--overflow-menu-options__option`); + listItems.forEach((li) => { + li.removeEventListener('mouseenter', handleOverflowLiEnter); + li.removeEventListener('mouseleave', handleOverflowLiLeave); + }); + } + } + + /** + * Finds and attaches event listeners to all kind of possible breadcrumb nodes upon drag start setUpDnd is invoked + */ + function addBreadcrumbEventListners() { + // Nodes which are visible are selected using common class name + const elements = document.querySelectorAll(`.${prefix}--breadcrumb-item`); + elements.forEach((element) => { + element.addEventListener('mouseenter', handleEnterNode); + element.addEventListener('mouseleave', handleLeaveNode); + }); + + // Detects if overflow menu containing hidden nodes is open or not + const elementExist = !!document.querySelectorAll(`.breadcrumb--overflow-items`); + // istanbul ignore else + if (elementExist) { + handleMenuItemEnter(); + } + } + + /** + * Removes all the event listeners attached to breadcrumb nodes + */ + function removeBreadcrumbEventListeners() { + const elements = document.querySelectorAll(`.${prefix}--breadcrumb-item`); + elements.forEach((element) => { + element.removeEventListener('mouseenter', handleEnterNode); + element.removeEventListener('mouseleave', handleLeaveNode); + }); + + const elementExist = !!document.querySelectorAll(`.breadcrumb--overflow-items`); + // istanbul ignore else + if (elementExist) { + handleMenuItemLeave(); + } + } + /** * Stops an event. This is used to stop click event during drag and drop, otherwise the * mouseup can trigger a click on the row and the row click handler will be invoked. For @@ -299,6 +455,10 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { document.body.addEventListener('mouseup', handleDrop); document.body.addEventListener('mousemove', handleDragMove); document.body.addEventListener('keydown', handleEscapeKey); + // istanbul ignore else + if (hasBreadcrumbDrop) { + addBreadcrumbEventListners(); + } // If the user goes to another window, cancel the dnd. window.addEventListener('blur', cancel); @@ -310,12 +470,20 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { document.body.classList.add(`${iotPrefix}--is-dragging`); return function tearDown() { - // console.debug('Clean up table DnD'); document.body.removeEventListener('click', stopEvent, true); document.body.removeEventListener('mouseup', handleDrop); document.body.removeEventListener('mousemove', handleDragMove); document.body.removeEventListener('keydown', handleEscapeKey); - + // istanbul ignore else + if (hasBreadcrumbDrop) { + removeBreadcrumbEventListeners(); + // removes style override of hyperlink on drop which was added on drag start + // istanbul ignore else + if (activeDropRowIdRef.current && activeDropRowIdRef.current.children) + activeDropRowIdRef.current.children[0].classList.remove( + `${prefix}--row--on--link--dropping` + ); + } window.removeEventListener('blur', cancel); document.body.classList.remove(`${iotPrefix}--is-dragging`); @@ -326,7 +494,15 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { setIsDragging(false); }; }, - [isPossibleDrag, handleDragMove, handleDrop, cancel, setIsDragging, setDragRowIds] + [ + isPossibleDrag, + handleDragMove, + handleDrop, + cancel, + setIsDragging, + setDragRowIds, + hasBreadcrumbDrop, + ] ); const handleStartPossibleDrag = useCallback( @@ -355,9 +531,6 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { [setDragRowIds, selectedIds] ); - /** Ref to the root DOM element of the row overlay. Used to directly update its style. */ - const overlayRef = useRef(null); - const handleEnterRow = useCallback( /** * Callback when the mouse enters a table row. The table needs to add this as an onmouseenter @@ -422,7 +595,9 @@ function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop) { {isDragging && } {/* We can show the preview even if not isDragging during snapback time. */} {dragPreview && ( - + // For breadcrumb overflow menu has zindex of 6000 hence to show the drag above it given + // value 6001 when breadcrumb drop is enabled. + {dragPreview} )} diff --git a/packages/react/src/components/Table/__snapshots__/Table.main.story.storyshot b/packages/react/src/components/Table/__snapshots__/Table.main.story.storyshot index 6830ac1ace..25b6952cbe 100644 --- a/packages/react/src/components/Table/__snapshots__/Table.main.story.storyshot +++ b/packages/react/src/components/Table/__snapshots__/Table.main.story.storyshot @@ -32184,6 +32184,88 @@ exports[`Storybook Snapshot tests and console checks Storyshots 1 - Watson IoT/T > Reset Rows +
+ +
{ + if(typeof dropRowIdOrNode === 'string'){ + console.info(`Dropped ${dragRowIds} onto ${dropRowId}`); + }else if (dropRowIdOrNode instanceof Element || (dropRowIdOrNode && dropRowIdOrNode.nodeType === Node.ELEMENT_NODE)){ + //for nodes residing in overflow menus need to read title through innerText + let breadcrumbTitle = dropRowIdOrNode.title && dropRowIdOrNode.title != "" ? dropRowIdOrNode.title : dropRowIdOrNode.innerText; + console.info(`Dropped ${dragRowIds} onto ${breadcrumbTitle}`); + } +} +... +``` diff --git a/packages/react/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap b/packages/react/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap index 8af4903fac..b909ebce7e 100644 --- a/packages/react/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap +++ b/packages/react/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap @@ -801,6 +801,7 @@ Map { "hasAdvancedFilter": false, "hasAggregations": false, "hasBatchActionToolbar": true, + "hasBreadcrumbDrop": false, "hasColumnSelection": false, "hasColumnSelectionConfig": false, "hasDragAndDrop": false, @@ -1607,6 +1608,9 @@ Map { "hasBatchActionToolbar": Object { "type": "bool", }, + "hasBreadcrumbDrop": Object { + "type": "bool", + }, "hasColumnSelection": Object { "type": "bool", }, @@ -3285,6 +3289,7 @@ Map { "dismissText": "Dismiss", "expandedIds": Array [], "expandedRows": Array [], + "hasBreadcrumbDrop": false, "hasDragAndDrop": false, "hasRowActions": false, "hasRowExpansion": false, @@ -3529,6 +3534,9 @@ Map { ], "type": "arrayOf", }, + "hasBreadcrumbDrop": Object { + "type": "bool", + }, "hasDragAndDrop": Object { "type": "bool", },