diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e5d44720a..2310ef9376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#2109](https://github.com/plotly/dash/pull/2109) Add `maxHeight` to Dropdown options menu. ### Fixed +- [#2114](https://github.com/plotly/dash/pull/2114) Fix bug [#1978](https://github.com/plotly/dash/issues/1978) where text could not be copied from cells in tables with `cell_selectable=False`. - [#2102](https://github.com/plotly/dash/pull/2102) Fix bug as reported in [dash-labs #113](https://github.com/plotly/dash-labs/issues/113) where files starting with `.` were not excluded when building `dash.page_registry`. - [#2098](https://github.com/plotly/dash/pull/2098) Accept HTTP code 400 as well as 401 for JWT expiry - [#2097](https://github.com/plotly/dash/pull/2097) Fix bug [#2095](https://github.com/plotly/dash/issues/2095) with TypeScript compiler and `React.FC` empty valueDeclaration error & support empty props components. diff --git a/components/dash-table/src/dash-table/components/ControlledTable/index.tsx b/components/dash-table/src/dash-table/components/ControlledTable/index.tsx index 9248829670..91d6f77c17 100644 --- a/components/dash-table/src/dash-table/components/ControlledTable/index.tsx +++ b/components/dash-table/src/dash-table/components/ControlledTable/index.tsx @@ -794,14 +794,17 @@ export default class ControlledTable extends PureComponent include_headers_on_copy_paste } = this.props; - TableClipboardHelper.toClipboard( - e, - selected_cells, - columns, - visibleColumns, - viewport.data, - include_headers_on_copy_paste - ); + // if no cells are selected, fall back to the browser's default copy event handling + if (selected_cells.length) { + TableClipboardHelper.toClipboard( + e, + selected_cells, + columns, + visibleColumns, + viewport.data, + include_headers_on_copy_paste + ); + } this.$el.focus(); }; diff --git a/components/dash-table/src/dash-table/components/Table/Table.less b/components/dash-table/src/dash-table/components/Table/Table.less index 4677d77c05..bf2b5ffadf 100644 --- a/components/dash-table/src/dash-table/components/Table/Table.less +++ b/components/dash-table/src/dash-table/components/Table/Table.less @@ -427,7 +427,7 @@ height: 100%; width: 100%; - &.unfocused::selection { + &.unfocused.selectable::selection { background-color: transparent; } diff --git a/components/dash-table/src/dash-table/derived/cell/contents.tsx b/components/dash-table/src/dash-table/derived/cell/contents.tsx index 6797b83248..fa04207c07 100644 --- a/components/dash-table/src/dash-table/derived/cell/contents.tsx +++ b/components/dash-table/src/dash-table/derived/cell/contents.tsx @@ -64,10 +64,13 @@ function getCellType( export default (propsFn: () => ICellFactoryProps) => new Contents(propsFn); class Contents { + private cell_selectable: boolean; constructor( propsFn: () => ICellFactoryProps, private readonly handlers = derivedCellEventHandlerProps(propsFn) - ) {} + ) { + this.cell_selectable = propsFn().cell_selectable; + } partialGet = memoizeOne( ( @@ -167,6 +170,7 @@ class Contents { const className = [ ...(active ? ['input-active'] : []), isFocused ? 'focused' : 'unfocused', + ...(this.cell_selectable ? ['selectable'] : []), 'dash-cell-value' ].join(' '); diff --git a/components/dash-table/tests/selenium/test_basic_copy_paste.py b/components/dash-table/tests/selenium/test_basic_copy_paste.py index ba6228bafd..813b36a6a7 100644 --- a/components/dash-table/tests/selenium/test_basic_copy_paste.py +++ b/components/dash-table/tests/selenium/test_basic_copy_paste.py @@ -42,6 +42,15 @@ def get_app(): sort_action="native", include_headers_on_copy_paste=True, ), + DataTable( + id="table3", + data=df[0:10], + columns=[ + {"name": i, "id": i, "deletable": True} for i in rawDf.columns + ], + cell_selectable=False, + sort_action="native", + ), ] ) @@ -301,3 +310,27 @@ def test_tbcp009_copy_9_and_10_click(test): ) assert test.get_log_errors() == [] + + +def test_tbcp010_copy_from_unselectable_cells_table(test): + test.start_server(get_app()) + + source = test.table("table3") # this table has cell_selectable=False + target = test.table("table2") + + # double click cell to (natively) mark the contained text + source.cell(2, 2).double_click() + assert source.cell(2, 2).get_text() == test.get_selected_text() + + # copy the source text to clipboard using CTRL+C + test.copy() + + # assert the target cell value is different before paste + target.cell(1, 1).click() + assert target.cell(1, 1).get_text() != source.cell(2, 2).get_text() + + # assert the target cell value has changed to the pasted value + test.paste() + assert target.cell(1, 1).get_text() == source.cell(2, 2).get_text() + + assert test.get_log_errors() == []