Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Commit b510ffa

Browse files
Issue 710 - Markdown link click (#787)
1 parent 5c239f1 commit b510ffa

File tree

14 files changed

+145
-43
lines changed

14 files changed

+145
-43
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2
33
jobs:
44
"server-test":
55
docker:
6-
- image: circleci/python:3.7-node-browsers
6+
- image: circleci/python:3.7.6-node-browsers
77
- image: cypress/base:10
88

99
steps:

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [Unreleased]
6+
### Added
7+
- [#787](https://github.com/plotly/dash-table/pull/787) Add `cell_selectable` property to allow/disallow cell selection
8+
9+
### Changed
10+
- [#787](https://github.com/plotly/dash-table/pull/787)
11+
- Clicking on a link in a Markdown cell now requires a single click instead of two
12+
- Links in Markdown cells now open a new tab (target="_blank")
13+
514
## [4.7.0] - 2020-05-05
615
### Added
716
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling

src/dash-table/components/CellMarkdown/index.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React, {
55
import DOM from 'core/browser/DOM';
66
import { memoizeOne } from 'core/memoizer';
77

8-
import MarkdownHighlighter from 'dash-table/utils/MarkdownHighlighter';
8+
import Markdown from 'dash-table/utils/Markdown';
99

1010
interface IProps {
1111
active: boolean;
@@ -18,15 +18,15 @@ export default class CellMarkdown extends PureComponent<IProps, {}> {
1818

1919
getMarkdown = memoizeOne((value: string, _ready: any) => ({
2020
dangerouslySetInnerHTML: {
21-
__html: MarkdownHighlighter.render(String(value))
21+
__html: Markdown.render(String(value))
2222
}
2323
}));
2424

2525
constructor(props: IProps) {
2626
super(props);
2727

28-
if (MarkdownHighlighter.isReady !== true) {
29-
MarkdownHighlighter.isReady.then(() => { this.setState({}); });
28+
if (Markdown.isReady !== true) {
29+
Markdown.isReady.then(() => { this.setState({}); });
3030
}
3131
}
3232

@@ -47,7 +47,7 @@ export default class CellMarkdown extends PureComponent<IProps, {}> {
4747
return (<div
4848
ref='el'
4949
className={[className, 'cell-markdown'].join(' ')}
50-
{...this.getMarkdown(value, MarkdownHighlighter.isReady)}
50+
{...this.getMarkdown(value, Markdown.isReady)}
5151
/>);
5252
}
5353

src/dash-table/components/EdgeFactory.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export default class EdgeFactory {
186186
}
187187

188188
private memoizedCreateEdges = memoizeOne((
189-
active_cell: ICellCoordinates,
189+
active_cell: ICellCoordinates | undefined,
190190
columns: Columns,
191191
visibleColumns: Columns,
192192
operations: number,

src/dash-table/components/Table/props.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ export interface IProps {
301301
tooltip_conditional: ConditionalTooltip[];
302302

303303
active_cell?: ICellCoordinates;
304+
cell_selectable?: boolean;
304305
column_selectable?: Selection;
305306
columns?: Columns;
306307
dropdown?: StaticDropdowns;
@@ -351,35 +352,35 @@ export interface IProps {
351352
}
352353

353354
interface IDefaultProps {
354-
active_cell: ICellCoordinates;
355+
cell_selectable: boolean;
355356
column_selectable: Selection;
357+
css: IStylesheetRule[];
356358
dropdown: StaticDropdowns;
357359
dropdown_conditional: ConditionalDropdowns;
358360
dropdown_data: DataDropdowns;
359-
css: IStylesheetRule[];
360361
editable: boolean;
362+
end_cell: ICellCoordinates;
361363
export_columns: ExportColumns;
362364
export_format: ExportFormat;
363365
export_headers: ExportHeaders;
364366
fill_width: boolean;
365367
filter_query: string;
366368
filter_action: TableAction;
367-
include_headers_on_copy_paste: boolean;
368-
merge_duplicate_headers: boolean;
369369
fixed_columns: Fixed;
370370
fixed_rows: Fixed;
371+
include_headers_on_copy_paste: boolean;
372+
merge_duplicate_headers: boolean;
371373
row_deletable: boolean;
372374
row_selectable: Selection;
373375
selected_cells: SelectedCells;
374376
selected_columns: string[];
375-
start_cell: ICellCoordinates;
376-
end_cell: ICellCoordinates;
377-
selected_rows: Indices;
378377
selected_row_ids: RowId[];
378+
selected_rows: Indices;
379379
sort_action: TableAction;
380380
sort_by: SortBy;
381381
sort_mode: SortMode;
382382
sort_as_null: SortAsNull;
383+
start_cell: ICellCoordinates;
383384
style_as_list_view: boolean;
384385
tooltip_data: DataTooltips;
385386

@@ -475,8 +476,9 @@ export type HeaderFactoryProps = ControlledTableProps & {
475476
};
476477

477478
export interface ICellFactoryProps {
478-
active_cell: ICellCoordinates;
479+
active_cell?: ICellCoordinates;
479480
applyFocus?: boolean;
481+
cell_selectable: boolean;
480482
dropdown: StaticDropdowns;
481483
dropdown_conditional: ConditionalDropdowns;
482484
dropdown_data: DataDropdowns;

src/dash-table/dash/DataTable.js

+7
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export const defaultProps = {
8585
selected_columns: [],
8686
selected_rows: [],
8787
selected_row_ids: [],
88+
cell_selectable: true,
8889
row_selectable: false,
8990

9091
style_table: {},
@@ -622,6 +623,12 @@ export const propTypes = {
622623
*/
623624
row_deletable: PropTypes.bool,
624625

626+
/**
627+
* If True (default), then it is possible to click and navigate
628+
* table cells.
629+
*/
630+
cell_selectable: PropTypes.bool,
631+
625632
/**
626633
* If `single`, then the user can select a single row
627634
* via a radio button that will appear next to each row.

src/dash-table/dash/Sanitizer.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
ExportFormat,
1717
ExportHeaders,
1818
IFilterAction,
19-
FilterLogicalOperator
19+
FilterLogicalOperator,
20+
SelectedCells
2021
} from 'dash-table/components/Table/props';
2122
import headerRows from 'dash-table/derived/header/headerRows';
2223
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
@@ -33,6 +34,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = {
3334

3435
const DEFAULT_NULLY = '';
3536
const DEFAULT_SPECIFIER = '';
37+
const NULL_SELECTED_CELLS: SelectedCells = [];
3638

3739
const data2number = (data?: any) => +data || 0;
3840

@@ -99,7 +101,16 @@ export default class Sanitizer {
99101
headerFormat = ExportHeaders.Ids;
100102
}
101103

104+
const active_cell = props.cell_selectable ?
105+
props.active_cell :
106+
undefined;
107+
108+
const selected_cells = props.cell_selectable ?
109+
props.selected_cells :
110+
NULL_SELECTED_CELLS;
111+
102112
return R.merge(props, {
113+
active_cell,
103114
columns,
104115
data,
105116
export_headers: headerFormat,
@@ -108,6 +119,7 @@ export default class Sanitizer {
108119
fixed_rows: getFixedRows(props.fixed_rows, columns, props.filter_action),
109120
loading_state: dataLoading(props.loading_state),
110121
locale_format,
122+
selected_cells,
111123
visibleColumns
112124
});
113125
}

src/dash-table/derived/cell/wrapperStyles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const getter = (
3030
styles: IConvertedStyle[],
3131
data: Data,
3232
offset: IViewportOffset,
33-
activeCell: ICellCoordinates,
33+
activeCell: ICellCoordinates | undefined,
3434
selectedCells: SelectedCells
3535
) => {
3636
baseline = shallowClone(baseline);

src/dash-table/handlers/cellEvents.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { min, max, set, lensPath } from 'ramda';
2-
import { ICellFactoryProps } from 'dash-table/components/Table/props';
2+
import { ICellFactoryProps, Presentation } from 'dash-table/components/Table/props';
33
import isActive from 'dash-table/derived/cell/isActive';
44
import isSelected from 'dash-table/derived/cell/isSelected';
55
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';
@@ -12,6 +12,7 @@ export const handleClick = (
1212
e: any
1313
) => {
1414
const {
15+
cell_selectable,
1516
selected_cells,
1617
active_cell,
1718
setProps,
@@ -29,7 +30,14 @@ export const handleClick = (
2930
return;
3031
}
3132

32-
e.preventDefault();
33+
const column = visibleColumns[col];
34+
if (column.presentation !== Presentation.Markdown) {
35+
e.preventDefault();
36+
}
37+
38+
if (!cell_selectable) {
39+
return;
40+
}
3341

3442
/*
3543
* In some cases this will initiate browser text selection.
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Remarkable } from 'remarkable';
22
import LazyLoader from 'dash-table/LazyLoader';
33

4-
export default class MarkdownHighlighter {
4+
export default class Markdown {
55

66
static isReady: Promise<boolean> | true = new Promise<boolean>(resolve => {
7-
MarkdownHighlighter.hljsResolve = resolve;
7+
Markdown.hljsResolve = resolve;
88
});
99

1010
static render = (value: string) => {
11-
return MarkdownHighlighter.md.render(value);
11+
return Markdown.md.render(value);
1212
}
1313

1414
private static hljsResolve: () => any;
@@ -17,26 +17,27 @@ export default class MarkdownHighlighter {
1717

1818
private static readonly md: Remarkable = new Remarkable({
1919
highlight: (str: string, lang: string) => {
20-
if (MarkdownHighlighter.hljs) {
21-
if (lang && MarkdownHighlighter.hljs.getLanguage(lang)) {
20+
if (Markdown.hljs) {
21+
if (lang && Markdown.hljs.getLanguage(lang)) {
2222
try {
23-
return MarkdownHighlighter.hljs.highlight(lang, str).value;
23+
return Markdown.hljs.highlight(lang, str).value;
2424
} catch (err) { }
2525
}
2626

2727
try {
28-
return MarkdownHighlighter.hljs.highlightAuto(str).value;
28+
return Markdown.hljs.highlightAuto(str).value;
2929
} catch (err) { }
3030
} else {
31-
MarkdownHighlighter.loadhljs();
31+
Markdown.loadhljs();
3232
}
3333
return '';
34-
}
34+
},
35+
linkTarget:'_blank'
3536
});
3637

3738
private static async loadhljs() {
38-
MarkdownHighlighter.hljs = await LazyLoader.hljs;
39-
MarkdownHighlighter.hljsResolve();
40-
MarkdownHighlighter.isReady = true;
39+
Markdown.hljs = await LazyLoader.hljs;
40+
Markdown.hljsResolve();
41+
Markdown.isReady = true;
4142
}
4243
}

tests/cypress/tests/standalone/markdown_test.ts

-9
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,6 @@ describe('markdown cells', () => {
136136
});
137137
});
138138

139-
describe('clicking links', () => {
140-
it('correctly redirects', () => {
141-
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);
142-
// change href, since Cypress raises error when navigating away from localhost
143-
DashTable.getCellById(10, 'markdown-links').within(() => cy.get('.dash-cell-value > p > a').invoke('attr', 'href', '#testlinkclick').click().click());
144-
cy.url().should('include', `#testlinkclick`);
145-
});
146-
});
147-
148139
describe('loading highlightjs', () => {
149140
it('loads highlight.js and does not attach hljs to window', () => {
150141
cy.visit(`http://localhost:8080?mode=${AppMode.Markdown}`);

tests/selenium/test_markdown_link.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import dash
2+
from dash_table import DataTable
3+
import pytest
4+
5+
6+
def get_app(cell_selectable):
7+
md = "[Click me](https://www.google.com)"
8+
9+
data = [
10+
dict(a=md, b=md),
11+
dict(a=md, b=md),
12+
]
13+
14+
app = dash.Dash(__name__)
15+
16+
app.layout = DataTable(
17+
id="table",
18+
columns=[
19+
dict(name="a", id="a", type="text", presentation="markdown"),
20+
dict(name="b", id="b", type="text", presentation="markdown"),
21+
],
22+
data=data,
23+
cell_selectable=cell_selectable,
24+
)
25+
26+
return app
27+
28+
29+
@pytest.mark.parametrize("cell_selectable", [True, False])
30+
def test_tmdl001_copy_markdown_to_text(test, cell_selectable):
31+
test.start_server(get_app(cell_selectable))
32+
33+
target = test.table("table")
34+
35+
assert len(test.driver.window_handles) == 1
36+
target.cell(0, "a").get().find_element_by_css_selector("a").click()
37+
assert target.cell(0, "a").is_selected() == cell_selectable
38+
assert len(test.driver.window_handles) == 2
39+
40+
# Make sure the new tab is what's expected
41+
test.driver.switch_to_window(test.driver.window_handles[1])
42+
assert test.driver.current_url.startswith("https://www.google.com")
43+
44+
# Make sure the cell is still selected iff cell_selectable, after switching tabs
45+
test.driver.switch_to_window(test.driver.window_handles[0])
46+
assert target.cell(0, "a").is_selected() == cell_selectable

tests/selenium/test_navigation.py

+18
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,21 @@ def test_navg004_keyboard_between_md_and_standard_cells(test, props):
185185
test.send_keys(Keys.ARROW_RIGHT)
186186
test.send_keys(Keys.ARROW_DOWN)
187187
assert target.cell(i, i).is_focused()
188+
189+
190+
@pytest.mark.parametrize("cell_selectable", [True, False])
191+
def test_navg005_unselectable_cells(test, cell_selectable):
192+
app = dash.Dash(__name__)
193+
app.layout = DataTable(
194+
id="table",
195+
columns=[dict(id="a", name="a"), dict(id="b", name="b")],
196+
data=[dict(a=0, b=0), dict(a=1, b=2)],
197+
cell_selectable=cell_selectable,
198+
)
199+
200+
test.start_server(app)
201+
202+
target = test.table("table")
203+
target.cell(0, "a").click()
204+
205+
assert target.cell(0, "a").is_selected() == cell_selectable

tests/visual/percy-storybook/Style.percy.tsx

+11-3
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,15 @@ storiesOf('DashTable/Style type condition', module)
531531
{ row: 2, column: 1, column_id: 'b' },
532532
{ row: 2, column: 2, column_id: 'c' }]}
533533
active_cell={{ row: 1, column: 1 }}
534+
/>))
535+
.add('unselectable cells', () => (<DataTable
536+
{...DEFAULT_TABLE}
537+
id='unselectable-cells'
538+
cell_selectable={false}
539+
selected_cells={[
540+
{ row: 1, column: 1, column_id: 'b' },
541+
{ row: 1, column: 2, column_id: 'c' },
542+
{ row: 2, column: 1, column_id: 'b' },
543+
{ row: 2, column: 2, column_id: 'c' }]}
544+
active_cell={{ row: 1, column: 1 }}
534545
/>));
535-
536-
537-

0 commit comments

Comments
 (0)