diff --git a/packages/@adobe/react-spectrum/src/table/table.css b/packages/@adobe/react-spectrum/src/table/table.css
index ed803cdca1f..489ca1dd4ca 100644
--- a/packages/@adobe/react-spectrum/src/table/table.css
+++ b/packages/@adobe/react-spectrum/src/table/table.css
@@ -225,10 +225,14 @@
/* forced-color-adjust: none, so that box-shadow style will render */
forced-color-adjust: none;
}
+}
- .react-spectrum-Table-cellWrapper.react-spectrum-Table-cellWrapper--dropTarget {
- background-color: var(--spectrum-table-droptarget-background-color);
- }
+/* Deliberately not chained with .react-spectrum-Table-cellWrapper: that class is only applied while
+ the cell height is not an estimate, so chaining it here drops the drop target background whenever
+ the table is still measuring. The .react-spectrum-Table ancestor supplies the specificity instead,
+ so this still wins over .spectrum-Table--quiet .spectrum-Table-row .spectrum-Table-cellWrapper. */
+.react-spectrum-Table .react-spectrum-Table-row .react-spectrum-Table-cellWrapper--dropTarget {
+ background-color: var(--spectrum-table-droptarget-background-color);
}
@media (forced-colors: active) {
diff --git a/packages/@adobe/react-spectrum/test/table/TableDnd.test.js b/packages/@adobe/react-spectrum/test/table/TableDnd.test.js
index acfc98faf70..922bb0cf991 100644
--- a/packages/@adobe/react-spectrum/test/table/TableDnd.test.js
+++ b/packages/@adobe/react-spectrum/test/table/TableDnd.test.js
@@ -3953,5 +3953,100 @@ describe('TableView', function () {
fireEvent.keyUp(document.body, {key: 'Escape'});
});
});
+
+ describe('drop target highlight', function () {
+ // The highlight is painted on the row (border) and on each of its cell wrappers (background),
+ // because the cell wrappers are opaque and sit on top of the row. See issue #5404.
+ function getCellWrappers(row) {
+ return [...row.children];
+ }
+
+ function expectHighlighted(row, isHighlighted) {
+ expect(row.className.includes('react-spectrum-Table-row--dropTarget')).toBe(isHighlighted);
+ let wrappers = getCellWrappers(row);
+ expect(wrappers.length).toBeGreaterThan(0);
+ for (let wrapper of wrappers) {
+ expect(wrapper.className.includes('react-spectrum-Table-cellWrapper--dropTarget')).toBe(
+ isHighlighted
+ );
+ }
+ }
+
+ it('should highlight the row and its cells while dragging over an item drop target', async function () {
+ let {getByRole} = render();
+ let grid = getByRole('grid');
+ let rows = within(within(grid).getAllByRole('rowgroup')[1]).getAllByRole('row');
+
+ let dragCell = within(rows[1]).getAllByRole('rowheader')[0];
+ let dataTransfer = new DataTransfer();
+ fireEvent.pointerDown(dragCell, {
+ pointerType: 'mouse',
+ button: 0,
+ pointerId: 1,
+ clientX: 0,
+ clientY: 0
+ });
+ fireEvent(dragCell, new DragEvent('dragstart', {dataTransfer, clientX: 0, clientY: 0}));
+ act(() => jest.runAllTimers());
+ expectHighlighted(rows[0], false);
+
+ // Drop onto the middle of the first row, which is an "on" drop position.
+ fireEvent(rows[0], new DragEvent('dragover', {dataTransfer, clientX: 1, clientY: 20}));
+ act(() => {
+ jest.advanceTimersByTime(100);
+ });
+
+ expectHighlighted(rows[0], true);
+ expectHighlighted(rows[3], false);
+
+ // "Three" is not a folder, so it only accepts insert positions. Dragging over it must clear
+ // the highlight rather than leaving it behind on the previous target.
+ fireEvent(rows[3], new DragEvent('dragover', {dataTransfer, clientX: 1, clientY: 145}));
+ act(() => {
+ jest.advanceTimersByTime(100);
+ });
+
+ expectHighlighted(rows[0], false);
+ expectHighlighted(rows[3], false);
+
+ fireEvent.pointerUp(dragCell, {
+ pointerType: 'mouse',
+ button: 0,
+ pointerId: 1,
+ clientX: 1,
+ clientY: 145
+ });
+ fireEvent(rows[3], new DragEvent('drop', {dataTransfer, clientX: 1, clientY: 145}));
+ fireEvent(dragCell, new DragEvent('dragend', {dataTransfer, clientX: 1, clientY: 145}));
+ act(() => jest.runAllTimers());
+ });
+
+ it('should highlight the row and its cells when a keyboard drag targets an item', async function () {
+ let {getByRole} = render();
+ let grid = getByRole('grid');
+ // Rows other than the active drop target are aria-hidden during a keyboard drag, so the
+ // hidden option is needed to reach them.
+ let findRow = name =>
+ within(grid)
+ .getAllByRole('row', {hidden: true})
+ .find(row => within(row).queryByText(name));
+
+ // Start a keyboard drag from "Folder 1". "Folder 2" is the only other row that accepts an
+ // "on" drop, so it is the first drop target.
+ await user.tab();
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on Folder 2');
+
+ expectHighlighted(findRow('Folder 2'), true);
+ expectHighlighted(findRow('Six'), false);
+
+ fireEvent.keyDown(document.body, {key: 'Escape'});
+ fireEvent.keyUp(document.body, {key: 'Escape'});
+ act(() => jest.runAllTimers());
+ expectHighlighted(findRow('Folder 2'), false);
+ });
+ });
});
});