diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f286445..70917f091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Fixed - [#817](https://github.com/plotly/dash-table/pull/817) Fix a regression introduced with [#722](https://github.com/plotly/dash-table/pull/722) causing the tooltips to be misaligned with respect to their parent cell +- [#818](https://github.com/plotly/dash-table/pull/818) Fix a regression causing copy/paste not to work when selecting a range of cells with Shift + mouse click ## [4.9.0] - 2020-07-27 ### Added diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index b21d51822..cd3f8a2dc 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -163,6 +163,28 @@ export default class ControlledTable extends PureComponent< this.handleDropdown(); this.adjustTooltipPosition(); + const {active_cell} = this.props; + + // Check if the focus is inside this table + if (this.containsActiveElement()) { + const active = this.getActiveCellAttributes(); + + // If there is an active cell and it does not have focus -> transfer focus + if ( + active && + active_cell && + (active.column_id !== active_cell?.column_id || + active.row !== active_cell?.row) + ) { + const target = this.$el.querySelector( + `td[data-dash-row="${active_cell.row}"][data-dash-column="${active_cell.column_id}"]:not(.phantom-cell)` + ) as HTMLElement; + if (target) { + target.focus(); + } + } + } + const {setState, uiCell, virtualization} = this.props; if (!virtualization) { @@ -194,11 +216,8 @@ export default class ControlledTable extends PureComponent< } handleClick = (event: any) => { - const $el = this.$el; - if ( - $el && - !$el.contains(event.target as Node) && + this.containsActiveElement() && /* * setProps is expensive, it causes excessive re-rendering in Dash. * so, only call when the table isn't already focussed, otherwise @@ -429,6 +448,52 @@ export default class ControlledTable extends PureComponent< return document.getElementById(this.props.id) as HTMLElement; } + private containsActiveElement(): boolean { + const $el = this.$el; + + return $el && $el.contains(document.activeElement); + } + + private getActiveCellAttributes(): { + column_id: string | null; + row: number | null; + } | void { + let activeElement = document.activeElement; + while (activeElement && activeElement.nodeName.toLowerCase() !== 'td') { + activeElement = activeElement.parentElement; + } + + if (!activeElement) { + return; + } + + const column_id = activeElement.getAttribute('data-dash-column'); + const row = activeElement.getAttribute('data-dash-row'); + + return {column_id, row: +(row ?? 0)}; + } + + /*#if TEST_COPY_PASTE*/ + private preventCopyPaste(): boolean { + if (!this.containsActiveElement()) { + return false; + } + + const {active_cell} = this.props; + const active = this.getActiveCellAttributes(); + + if ( + !active || + active.column_id !== active_cell?.column_id || + active.row !== active_cell?.row + ) { + return true; + } + + return false; + } + /*#endif*/ + handleKeyDown = (e: any) => { const {setProps, is_focused} = this.props; @@ -443,16 +508,20 @@ export default class ControlledTable extends PureComponent< if (ctrlDown && e.keyCode === KEY_CODES.V) { /*#if TEST_COPY_PASTE*/ - this.onPaste({} as any); e.preventDefault(); + if (!this.preventCopyPaste()) { + this.onPaste({} as any); + } /*#endif*/ return; } if (e.keyCode === KEY_CODES.C && ctrlDown && !is_focused) { /*#if TEST_COPY_PASTE*/ - this.onCopy(e as any); e.preventDefault(); + if (!this.preventCopyPaste()) { + this.onCopy(e as any); + } /*#endif*/ return; } diff --git a/tests/selenium/test_basic_copy_paste.py b/tests/selenium/test_basic_copy_paste.py index c35cb1d07..957caa8bf 100644 --- a/tests/selenium/test_basic_copy_paste.py +++ b/tests/selenium/test_basic_copy_paste.py @@ -1,4 +1,5 @@ import dash +import pytest from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate @@ -113,13 +114,20 @@ def test_tbcp002_sorted_copy_paste_callback(test): assert target.cell(2, 1).get_text() == "MODIFIED" -def test_tbcp003_copy_multiple_rows(test): +@pytest.mark.parametrize("mouse_navigation", [True, False]) +def test_tbcp003_copy_multiple_rows(test, mouse_navigation): test.start_server(get_app()) target = test.table("table") - with test.hold(Keys.SHIFT): + + if mouse_navigation: + with test.hold(Keys.SHIFT): + target.cell(0, 0).click() + target.cell(2, 0).click() + else: target.cell(0, 0).click() - target.cell(2, 0).click() + with test.hold(Keys.SHIFT): + test.send_keys(Keys.ARROW_DOWN + Keys.ARROW_DOWN) test.copy() target.cell(3, 0).click()